Compare commits

..

14 Commits

Author SHA1 Message Date
Olivia Ruiz-Knott
caddea6758 Consider this for alert reference? 2020-06-30 16:02:54 -04:00
Justin Hynes
bbe9fef27b ugh, quality 2020-06-30 15:50:30 -04:00
Justin Hynes
1b9ff1446b Revert earlier change to avoid double conversion of data that is not needed. 2020-06-30 15:44:19 -04:00
Justin Hynes
38ab6d99ab Fix linting/quality issues. 2020-06-30 11:59:37 -04:00
Justin Hynes
77644bafc5 Add a few more tests. 2020-06-30 10:14:30 -04:00
Justin Hynes
74d9c18568 Add supporting pieces to start writing unit tests for DemographicsSection 2020-06-30 09:50:57 -04:00
Justin Hynes
8baccba066 Small update to error handling in postDemographics call 2020-06-30 09:50:56 -04:00
Justin Hynes
157f31cbc5 Add a ref and supporting callback(s) so we can focus the user on the alert if an error occurred updating their demographics data. 2020-06-30 09:50:55 -04:00
Justin Hynes
73d23c96b9 use lodash/get for page crash fix as recommended by TJ 2020-06-30 09:50:53 -04:00
Justin Hynes
09c0ef2b2b reverse unneeded changes in sagas.js 2020-06-30 09:50:51 -04:00
Justin Hynes
5ea94107af add an Alert to let the end user know there was an issue saving information on a PATCH call failure 2020-06-30 09:50:47 -04:00
Justin Hynes
f5d5eaa375 WIP, trying to update fetchSettings saga to pass back field errors to help determine if we need to show an alert for service issues. 2020-06-30 09:50:45 -04:00
Justin Hynes
a608c536b5 Work in progress on error message logic if Demographics is not available 2020-06-30 09:50:44 -04:00
Justin Hynes
0bfdcfabbb MICROBA-309 | Accounts MFE and Demographics continued
[MICROBA-309]
- Adjust error handling in getDemographics() call to do a POST to create the entity only when encountering a 404 on a GET
- Fix issue that can cause the Accounts page to error out if the Demographics service wasn't running. Add check for `demographics_user_ethnicity` prop existence before attempting to access the value
2020-06-30 09:50:41 -04:00
138 changed files with 17071 additions and 26464 deletions

45
.env
View File

@@ -1,27 +1,18 @@
ACCESS_TOKEN_COOKIE_NAME='' ACCESS_TOKEN_COOKIE_NAME=null
BASE_URL='' BASE_URL=null
COACHING_ENABLED='' CREDENTIALS_BASE_URL=null
CREDENTIALS_BASE_URL='' CSRF_TOKEN_API_PATH=null
CSRF_TOKEN_API_PATH='' ECOMMERCE_BASE_URL=null
DEMOGRAPHICS_BASE_URL='' LANGUAGE_PREFERENCE_COOKIE_NAME=null
DISCOVERY_API_BASE_URL='' LMS_BASE_URL=null
ECOMMERCE_BASE_URL='' DEMOGRAPHICS_BASE_URL=null
ENABLE_DEMOGRAPHICS_COLLECTION='' LOGIN_URL=null
FAVICON_URL='' LOGOUT_URL=null
LANGUAGE_PREFERENCE_COOKIE_NAME='' MARKETING_SITE_BASE_URL=null
LMS_BASE_URL='' NODE_ENV=null
LOGIN_URL='' ORDER_HISTORY_URL=null
LOGO_TRADEMARK_URL='' REFRESH_ACCESS_TOKEN_ENDPOINT=null
LOGO_URL='' SEGMENT_KEY=null
LOGO_WHITE_URL='' SITE_NAME=null
LOGOUT_URL='' SUPPORT_URL=null
MARKETING_SITE_BASE_URL='' USER_INFO_COOKIE_NAME=null
NODE_ENV=''
ORDER_HISTORY_URL=''
PUBLISHER_BASE_URL=''
REFRESH_ACCESS_TOKEN_ENDPOINT=''
SEGMENT_KEY=''
SITE_NAME=''
STUDIO_BASE_URL=''
SUPPORT_URL=''
USER_INFO_COOKIE_NAME=''

View File

@@ -1,28 +1,22 @@
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='localhost:1997' BASE_URL='localhost:1997'
COACHING_ENABLED=''
CREDENTIALS_BASE_URL='http://localhost:18150' CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token' CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
DISCOVERY_API_BASE_URL=''
ECOMMERCE_BASE_URL='http://localhost:18130' 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' LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000' LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login' LOGIN_URL='http://localhost:18000/login'
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
LOGOUT_URL='http://localhost:18000/logout' LOGOUT_URL='http://localhost:18000/logout'
MARKETING_SITE_BASE_URL='http://localhost:5335' MARKETING_SITE_BASE_URL='http://localhost:18000'
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
NODE_ENV='development' NODE_ENV='development'
ORDER_HISTORY_URL='http://localhost:1996/orders' ORDER_HISTORY_URL='localhost:1996/orders'
PORT=1997 PORT=1997
PUBLISHER_BASE_URL=''
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh' REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY='' SEGMENT_KEY=null
SITE_NAME=localhost SITE_NAME='edX'
STUDIO_BASE_URL=''
SUPPORT_URL='http://localhost:18000/support' SUPPORT_URL='http://localhost:18000/support'
USER_INFO_COOKIE_NAME='edx-user-info' USER_INFO_COOKIE_NAME='edx-user-info'
# Temporary, Remove this once we are ready to release the feature.
COACHING_ENABLED=true
ENABLE_DEMOGRAPHICS_COLLECTION=true

View File

@@ -1,27 +1,20 @@
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='localhost:1997' BASE_URL='localhost:1997'
COACHING_ENABLED=''
CREDENTIALS_BASE_URL='http://localhost:18150' CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token' CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
DISCOVERY_API_BASE_URL=''
ECOMMERCE_BASE_URL='http://localhost:18130' 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' LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000' LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login' LOGIN_URL='http://localhost:18000/login'
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
LOGOUT_URL='http://localhost:18000/logout' LOGOUT_URL='http://localhost:18000/logout'
MARKETING_SITE_BASE_URL='http://localhost:5335' MARKETING_SITE_BASE_URL='http://localhost:18000'
NODE_ENV='' DEMOGRAPHICS_BASE_URL='http://localhost:18360'
ORDER_HISTORY_URL='http://localhost:1996/orders' NODE_ENV=null
PUBLISHER_BASE_URL='' ORDER_HISTORY_URL='localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh' REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY='' SEGMENT_KEY=null
SITE_NAME=localhost SITE_NAME='edX'
STUDIO_BASE_URL=''
SUPPORT_URL='http://localhost:18000/support' SUPPORT_URL='http://localhost:18000/support'
USER_INFO_COOKIE_NAME='edx-user-info' USER_INFO_COOKIE_NAME='edx-user-info'
COACHING_ENABLED=''
ENABLE_DEMOGRAPHICS_COLLECTION=''

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
* @edx/community-engineering

View File

@@ -1,32 +0,0 @@
---
name: ci
on:
push:
branches:
- master
pull_request:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version:
- 12
npm-test:
- i18n_extract
- is-es5
- lint
- test
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm install -g npm@6
- run: make requirements
- run: make test NPM_TESTS=build
- run: make test NPM_TESTS=${{ matrix.npm-test }}
- name: upload coverage
uses: codecov/codecov-action@v2
with:
fail_ci_if_error: false

View File

@@ -1,10 +0,0 @@
# Run commitlint on the commit messages in a pull request.
name: Lint Commit Messages
on:
- pull_request
jobs:
commitlint:
uses: edx/.github/.github/workflows/commitlint.yml@master

1
.gitignore vendored
View File

@@ -16,4 +16,3 @@ temp/babel-plugin-react-intl
*~ *~
/temp /temp
/.vscode /.vscode
/module.config.js

15
.travis.yml Executable file
View File

@@ -0,0 +1,15 @@
language: node_js
node_js: 12
before_install:
- npm install -g npm@6
install:
- npm ci
script:
- make validate-no-uncommitted-package-lock-changes
- npm run i18n_extract
- npm run lint
- npm run test
- npm run build
- npm run is-es5
after_success:
- codecov

View File

@@ -10,19 +10,8 @@ tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transi
# This directory must match .babelrc . # This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl transifex_temp = ./temp/babel-plugin-react-intl
NPM_TESTS=build i18n_extract lint test is-es5 requirements:
npm install
.PHONY: test
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
.PHONY: test.npm.*
test.npm.%: validate-no-uncommitted-package-lock-changes
test -d node_modules || $(MAKE) requirements
npm run $(*)
.PHONY: requirements
requirements: ## install ci requirements
npm ci
i18n.extract: i18n.extract:
# Pulling display strings from .jsx files into .json files... # Pulling display strings from .jsx files into .json files...

View File

@@ -1,110 +1,53 @@
|ci-badge| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release| |Build Status| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
frontend-app-account frontend-app-account
==================== ====================
Please tag **@edx/community-engineering** on any PRs or issues. Thanks! This is a micro-frontend application responsible for the display and updating of a user's account information. Please tag **@edx/arch-team** on any PRs or issues.
Introduction Development
------------ -----------
This is a micro-frontend application responsible for the display and updating of a user's account information. Start Devstack
^^^^^^^^^^^^^^
What is the domain of this MFE? To use this application `devstack <https://github.com/edx/devstack>`__ must be running and you must be logged into it.
In this MFE: Private user settings UIs. Public facing profile is in a `separate MFE (Profile) <https://github.com/edx/frontend-app-profile>`_ - Start devstack
- Log in (http://localhost:18000/login)
- Account settings page Start the development server
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Demographics collection In this project, install requirements and start the development server by running:
- IDV (Identity Verification) .. code:: bash
Installation npm install
------------ npm start # The server will run on port 1997
This MFE is bundled with `Devstack <https://github.com/edx/devstack>`_, see the `Getting Started <https://github.com/edx/devstack#getting-started>`_ section for setup instructions. Once the dev server is up visit http://localhost:1997.
1. Install Devstack using the `Getting Started <https://github.com/edx/devstack#getting-started>`_ instructions. Configuration and Deployment
----------------------------
2. Start up Devstack, if it's not already started. This MFE is configured via node environment variables supplied at build time. See the .env file for the list of required environment variables. Example build syntax with a single environment variable:
3. Log in to Devstack (http://localhost:18000/login )
4. Within this project, install requirements and start the development server:
.. code-block::
npm install
npm start # The server will run on port 1997
5. Once the dev server is up, visit http://localhost:1997 to access the MFE
.. image:: ./docs/images/localhost_preview.png
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>`__.
The account settings micro-frontend also supports the following additional variable:
``SUPPORT_URL``
Example: ``https://support.example.com``
The fully-qualified URL to the support page in the target environment.
edX-specific Environment Variables
**********************************
Furthermore, there are several edX-specific environment variables that enable integrations with closed-source services private to the edX organization, and are unsupported in Open edX. Enabling these environment variables will result in undefined behavior in Open edX installations:
``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:
.. code:: bash .. code:: bash
NODE_ENV=development ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' npm run build NODE_ENV=development ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' npm run build
For more information see the document: `Micro-frontend applications in Open 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>`__. edX <https://github.com/edx/edx-developer-docs/blob/5191e800bf16cf42f25c58c58f983bdaf7f9305d/docs/micro-frontends-in-open-edx.rst>`__.
Known Issues Notes
------------ -----
None The production Webpack configuration for this repo uses `Purgecss <https://www.purgecss.com/>`__ to remove unused CSS from the production css file. In ``webpack.prod.config.js`` the Purgecss plugin is configured to scan directories to determine what css selectors should remain. Currently the src/ directory is scanned along with all ``@edx/frontend-component*`` node modules and ``@edx/paragon``. **If you add and use a component in this repo that relies on HTML classes or ids for styling you must add it to the Purgecss configuration or it will be unstyled in the production build.**
Development Roadmap .. |Build Status| image:: https://api.travis-ci.org/edx/frontend-app-account.svg?branch=master
------------------- :target: https://travis-ci.org/edx/frontend-app-account
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.
==============================
.. |ci-badge| image:: https://github.com/edx/edx-developer-docs/actions/workflows/ci.yml/badge.svg
:target: https://github.com/edx/edx-developer-docs/actions/workflows/ci.yml
:alt: Continuous Integration
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-app-account .. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-app-account
:target: https://codecov.io/gh/edx/frontend-app-account :target: https://codecov.io/gh/edx/frontend-app-account
.. |npm_version| image:: https://img.shields.io/npm/v/@edx/frontend-app-account.svg .. |npm_version| image:: https://img.shields.io/npm/v/@edx/frontend-app-account.svg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -1,7 +1,7 @@
const { createConfig } = require('@edx/frontend-build'); const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('jest', { module.exports = createConfig('jest', {
setupFilesAfterEnv: [ setupFiles: [
'<rootDir>/src/setupTest.js', '<rootDir>/src/setupTest.js',
], ],
}); });

View File

@@ -3,4 +3,5 @@
nick: acct nick: acct
oeps: {} oeps: {}
owner: edx/arch-team
openedx-release: {ref: master} openedx-release: {ref: master}

24168
package-lock.json generated

File diff suppressed because it is too large Load Diff

80
package.json Normal file → Executable file
View File

@@ -12,7 +12,7 @@
"build": "fedx-scripts webpack", "build": "fedx-scripts webpack",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null", "i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"is-es5": "es-check es5 ./dist/*.js", "is-es5": "es-check es5 ./dist/*.js",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .", "lint": "fedx-scripts eslint",
"snapshot": "fedx-scripts jest --updateSnapshot", "snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress", "start": "fedx-scripts webpack-dev-server --progress",
"test": "fedx-scripts jest --coverage --passWithNoTests" "test": "fedx-scripts jest --coverage --passWithNoTests"
@@ -29,27 +29,23 @@
"ie 11" "ie 11"
], ],
"dependencies": { "dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.1.0", "@edx/frontend-component-footer": "10.0.9",
"@edx/frontend-component-footer": "10.1.6", "@edx/frontend-component-header": "2.0.5",
"@edx/frontend-component-header": "^2.4.3", "@edx/frontend-platform": "1.1.14",
"@edx/frontend-platform": "1.12.7", "@edx/paragon": "7.1.5",
"@edx/paragon": "16.1.0", "@fortawesome/fontawesome-svg-core": "1.2.29",
"@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-brands-svg-icons": "5.8.2",
"@fortawesome/free-brands-svg-icons": "5.15.4", "@fortawesome/free-regular-svg-icons": "5.7.2",
"@fortawesome/free-regular-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "5.8.2",
"@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "0.1.11",
"@fortawesome/react-fontawesome": "0.1.15", "babel-polyfill": "6.26.0",
"@tensorflow-models/blazeface": "0.0.7", "classnames": "2.2.6",
"@tensorflow/tfjs-converter": "1.7.4",
"@tensorflow/tfjs-core": "1.7.4",
"bowser": "2.11.0",
"classnames": "2.3.1",
"core-js": "3.18.2",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"form-urlencoded": "4.0.1", "form-urlencoded": "4.0.1",
"formdata-polyfill": "4.0.10", "formdata-polyfill": "3.0.20",
"history": "4.10.1", "history": "4.10.1",
"jslib-html5-camera-photo": "3.1.8", "jslib-html5-camera-photo": "^3.1.3",
"lodash.camelcase": "4.3.0",
"lodash.debounce": "4.0.8", "lodash.debounce": "4.0.8",
"lodash.findindex": "4.6.0", "lodash.findindex": "4.6.0",
"lodash.get": "4.4.2", "lodash.get": "4.4.2",
@@ -57,36 +53,38 @@
"lodash.merge": "4.6.2", "lodash.merge": "4.6.2",
"lodash.omit": "4.5.0", "lodash.omit": "4.5.0",
"lodash.pick": "4.4.0", "lodash.pick": "4.4.0",
"lodash.pickby": "4.6.0", "lodash.pickby": "^4.6.0",
"memoize-one": "5.2.1", "lodash.snakecase": "4.1.1",
"memoize-one": "5.1.1",
"newrelic": "5.13.1",
"prop-types": "15.7.2", "prop-types": "15.7.2",
"qs": "6.10.1", "qs": "6.9.4",
"react": "16.14.0", "react": "16.10.2",
"react-dom": "16.14.0", "react-dom": "16.10.2",
"react-redux": "7.2.5", "react-redux": "7.1.3",
"react-router": "5.2.1", "react-router": "5.1.2",
"react-router-dom": "5.3.0", "react-router-dom": "5.1.2",
"react-router-hash-link": "1.2.2", "react-router-hash-link": "1.2.2",
"react-scrollspy": "3.4.3", "react-scrollspy": "3.4.2",
"react-transition-group": "4.4.2", "react-transition-group": "4.3.0",
"redux": "4.1.1", "redux": "4.0.5",
"redux-devtools-extension": "2.13.9", "redux-devtools-extension": "2.13.8",
"redux-logger": "3.0.6", "redux-logger": "3.0.6",
"redux-saga": "1.1.3", "redux-saga": "1.1.3",
"redux-thunk": "2.3.0", "redux-thunk": "2.3.0",
"regenerator-runtime": "0.13.9",
"reselect": "4.0.0", "reselect": "4.0.0",
"universal-cookie": "4.0.4" "universal-cookie": "4.0.3"
}, },
"devDependencies": { "devDependencies": {
"@edx/frontend-build": "^9.0.5", "@edx/frontend-build": "2.0.6",
"@testing-library/jest-dom": "5.14.1", "codecov": "3.6.5",
"@testing-library/react": "12.1.2", "enzyme": "3.10.0",
"codecov": "3.8.3", "enzyme-adapter-react-16": "1.15.2",
"enzyme": "3.11.0", "es-check": "5.0.0",
"enzyme-adapter-react-16": "1.15.6", "glob": "7.1.6",
"es-check": "6.0.0", "husky": "3.0.9",
"react-test-renderer": "16.14.0", "purgecss-webpack-plugin": "1.6.0",
"react-test-renderer": "16.8.6",
"reactifex": "1.1.1", "reactifex": "1.1.1",
"redux-mock-store": "1.5.4" "redux-mock-store": "1.5.4"
} }

View File

@@ -1,15 +1,10 @@
<!doctype html> <!doctype html>
<html lang="en-us"> <html lang="en-us">
<head> <head>
<title>Account | <%= process.env.SITE_NAME %></title> <title>Account | edX</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" /> <link rel="shortcut icon" href="/favicon.ico" 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> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -1,28 +1,9 @@
{ {
"extends": [ "extends": [
"config:base", "config:base"
"schedule:weekly",
":automergeLinters",
":automergeMinor",
":automergeTesters",
":enableVulnerabilityAlerts",
":rebaseStalePrs",
":semanticCommits",
":updateNotScheduled"
], ],
"packageRules": [ "patch": {
{ "automerge": true
"matchDepTypes": [ },
"devDependencies" "rebaseStalePrs": true
],
"matchUpdateTypes": [
"lockFileMaintenance",
"minor",
"patch",
"pin"
],
"automerge": true
}
],
"timezone": "America/New_York"
} }

View File

@@ -13,30 +13,20 @@ import {
getCountryList, getCountryList,
getLanguageList, getLanguageList,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { import { Hyperlink } from '@edx/paragon';
Button, Hyperlink, Icon, Alert,
} from '@edx/paragon';
import { CheckCircle, Error, WarningFilled } from '@edx/paragon/icons';
import messages from './AccountSettingsPage.messages'; import messages from './AccountSettingsPage.messages';
import { import { fetchSettings, saveSettings, updateDraft } from './data/actions';
fetchSettings,
saveMultipleSettings,
saveSettings,
updateDraft,
beginNameChange,
} from './data/actions';
import { accountSettingsPageSelector } from './data/selectors'; import { accountSettingsPageSelector } from './data/selectors';
import PageLoading from './PageLoading'; import PageLoading from './PageLoading';
import Alert from './Alert';
import JumpNav from './JumpNav'; import JumpNav from './JumpNav';
import DeleteAccount from './delete-account'; import DeleteAccount from './delete-account';
import EditableField from './EditableField'; import EditableField from './EditableField';
import ResetPassword from './reset-password'; import ResetPassword from './reset-password';
import NameChange from './name-change';
import ThirdPartyAuth from './third-party-auth'; import ThirdPartyAuth from './third-party-auth';
import BetaLanguageBanner from './BetaLanguageBanner'; import BetaLanguageBanner from './BetaLanguageBanner';
import EmailField from './EmailField'; import EmailField from './EmailField';
import OneTimeDismissibleAlert from './OneTimeDismissibleAlert';
import { import {
YEAR_OF_BIRTH_OPTIONS, YEAR_OF_BIRTH_OPTIONS,
EDUCATION_LEVELS, EDUCATION_LEVELS,
@@ -54,8 +44,8 @@ class AccountSettingsPage extends React.Component {
// If there is a "duplicate_provider" query parameter, that's the backend's // 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 // 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, // to another Open edX account. We use this to display a message to that effect, and remove the
// and remove the parameter from the URL. // parameter from the URL.
const duplicateTpaProvider = getQueryParameters().duplicate_provider; const duplicateTpaProvider = getQueryParameters().duplicate_provider;
if (duplicateTpaProvider !== undefined) { if (duplicateTpaProvider !== undefined) {
history.replace(history.location.pathname); history.replace(history.location.pathname);
@@ -63,16 +53,6 @@ class AccountSettingsPage extends React.Component {
this.state = { this.state = {
duplicateTpaProvider, duplicateTpaProvider,
}; };
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(),
'#delete-account': React.createRef(),
};
} }
componentDidMount() { componentDidMount() {
@@ -85,20 +65,6 @@ class AccountSettingsPage extends React.Component {
}); });
} }
componentDidUpdate(prevProps) {
if (prevProps.loading && !prevProps.loaded && this.props.loaded) {
const locationHash = global.location.hash;
// Check for the locationHash in the URL and then scroll to it if it is in the
// NavLinks list
if (typeof locationHash !== 'string') {
return;
}
if (Object.keys(this.navLinkRefs).includes(locationHash) && this.navLinkRefs[locationHash].current) {
window.scrollTo(0, this.navLinkRefs[locationHash].current.offsetTop);
}
}
}
// NOTE: We need 'locale' for the memoization in getLocalizedTimeZoneOptions. Don't remove it! // NOTE: We need 'locale' for the memoization in getLocalizedTimeZoneOptions. Don't remove it!
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
getLocalizedTimeZoneOptions = memoize((timeZoneOptions, countryTimeZoneOptions, locale) => { getLocalizedTimeZoneOptions = memoize((timeZoneOptions, countryTimeZoneOptions, locale) => {
@@ -146,57 +112,6 @@ class AccountSettingsPage extends React.Component {
})), })),
})); }));
sortDates = (a, b) => {
const aTimeSinceEpoch = new Date(a).getTime();
const bTimeSinceEpoch = new Date(b).getTime();
return bTimeSinceEpoch - aTimeSinceEpoch;
}
sortVerifiedNameRecords = verifiedNameHistory => {
if (Array.isArray(verifiedNameHistory)) {
return [...verifiedNameHistory].sort(this.sortDates);
}
return [];
}
handleEditableFieldChange = (name, value) => {
this.props.updateDraft(name, value);
}
handleSubmit = (formId, values) => {
this.props.saveSettings(formId, values);
}
handleSubmitProfileName = (formId, values) => {
if (Object.keys(this.props.drafts).includes('useVerifiedNameForCerts')) {
this.props.saveMultipleSettings([
{
formId,
commitValues: values,
},
{
formId: 'useVerifiedNameForCerts',
commitValues: this.props.formValues.useVerifiedNameForCerts,
},
], formId);
} else {
this.props.saveSettings(formId, values);
}
};
handleSubmitVerifiedName = (formId, values) => {
if (Object.keys(this.props.drafts).includes('useVerifiedNameForCerts')) {
this.props.saveSettings('useVerifiedNameForCerts', this.props.formValues.useVerifiedNameForCerts);
}
if (values !== this.props.committedValues?.verified_name) {
this.props.beginNameChange(formId);
} else {
this.props.saveSettings(formId, values);
}
}
isEditable(fieldName) { isEditable(fieldName) {
return !this.props.staticFields.includes(fieldName); return !this.props.staticFields.includes(fieldName);
} }
@@ -207,6 +122,14 @@ class AccountSettingsPage extends React.Component {
return Boolean(this.props.profileDataManager); return Boolean(this.props.profileDataManager);
} }
handleEditableFieldChange = (name, value) => {
this.props.updateDraft(name, value);
};
handleSubmit = (formId, values) => {
this.props.saveSettings(formId, values);
};
renderDuplicateTpaProviderMessage() { renderDuplicateTpaProviderMessage() {
if (!this.state.duplicateTpaProvider) { if (!this.state.duplicateTpaProvider) {
return null; return null;
@@ -214,14 +137,13 @@ class AccountSettingsPage extends React.Component {
return ( return (
<div> <div>
<Alert variant="danger"> <Alert className="alert alert-danger" role="alert">
<FormattedMessage <FormattedMessage
id="account.settings.message.duplicate.tpa.provider" id="account.settings.message.duplicate.tpa.provider"
defaultMessage="The {provider} account you selected is already linked to another {siteName} account." defaultMessage="The {provider} account you selected is already linked to another edX account."
description="alert message informing the user that the third-party account they attempted to link is already linked to another account" description="alert message informing the user that the third-party account they attempted to link is already linked to another edX account"
values={{ values={{
provider: <b>{this.state.duplicateTpaProvider}</b>, provider: <b>{this.state.duplicateTpaProvider}</b>,
siteName: getConfig().SITE_NAME,
}} }}
/> />
</Alert> </Alert>
@@ -236,7 +158,7 @@ class AccountSettingsPage extends React.Component {
return ( return (
<div> <div>
<Alert variant="info"> <Alert className="alert alert-primary" role="alert">
<FormattedMessage <FormattedMessage
id="account.settings.message.managed.settings" id="account.settings.message.managed.settings"
defaultMessage="Your profile settings are managed by {managerTitle}. Contact your administrator or {support} for help." defaultMessage="Your profile settings are managed by {managerTitle}. Contact your administrator or {support} for help."
@@ -259,156 +181,6 @@ class AccountSettingsPage extends React.Component {
); );
} }
renderFullNameHelpText = (status) => {
if (
!this.props.verifiedNameHistory
|| !this.props.verifiedNameEnabled
) {
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text']);
}
switch (status) {
case 'submitted':
if (this.props.committedValues.useVerifiedNameForCerts) {
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.submitted']);
}
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.submitted.certificate']);
default:
if (this.props.committedValues.useVerifiedNameForCerts) {
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.non.certificate']);
}
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.certificate']);
}
}
renderVerifiedNameSuccessMessage = (verifiedName, created) => {
const dateValue = new Date(created).valueOf();
const id = `dismissedVerifiedNameSuccessMessage-${verifiedName}-${dateValue}`;
return (
<OneTimeDismissibleAlert
id={id}
variant="success"
icon={CheckCircle}
header={this.props.intl.formatMessage(messages['account.settings.field.name.verified.success.message.header'])}
body={this.props.intl.formatMessage(messages['account.settings.field.name.verified.success.message'])}
/>
);
}
renderVerifiedNameFailureMessage = (verifiedName, created) => {
const dateValue = new Date(created).valueOf();
const id = `dismissedVerifiedNameFailureMessage-${verifiedName}-${dateValue}`;
return (
<OneTimeDismissibleAlert
id={id}
variant="danger"
icon={Error}
header={this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message.header'])}
body={
(
<>
<div className="d-flex flex-row">
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message'])}
</div>
<div className="d-flex flex-row-reverse mt-3">
<Button
variant="primary"
href="https://support.edx.org/hc/en-us/articles/360004381594-Why-was-my-ID-verification-denied"
>
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message.help.link'])}
</Button>{' '}
</div>
</>
)
}
/>
);
}
renderVerifiedNameSubmittedMessage = (willCertNameChange) => (
<Alert
variant="warning"
icon={WarningFilled}
>
<Alert.Heading>
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.submitted.message.header'])}
</Alert.Heading>
<p>
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.submitted.message'])}{' '}
{
willCertNameChange
&& this.props.intl.formatMessage(messages['account.settings.field.name.verified.submitted.message.certificate'])
}
</p>
</Alert>
)
renderVerifiedNameMessage = verifiedNameRecord => {
const {
created,
status,
profile_name: profileName,
verified_name: verifiedName,
} = verifiedNameRecord;
let willCertNameChange = false;
if (
(
// User submitted a profile name change, and uses their profile name on certificates
this.props.committedValues.name !== profileName
&& !this.props.committedValues.useVerifiedNameForCerts
)
|| (
// User submitted a verified name change, and uses their verified name on certificates
this.props.committedValues.name === profileName
&& this.props.committedValues.useVerifiedNameForCerts
)
) {
willCertNameChange = true;
}
switch (status) {
case 'approved':
return this.renderVerifiedNameSuccessMessage(verifiedName, created);
case 'denied':
return this.renderVerifiedNameFailureMessage(verifiedName, created);
case 'submitted':
return this.renderVerifiedNameSubmittedMessage(willCertNameChange);
default:
return null;
}
}
renderVerifiedNameIcon = (status) => {
switch (status) {
case 'approved':
return (<Icon src={CheckCircle} className="ml-1" style={{ height: '18px', width: '18px', color: 'green' }} />);
case 'submitted':
return (<Icon src={WarningFilled} className="ml-1" style={{ height: '18px', width: '18px', color: 'yellow' }} />);
default:
return null;
}
}
renderVerifiedNameHelpText = (status) => {
switch (status) {
case 'approved':
if (this.props.committedValues.useVerifiedNameForCerts) {
return this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.certificate']);
}
return this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.verified']);
case 'submitted':
if (this.props.committedValues.useVerifiedNameForCerts) {
return this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.submitted.certificate']);
}
return this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.submitted']);
default:
return null;
}
}
renderEmptyStaticFieldMessage() { renderEmptyStaticFieldMessage() {
if (this.isManagedProfile()) { if (this.isManagedProfile()) {
return this.props.intl.formatMessage(messages['account.settings.static.field.empty'], { return this.props.intl.formatMessage(messages['account.settings.static.field.empty'], {
@@ -418,15 +190,8 @@ class AccountSettingsPage extends React.Component {
return this.props.intl.formatMessage(messages['account.settings.static.field.empty.no.admin']); return this.props.intl.formatMessage(messages['account.settings.static.field.empty.no.admin']);
} }
renderNameChangeModal() {
if (this.props.nameChangeModal && this.props.nameChangeModal.formId) {
return <NameChange targetFormId={this.props.nameChangeModal.formId} />;
}
return null;
}
renderSecondaryEmailField(editableFieldProps) { renderSecondaryEmailField(editableFieldProps) {
if (!this.props.formValues.secondary_email_enabled) { if (!Boolean(this.props.formValues.secondary_email_enabled)) {
return null; return null;
} }
@@ -442,16 +207,6 @@ class AccountSettingsPage extends React.Component {
); );
} }
renderDemographicsSection() {
// check the result of an LMS API call to determine if we should render the DemographicsSection component
if (this.props.formValues.shouldDisplayDemographicsSection) {
return (
<DemographicsSection forwardRef={this.navLinkRefs['#demographics-information']} />
);
}
return null;
}
renderContent() { renderContent() {
const editableFieldProps = { const editableFieldProps = {
onChange: this.handleEditableFieldChange, onChange: this.handleEditableFieldChange,
@@ -469,9 +224,7 @@ class AccountSettingsPage extends React.Component {
} = this.getLocalizedOptions(this.context.locale, this.props.formValues.country); } = this.getLocalizedOptions(this.context.locale, this.props.formValues.country);
// Show State field only if the country is US (could include Canada later) // Show State field only if the country is US (could include Canada later)
const showState = this.props.formValues.country === COUNTRY_WITH_STATES; const showState = this.props.formValues.country == COUNTRY_WITH_STATES;
const { verifiedName, verifiedNameEnabled } = this.props;
const timeZoneOptions = this.getLocalizedTimeZoneOptions( const timeZoneOptions = this.getLocalizedTimeZoneOptions(
this.props.timeZoneOptions, this.props.timeZoneOptions,
@@ -480,104 +233,50 @@ class AccountSettingsPage extends React.Component {
); );
const hasLinkedTPA = findIndex(this.props.tpaProviders, provider => provider.connected) >= 0; const hasLinkedTPA = findIndex(this.props.tpaProviders, provider => provider.connected) >= 0;
return (
<>
<div className="account-section" id="basic-information" ref={this.navLinkRefs['#basic-information']}>
{
verifiedNameEnabled && this.props.mostRecentVerifiedName
&& this.renderVerifiedNameMessage(this.props.mostRecentVerifiedName)
}
return (
<React.Fragment>
<div className="account-section" id="basic-information">
<h2 className="section-heading"> <h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.account.information'])} {this.props.intl.formatMessage(messages['account.settings.section.account.information'])}
</h2> </h2>
<p>{this.props.intl.formatMessage(messages['account.settings.section.account.information.description'])}</p> <p>{this.props.intl.formatMessage(messages['account.settings.section.account.information.description'])}</p>
{this.renderManagedProfileMessage()} {this.renderManagedProfileMessage()}
{this.renderNameChangeModal()}
<EditableField <EditableField
name="username" name="username"
type="text" type="text"
value={this.props.formValues.username} value={this.props.formValues.username}
label={this.props.intl.formatMessage(messages['account.settings.field.username'])} label={this.props.intl.formatMessage(messages['account.settings.field.username'])}
helpText={this.props.intl.formatMessage( helpText={this.props.intl.formatMessage(messages['account.settings.field.username.help.text'])}
messages['account.settings.field.username.help.text'],
{ siteName: getConfig().SITE_NAME },
)}
isEditable={false} isEditable={false}
{...editableFieldProps} {...editableFieldProps}
/> />
<EditableField <EditableField
name="name" name="name"
type="text" type="text"
value={ value={this.props.formValues.name}
verifiedNameEnabled
&& verifiedName?.status === 'submitted'
&& this.props.formValues.pending_name_change
? this.props.formValues.pending_name_change
: this.props.formValues.name
}
label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])} label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])}
emptyLabel={ emptyLabel={
this.isEditable('name') this.isEditable('name') ?
? this.props.intl.formatMessage(messages['account.settings.field.full.name.empty']) this.props.intl.formatMessage(messages['account.settings.field.full.name.empty']) :
: this.renderEmptyStaticFieldMessage() this.renderEmptyStaticFieldMessage()
} }
helpText={ helpText={this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])}
verifiedNameEnabled && verifiedName isEditable={this.isEditable('name')}
? this.renderFullNameHelpText(verifiedName.status) {...editableFieldProps}
: this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])
}
isEditable={
verifiedNameEnabled && verifiedName
? this.isEditable('verifiedName') && this.isEditable('name')
: this.isEditable('name')
}
isGrayedOut={
verifiedNameEnabled && verifiedName && !this.isEditable('verifiedName')
}
onChange={this.handleEditableFieldChange}
onSubmit={this.handleSubmitProfileName}
/> />
{verifiedNameEnabled && verifiedName
&& (
<EditableField
name="verified_name"
type="text"
value={this.props.formValues.verified_name}
label={
(
<div className="d-flex">
{this.props.intl.formatMessage(messages['account.settings.field.name.verified'])}
{
this.renderVerifiedNameIcon(verifiedName.status)
}
</div>
)
}
helpText={this.renderVerifiedNameHelpText(verifiedName.status)}
isEditable={this.isEditable('verifiedName')}
isGrayedOut={!this.isEditable('verifiedName')}
onChange={this.handleEditableFieldChange}
onSubmit={this.handleSubmitVerifiedName}
/>
)}
<EmailField <EmailField
name="email" name="email"
label={this.props.intl.formatMessage(messages['account.settings.field.email'])} label={this.props.intl.formatMessage(messages['account.settings.field.email'])}
emptyLabel={ emptyLabel={
this.isEditable('email') this.isEditable('email') ?
? this.props.intl.formatMessage(messages['account.settings.field.email.empty']) this.props.intl.formatMessage(messages['account.settings.field.email.empty']) :
: this.renderEmptyStaticFieldMessage() this.renderEmptyStaticFieldMessage()
} }
value={this.props.formValues.email} value={this.props.formValues.email}
confirmationMessageDefinition={messages['account.settings.field.email.confirmation']} confirmationMessageDefinition={messages['account.settings.field.email.confirmation']}
helpText={this.props.intl.formatMessage( helpText={this.props.intl.formatMessage(messages['account.settings.field.email.help.text'])}
messages['account.settings.field.email.help.text'],
{ siteName: getConfig().SITE_NAME },
)}
isEditable={this.isEditable('email')} isEditable={this.isEditable('email')}
{...editableFieldProps} {...editableFieldProps}
/> />
@@ -599,15 +298,14 @@ class AccountSettingsPage extends React.Component {
options={countryOptions} options={countryOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.country'])} label={this.props.intl.formatMessage(messages['account.settings.field.country'])}
emptyLabel={ emptyLabel={
this.isEditable('country') this.isEditable('country') ?
? this.props.intl.formatMessage(messages['account.settings.field.country.empty']) this.props.intl.formatMessage(messages['account.settings.field.country.empty']) :
: this.renderEmptyStaticFieldMessage() this.renderEmptyStaticFieldMessage()
} }
isEditable={this.isEditable('country')} isEditable={this.isEditable('country')}
{...editableFieldProps} {...editableFieldProps}
/> />
{showState {showState &&
&& (
<EditableField <EditableField
name="state" name="state"
type="select" type="select"
@@ -615,17 +313,17 @@ class AccountSettingsPage extends React.Component {
options={stateOptions} options={stateOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.state'])} label={this.props.intl.formatMessage(messages['account.settings.field.state'])}
emptyLabel={ emptyLabel={
this.isEditable('state') this.isEditable('state') ?
? this.props.intl.formatMessage(messages['account.settings.field.state.empty']) this.props.intl.formatMessage(messages['account.settings.field.state.empty']) :
: this.renderEmptyStaticFieldMessage() this.renderEmptyStaticFieldMessage()
} }
isEditable={this.isEditable('state')} isEditable={this.isEditable('state')}
{...editableFieldProps} {...editableFieldProps}
/> />
)} }
</div> </div>
<div className="account-section" id="profile-information" ref={this.navLinkRefs['#profile-information']}> <div className="account-section" id="profile-information">
<h2 className="section-heading"> <h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.profile.information'])} {this.props.intl.formatMessage(messages['account.settings.section.profile.information'])}
</h2> </h2>
@@ -657,27 +355,23 @@ class AccountSettingsPage extends React.Component {
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])} emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
{...editableFieldProps} {...editableFieldProps}
/> />
{getConfig().COACHING_ENABLED {getConfig().COACHING_ENABLED &&
&& this.props.formValues.coaching.eligible_for_coaching this.props.formValues.coaching.eligible_for_coaching &&
&& (
<CoachingToggle <CoachingToggle
name="coaching" name="coaching"
phone_number={this.props.formValues.phone_number} phone_number={this.props.formValues.phone_number}
coaching={this.props.formValues.coaching} coaching={this.props.formValues.coaching}
/> />
)} }
</div> </div>
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && this.renderDemographicsSection()} {getConfig().ENABLE_DEMOGRAPHICS_COLLECTION &&
<DemographicsSection />
}
<div className="account-section" id="social-media"> <div className="account-section" id="social-media">
<h2 className="section-heading"> <h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.social.media'])} {this.props.intl.formatMessage(messages['account.settings.section.social.media'])}
</h2> </h2>
<p> <p>{this.props.intl.formatMessage(messages['account.settings.section.social.media.description'])}</p>
{this.props.intl.formatMessage(
messages['account.settings.section.social.media.description'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<EditableField <EditableField
name="social_link_linkedin" name="social_link_linkedin"
@@ -705,7 +399,7 @@ class AccountSettingsPage extends React.Component {
/> />
</div> </div>
<div className="account-section" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}> <div className="account-section" id="site-preferences">
<h2 className="section-heading"> <h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.site.preferences'])} {this.props.intl.formatMessage(messages['account.settings.section.site.preferences'])}
</h2> </h2>
@@ -736,25 +430,20 @@ class AccountSettingsPage extends React.Component {
/> />
</div> </div>
<div className="account-section" id="linked-accounts" ref={this.navLinkRefs['#linked-accounts']}> <div className="account-section" id="linked-accounts">
<h2 className="section-heading">{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts'])}</h2> <h2 className="section-heading">{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts'])}</h2>
<p> <p>{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts.description'])}</p>
{this.props.intl.formatMessage(
messages['account.settings.section.linked.accounts.description'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<ThirdPartyAuth /> <ThirdPartyAuth />
</div> </div>
<div className="account-section" id="delete-account" ref={this.navLinkRefs['#delete-account']}> <div className="account-section" id="delete-account">
<DeleteAccount <DeleteAccount
isVerifiedAccount={this.props.isActive} isVerifiedAccount={this.props.isActive}
hasLinkedTPA={hasLinkedTPA} hasLinkedTPA={hasLinkedTPA}
/> />
</div> </div>
</> </React.Fragment>
); );
} }
@@ -790,9 +479,7 @@ class AccountSettingsPage extends React.Component {
<div> <div>
<div className="row"> <div className="row">
<div className="col-md-3"> <div className="col-md-3">
<JumpNav <JumpNav />
displayDemographicsLink={this.props.formValues.shouldDisplayDemographicsSection}
/>
</div> </div>
<div className="col-md-9"> <div className="col-md-9">
{loading ? this.renderLoading() : null} {loading ? this.renderLoading() : null}
@@ -820,13 +507,11 @@ AccountSettingsPage.propTypes = {
name: PropTypes.string, name: PropTypes.string,
email: PropTypes.string, email: PropTypes.string,
secondary_email: PropTypes.string, secondary_email: PropTypes.string,
secondary_email_enabled: PropTypes.bool,
year_of_birth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), year_of_birth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
country: PropTypes.string, country: PropTypes.string,
level_of_education: PropTypes.string, level_of_education: PropTypes.string,
gender: PropTypes.string, gender: PropTypes.string,
language_proficiencies: PropTypes.string, language_proficiencies: PropTypes.string,
pending_name_change: PropTypes.string,
phone_number: PropTypes.string, phone_number: PropTypes.string,
social_link_linkedin: PropTypes.string, social_link_linkedin: PropTypes.string,
social_link_facebook: PropTypes.string, social_link_facebook: PropTypes.string,
@@ -837,20 +522,7 @@ AccountSettingsPage.propTypes = {
user: PropTypes.number.isRequired, user: PropTypes.number.isRequired,
eligible_for_coaching: PropTypes.bool.isRequired, eligible_for_coaching: PropTypes.bool.isRequired,
}), }),
state: PropTypes.string,
shouldDisplayDemographicsSection: PropTypes.bool,
useVerifiedNameForCerts: PropTypes.bool.isRequired,
verified_name: PropTypes.string,
}).isRequired, }).isRequired,
committedValues: PropTypes.shape({
name: PropTypes.string,
useVerifiedNameForCerts: PropTypes.bool,
verified_name: PropTypes.string,
}),
drafts: PropTypes.shape({}),
formErrors: PropTypes.shape({
name: PropTypes.string,
}),
siteLanguage: PropTypes.shape({ siteLanguage: PropTypes.shape({
previousValue: PropTypes.string, previousValue: PropTypes.string,
draft: PropTypes.string, draft: PropTypes.string,
@@ -874,43 +546,15 @@ AccountSettingsPage.propTypes = {
})), })),
fetchSiteLanguages: PropTypes.func.isRequired, fetchSiteLanguages: PropTypes.func.isRequired,
updateDraft: PropTypes.func.isRequired, updateDraft: PropTypes.func.isRequired,
saveMultipleSettings: PropTypes.func.isRequired,
saveSettings: PropTypes.func.isRequired, saveSettings: PropTypes.func.isRequired,
fetchSettings: PropTypes.func.isRequired, fetchSettings: PropTypes.func.isRequired,
beginNameChange: PropTypes.func.isRequired, tpaProviders: PropTypes.arrayOf(PropTypes.object),
tpaProviders: PropTypes.arrayOf(PropTypes.shape({
connected: PropTypes.bool,
})),
nameChangeModal: PropTypes.shape({
formId: PropTypes.string,
}),
verifiedNameEnabled: PropTypes.bool,
verifiedName: PropTypes.shape({
verified_name: PropTypes.string,
status: PropTypes.string,
}),
mostRecentVerifiedName: PropTypes.shape({
verified_name: PropTypes.string,
status: PropTypes.string,
}),
verifiedNameHistory: PropTypes.arrayOf(
PropTypes.shape({
verified_name: PropTypes.string,
status: PropTypes.string,
}),
),
}; };
AccountSettingsPage.defaultProps = { AccountSettingsPage.defaultProps = {
loading: false, loading: false,
loaded: false, loaded: false,
loadingError: null, loadingError: null,
committedValues: {
useVerifiedNameForCerts: false,
verified_name: null,
},
drafts: {},
formErrors: {},
siteLanguage: null, siteLanguage: null,
siteLanguageOptions: [], siteLanguageOptions: [],
timeZoneOptions: [], timeZoneOptions: [],
@@ -920,18 +564,11 @@ AccountSettingsPage.defaultProps = {
tpaProviders: [], tpaProviders: [],
isActive: true, isActive: true,
secondary_email_enabled: false, secondary_email_enabled: false,
nameChangeModal: {},
verifiedNameEnabled: false,
verifiedName: null,
mostRecentVerifiedName: {},
verifiedNameHistory: [],
}; };
export default connect(accountSettingsPageSelector, { export default connect(accountSettingsPageSelector, {
fetchSettings, fetchSettings,
saveSettings, saveSettings,
saveMultipleSettings,
updateDraft, updateDraft,
fetchSiteLanguages, fetchSiteLanguages,
beginNameChange,
})(injectIntl(AccountSettingsPage)); })(injectIntl(AccountSettingsPage));

View File

@@ -63,7 +63,7 @@ const messages = defineMessages({
}, },
'account.settings.section.linked.accounts.description': { 'account.settings.section.linked.accounts.description': {
id: 'account.settings.section.linked.accounts.description', id: 'account.settings.section.linked.accounts.description',
defaultMessage: 'You can link your identity accounts to simplify signing in to {siteName}.', defaultMessage: 'You can link your identity accounts to simplify signing in to edX.',
description: 'The linked accounts section heading description.', description: 'The linked accounts section heading description.',
}, },
'account.settings.field.username': { 'account.settings.field.username': {
@@ -73,7 +73,7 @@ const messages = defineMessages({
}, },
'account.settings.field.username.help.text': { 'account.settings.field.username.help.text': {
id: 'account.settings.field.username.help.text', id: 'account.settings.field.username.help.text',
defaultMessage: 'The name that identifies you on {siteName}. You cannot change your username.', defaultMessage: 'The name that identifies you on edX. You cannot change your username.',
description: 'Help text for the account settings username field.', description: 'Help text for the account settings username field.',
}, },
'account.settings.field.full.name': { 'account.settings.field.full.name': {
@@ -91,96 +91,6 @@ const messages = defineMessages({
defaultMessage: 'The name that is used for ID verification and that appears on your certificates.', defaultMessage: 'The name that is used for ID verification and that appears on your certificates.',
description: 'Help text for the account settings name field.', description: 'Help text for the account settings name field.',
}, },
'account.settings.field.full.name.help.text.non.certificate': {
id: 'account.settings.field.full.name.help.text.non.certificate',
defaultMessage: 'The name that appears on your public profile.',
description: 'Help text for the account settings name field.',
},
'account.settings.field.full.name.help.text.certificate': {
id: 'account.settings.field.full.name.help.text.certificate',
defaultMessage: 'This name is selected to appear on your certificates and public-facing records.',
description: 'Help text for the account settings name field.',
},
'account.settings.field.name.verified': {
id: 'account.settings.field.name.verified',
defaultMessage: 'Verified name',
description: 'Label for account settings verified name field.',
},
'account.settings.field.name.verified.help.text.verified': {
id: 'account.settings.field.name.verified.help.text.verified',
defaultMessage: 'This name has been verified by government ID.',
description: 'Help text for the account settings verified name field when the name is verified.',
},
'account.settings.field.name.verified.help.text.certificate': {
id: 'account.settings.field.name.verified.help.text.certificate',
defaultMessage: 'This name has been verified by government ID and selected to appear on your certificates and public-facing records.',
description: 'Help text for the account settings verified name field when the name is selected for certificates.',
},
'account.settings.field.name.verified.help.text.submitted': {
id: 'account.settings.field.name.verified.help.text.submitted',
defaultMessage: 'Verification has been submitted. This usually takes 48 hours or less. 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.',
},
'account.settings.field.name.verified.help.text.submitted.certificate': {
id: 'account.settings.field.name.verified.help.text.submitted.certificate',
defaultMessage: 'When identity verification is successful, this name will appear on your certificates 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 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 government-issued ID.',
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.',
description: 'Help text for the account settings full name field when a verified name has been submitted.',
},
'account.settings.field.full.name.help.text.submitted.certificate': {
id: 'account.settings.field.full.name.help.text.submitted.certificate',
defaultMessage: 'When identity verification is successful, this name will appear on your certificates and public-facing records. Full name cannot be changed at this time.',
description: 'Help text for the account settings full name field when a full name has been submitted and will appear on certificates.',
},
'account.settings.field.name.verified.success.message': {
id: 'account.settings.field.name.verified.success.message',
defaultMessage: 'Your identity verification request has successfully completed. You now have the option of selecting which name you prefer to appear on your certificates and public-records.',
description: 'The body of the success alert indicating that a user\'s name has been verified',
},
'account.settings.field.name.verified.success.message.header': {
id: 'account.settings.field.name.verified.success.message.header',
defaultMessage: 'Your name change request is complete!',
description: 'The header of the success alert indicating that a user\'s name has been verified',
},
'account.settings.field.name.verified.failure.message': {
id: 'account.settings.field.name.verified.failure.message',
defaultMessage: 'Your most recent identity verification attempt did not pass. Related account settings have been restored.',
description: 'The body of the failure alert indicating that a user\'s name was not able to be verified',
},
'account.settings.field.name.verified.failure.message.header': {
id: 'account.settings.field.name.verified.failure.message.header',
defaultMessage: 'We were not able to verify your identity.',
description: 'The header of the failure alert indicating that a user\'s name was not able to be verified',
},
'account.settings.field.name.verified.failure.message.help.link': {
id: 'account.settings.field.name.verified.failure.message.help.link',
defaultMessage: 'Learn more about ID verification',
description: 'The text of the button displayed when a user\'s name was not able to be verified, intended to direct the user to a help article about ID verification.',
},
'account.settings.field.name.verified.submitted.message': {
id: 'account.settings.field.name.verified.submitted.message',
defaultMessage: 'Your identity verification request has been submitted and usually takes between 24 and 48 hours to complete.',
description: 'The body of the submitted alert indicating that a user\'s name has been submitted for verification',
},
'account.settings.field.name.verified.submitted.message.certificate': {
id: 'account.settings.field.name.verified.submitted.message.certificate',
defaultMessage: 'When your request is approved, your updated name will appear on all associated certificates and public-facing records.',
description: 'The body of the submitted alert indicating that a user\'s name will be updated on certificates.',
},
'account.settings.field.name.verified.submitted.message.header': {
id: 'account.settings.field.name.verified.submitted.message.header',
defaultMessage: 'Your name change request is almost complete!',
description: 'The header of the submitted alert indicating that a user\'s name has been submitted for verification',
},
'account.settings.field.email': { 'account.settings.field.email': {
id: 'account.settings.field.email', id: 'account.settings.field.email',
defaultMessage: 'Email address (Sign in)', defaultMessage: 'Email address (Sign in)',
@@ -198,7 +108,7 @@ const messages = defineMessages({
}, },
'account.settings.field.email.help.text': { 'account.settings.field.email.help.text': {
id: 'account.settings.field.email.help.text', id: 'account.settings.field.email.help.text',
defaultMessage: 'You receive messages from {siteName} and course teams at this address.', defaultMessage: 'You receive messages from edX and course teams at this address.',
description: 'Help text for the account settings email field.', description: 'Help text for the account settings email field.',
}, },
'account.settings.field.secondary.email': { 'account.settings.field.secondary.email': {
@@ -369,18 +279,18 @@ const messages = defineMessages({
}, },
'account.settings.field.language.proficiencies': { 'account.settings.field.language.proficiencies': {
id: 'account.settings.field.language.proficiencies', id: 'account.settings.field.language.proficiencies',
defaultMessage: 'Spoken language', defaultMessage: 'Spoken languages',
description: 'Label for account settings spoken language field.', description: 'Label for account settings spoken languages field.',
}, },
'account.settings.field.language.proficiencies.empty': { 'account.settings.field.language.proficiencies.empty': {
id: 'account.settings.field.language.proficiencies.empty', id: 'account.settings.field.language.proficiencies.empty',
defaultMessage: 'Add a spoken language', defaultMessage: 'Add a spoken language',
description: 'Placeholder for empty account settings spoken language field.', description: 'Placeholder for empty account settings spoken languages field.',
}, },
'account.settings.field.language_proficiencies.options.empty': { 'account.settings.field.language_proficiencies.options.empty': {
id: 'account.settings.field.language_proficiencies.options.empty', id: 'account.settings.field.language_proficiencies.options.empty',
defaultMessage: 'Select a Language', defaultMessage: 'Select a Language',
description: 'Option for an empty value on account settings spoken language field.', description: 'Option for an empty value on account settings spoken languages field.',
}, },
'account.settings.field.time.zone': { 'account.settings.field.time.zone': {
@@ -421,7 +331,7 @@ const messages = defineMessages({
}, },
'account.settings.section.social.media.description': { 'account.settings.section.social.media.description': {
id: 'account.settings.section.social.media.description', id: 'account.settings.section.social.media.description',
defaultMessage: 'Optionally, link your personal accounts to the social media icons on your {siteName} profile.', defaultMessage: 'Optionally, link your personal accounts to the social media icons on your edX profile.',
description: 'Section subheader for social media links settings', description: 'Section subheader for social media links settings',
}, },
'account.settings.field.social.platform.name.linkedin': { 'account.settings.field.social.platform.name.linkedin': {

View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
function Alert(props) { function Alert(props) {
return ( return (
<div className={classNames('alert d-flex align-items-start', props.className)}> <div className={classNames('alert d-flex align-items-start', props.className)}>
@@ -15,6 +16,7 @@ function Alert(props) {
); );
} }
Alert.propTypes = { Alert.propTypes = {
className: PropTypes.string, className: PropTypes.string,
icon: PropTypes.node, icon: PropTypes.node,
@@ -27,4 +29,5 @@ Alert.defaultProps = {
children: undefined, children: undefined,
}; };
export default Alert; export default Alert;

View File

@@ -65,7 +65,7 @@ class BetaLanguageBanner extends React.Component {
})} })}
</p> </p>
<div> <div>
<Button onClick={this.handleRevertLanguage} className="mr-2"> <Button onClick={this.handleRevertLanguage} className="btn btn-primary mr-2">
{this.props.intl.formatMessage( {this.props.intl.formatMessage(
messages['account.settings.banner.beta.language.action.switch.back'], messages['account.settings.banner.beta.language.action.switch.back'],
{ previous_language: previousLanguage.name }, { previous_language: previousLanguage.name },

View File

@@ -2,9 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { import { Button, Input, StatefulButton, ValidationFormGroup } from '@edx/paragon';
Button, Input, StatefulButton, ValidationFormGroup,
} from '@edx/paragon';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons'; import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -16,7 +14,7 @@ import {
closeForm, closeForm,
} from './data/actions'; } from './data/actions';
import { editableFieldSelector } from './data/selectors'; import { editableFieldSelector } from './data/selectors';
import CertificatePreference from './certificate-preference/CertificatePreference';
function EditableField(props) { function EditableField(props) {
const { const {
@@ -38,7 +36,6 @@ function EditableField(props) {
onChange, onChange,
isEditing, isEditing,
isEditable, isEditable,
isGrayedOut,
intl, intl,
...others ...others
} = props; } = props;
@@ -63,37 +60,33 @@ function EditableField(props) {
const renderEmptyLabel = () => { const renderEmptyLabel = () => {
if (isEditable) { if (isEditable) {
return <Button variant="link" onClick={handleEdit} className="p-0">{emptyLabel}</Button>; return <Button onClick={handleEdit} className="btn-link p-0">{emptyLabel}</Button>;
} }
return <span className="text-muted">{emptyLabel}</span>; return <span className="text-muted">{emptyLabel}</span>;
}; };
const renderValue = (rawValue) => { const renderValue = (rawValue) => {
if (!rawValue) { if (!rawValue) return renderEmptyLabel();
return renderEmptyLabel(); let value = rawValue;
}
let finalValue = rawValue;
if (options) { if (options) {
// Use == instead of === to prevent issues when HTML casts numbers as strings // Use == instead of === to prevent issues when HTML casts numbers as strings
// eslint-disable-next-line eqeqeq // eslint-disable-next-line eqeqeq
const selectedOption = options.find(option => option.value == rawValue); const selectedOption = options.find(option => option.value == rawValue);
if (selectedOption) { if (selectedOption) {
finalValue = selectedOption.label; value = selectedOption.label;
} };
} }
if (userSuppliedValue) { if (userSuppliedValue) {
finalValue += `: ${userSuppliedValue}`; value += `: ${userSuppliedValue}`;
} }
return finalValue; return value;
}; };
const renderConfirmationMessage = () => { const renderConfirmationMessage = () => {
if (!confirmationMessageDefinition || !confirmationValue) { if (!confirmationMessageDefinition || !confirmationValue) return null;
return null;
}
return intl.formatMessage(confirmationMessageDefinition, { return intl.formatMessage(confirmationMessageDefinition, {
value: confirmationValue, value: confirmationValue,
}); });
@@ -104,69 +97,65 @@ function EditableField(props) {
expression={isEditing ? 'editing' : 'default'} expression={isEditing ? 'editing' : 'default'}
cases={{ cases={{
editing: ( editing: (
<> <form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit}> <ValidationFormGroup
<ValidationFormGroup for={id}
for={id} invalid={error != null}
invalid={error != null} invalidMessage={error}
invalidMessage={error} helpText={helpText}
helpText={helpText} >
<label className="h6 d-block" htmlFor={id}>{label}</label>
<Input
name={name}
id={id}
type={type}
value={value}
onChange={handleChange}
options={options}
{...others}
/>
<>{others.children}</>
</ValidationFormGroup>
<p>
<StatefulButton
type="submit"
className="btn-primary 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
onClick={handleCancel}
className="btn-outline-primary"
> >
<label className="h6 d-block" htmlFor={id}>{label}</label> {intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
<Input </Button>
data-hj-suppress </p>
name={name} </form>
id={id}
type={type}
value={value}
onChange={handleChange}
options={options}
{...others}
/>
<>{others.children}</>
</ValidationFormGroup>
<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: ( default: (
<div className="form-group"> <div className="form-group">
<div className="d-flex align-items-start"> <div className="d-flex align-items-start">
<h6 aria-level="3">{label}</h6> <h6 aria-level="3">{label}</h6>
{isEditable ? ( {isEditable ? (
<Button variant="link" onClick={handleEdit} className="ml-3"> <Button onClick={handleEdit} className="ml-3 btn-link">
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />{intl.formatMessage(messages['account.settings.editable.field.action.edit'])} <FontAwesomeIcon className="mr-1" icon={faPencilAlt} />{intl.formatMessage(messages['account.settings.editable.field.action.edit'])}
</Button> </Button>
) : null} ) : null}
</div> </div>
<p data-hj-suppress className={isGrayedOut ? 'grayed-out' : null}>{renderValue(value)}</p> <p>{renderValue(value)}</p>
<p className="small text-muted mt-n2">{renderConfirmationMessage() || helpText}</p> <p className="small text-muted mt-n2">{renderConfirmationMessage() || helpText}</p>
</div> </div>
), ),
@@ -175,9 +164,10 @@ function EditableField(props) {
); );
} }
EditableField.propTypes = { EditableField.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]), label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
emptyLabel: PropTypes.node, emptyLabel: PropTypes.node,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
@@ -201,7 +191,6 @@ EditableField.propTypes = {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
isEditing: PropTypes.bool, isEditing: PropTypes.bool,
isEditable: PropTypes.bool, isEditable: PropTypes.bool,
isGrayedOut: PropTypes.bool,
intl: intlShape.isRequired, intl: intlShape.isRequired,
}; };
@@ -217,10 +206,9 @@ EditableField.defaultProps = {
helpText: undefined, helpText: undefined,
isEditing: false, isEditing: false,
isEditable: true, isEditable: true,
isGrayedOut: false,
userSuppliedValue: undefined,
}; };
export default connect(editableFieldSelector, { export default connect(editableFieldSelector, {
onEdit: openForm, onEdit: openForm,
onCancel: closeForm, onCancel: closeForm,

View File

@@ -2,9 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { import { Button, StatefulButton, Input, ValidationFormGroup } from '@edx/paragon';
Button, StatefulButton, Input, ValidationFormGroup,
} from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExclamationTriangle, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; import { faExclamationTriangle, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
@@ -18,6 +16,7 @@ import {
} from './data/actions'; } from './data/actions';
import { editableFieldSelector } from './data/selectors'; import { editableFieldSelector } from './data/selectors';
function EmailField(props) { function EmailField(props) {
const { const {
name, name,
@@ -57,9 +56,7 @@ function EmailField(props) {
}; };
const renderConfirmationMessage = () => { const renderConfirmationMessage = () => {
if (!confirmationMessageDefinition || !confirmationValue) { if (!confirmationMessageDefinition || !confirmationValue) return null;
return null;
}
return ( return (
<Alert <Alert
className="alert-warning mt-n2" className="alert-warning mt-n2"
@@ -88,15 +85,13 @@ function EmailField(props) {
const renderEmptyLabel = () => { const renderEmptyLabel = () => {
if (isEditable) { if (isEditable) {
return <Button variant="link" onClick={handleEdit} className="p-0">{emptyLabel}</Button>; return <Button onClick={handleEdit} className="btn-link p-0">{emptyLabel}</Button>;
} }
return <span className="text-muted">{emptyLabel}</span>; return <span className="text-muted">{emptyLabel}</span>;
}; };
const renderValue = () => { const renderValue = () => {
if (confirmationValue) { if (confirmationValue) return renderConfirmationValue();
return renderConfirmationValue();
}
return value || renderEmptyLabel(); return value || renderEmptyLabel();
}; };
@@ -114,7 +109,6 @@ function EmailField(props) {
> >
<label className="h6 d-block" htmlFor={id}>{label}</label> <label className="h6 d-block" htmlFor={id}>{label}</label>
<Input <Input
data-hj-suppress
name={name} name={name}
id={id} id={id}
type="email" type="email"
@@ -125,7 +119,7 @@ function EmailField(props) {
<p> <p>
<StatefulButton <StatefulButton
type="submit" type="submit"
className="mr-2" className="btn-primary mr-2"
state={saveState} state={saveState}
labels={{ labels={{
default: intl.formatMessage(messages['account.settings.editable.field.action.save']), default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
@@ -138,13 +132,13 @@ function EmailField(props) {
// Swallowing the onSubmit event on the form would be better, but // Swallowing the onSubmit event on the form would be better, but
// we would have to add that logic for every field given our // we would have to add that logic for every field given our
// current structure of the application. // current structure of the application.
if (saveState === 'pending') { e.preventDefault(); } if (saveState === 'pending') e.preventDefault();
}} }}
disabledStates={[]} disabledStates={[]}
/> />
<Button <Button
variant="outline-primary"
onClick={handleCancel} onClick={handleCancel}
className="btn-outline-primary"
> >
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])} {intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
</Button> </Button>
@@ -156,13 +150,13 @@ function EmailField(props) {
<div className="d-flex align-items-start"> <div className="d-flex align-items-start">
<h6 aria-level="3">{label}</h6> <h6 aria-level="3">{label}</h6>
{isEditable ? ( {isEditable ? (
<Button variant="link" onClick={handleEdit} className="ml-3"> <Button onClick={handleEdit} className="ml-3 btn-link">
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} /> <FontAwesomeIcon className="mr-1" icon={faPencilAlt} />
{intl.formatMessage(messages['account.settings.editable.field.action.edit'])} {intl.formatMessage(messages['account.settings.editable.field.action.edit'])}
</Button> </Button>
) : null} ) : null}
</div> </div>
<p data-hj-suppress>{renderValue()}</p> <p>{renderValue()}</p>
{renderConfirmationMessage() || <p className="small text-muted mt-n2">{helpText}</p>} {renderConfirmationMessage() || <p className="small text-muted mt-n2">{helpText}</p>}
</div> </div>
), ),
@@ -171,6 +165,7 @@ function EmailField(props) {
); );
} }
EmailField.propTypes = { EmailField.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
@@ -207,6 +202,7 @@ EmailField.defaultProps = {
isEditable: true, isEditable: true,
}; };
export default connect(editableFieldSelector, { export default connect(editableFieldSelector, {
onEdit: openForm, onEdit: openForm,
onCancel: closeForm, onCancel: closeForm,

View File

@@ -3,11 +3,11 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { NavHashLink } from 'react-router-hash-link'; import { NavHashLink } from 'react-router-hash-link';
import Scrollspy from 'react-scrollspy'; import Scrollspy from 'react-scrollspy';
import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types';
import messages from './AccountSettingsPage.messages'; import messages from './AccountSettingsPage.messages';
import { getConfig } from '@edx/frontend-platform';
function JumpNav({ intl, displayDemographicsLink }) {
function JumpNav({ intl }) {
return ( return (
<div className="jump-nav"> <div className="jump-nav">
<Scrollspy <Scrollspy
@@ -33,14 +33,13 @@ function JumpNav({ intl, displayDemographicsLink }) {
{intl.formatMessage(messages['account.settings.section.profile.information'])} {intl.formatMessage(messages['account.settings.section.profile.information'])}
</NavHashLink> </NavHashLink>
</li> </li>
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && displayDemographicsLink {getConfig().ENABLE_DEMOGRAPHICS_COLLECTION &&
&& (
<li> <li>
<NavHashLink to="#demographics-information"> <NavHashLink to="#demographics-information">
{intl.formatMessage(messages['account.settings.section.demographics.information'])} {intl.formatMessage(messages['account.settings.section.demographics.information'])}
</NavHashLink> </NavHashLink>
</li> </li>
)} }
<li> <li>
<NavHashLink to="#social-media"> <NavHashLink to="#social-media">
{intl.formatMessage(messages['account.settings.section.social.media'])} {intl.formatMessage(messages['account.settings.section.social.media'])}
@@ -66,9 +65,10 @@ function JumpNav({ intl, displayDemographicsLink }) {
); );
} }
JumpNav.propTypes = { JumpNav.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,
displayDemographicsLink: PropTypes.bool.isRequired,
}; };
export default injectIntl(JumpNav); export default injectIntl(JumpNav);

View File

@@ -1,43 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Alert } from '@edx/paragon';
export default function OneTimeDismissibleAlert(props) {
const [dismissed, setDismissed] = useState(localStorage.getItem(props.id) !== 'true');
const onClose = () => {
localStorage.setItem(props.id, 'true');
setDismissed(false);
};
return (
<Alert
variant={props.variant}
dismissible
icon={props.icon}
onClose={onClose}
show={dismissed}
>
<Alert.Heading>{props.header}</Alert.Heading>
<p>
{props.body}
</p>
</Alert>
);
}
OneTimeDismissibleAlert.propTypes = {
id: PropTypes.string.isRequired,
variant: PropTypes.string,
icon: PropTypes.func,
header: PropTypes.string,
body: PropTypes.string,
};
OneTimeDismissibleAlert.defaultProps = {
variant: 'success',
icon: undefined,
header: undefined,
body: undefined,
};

View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { TransitionReplace } from '@edx/paragon'; import { TransitionReplace } from '@edx/paragon';
const onChildExit = (htmlNode) => { const onChildExit = (htmlNode) => {
// If the leaving child has focus, take control and redirect it // If the leaving child has focus, take control and redirect it
if (htmlNode.contains(document.activeElement)) { if (htmlNode.contains(document.activeElement)) {
@@ -10,9 +11,7 @@ const onChildExit = (htmlNode) => {
const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling; const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling;
// There's no replacement, do nothing. // There's no replacement, do nothing.
if (!enteringChild) { if (!enteringChild) return;
return;
}
// Get all the focusable elements in the entering child and focus the first one // Get all the focusable elements in the entering child and focus the first one
const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
@@ -22,6 +21,7 @@ const onChildExit = (htmlNode) => {
} }
}; };
function SwitchContent({ expression, cases, className }) { function SwitchContent({ expression, cases, className }) {
const getContent = (caseKey) => { const getContent = (caseKey) => {
if (cases[caseKey]) { if (cases[caseKey]) {
@@ -29,8 +29,7 @@ function SwitchContent({ expression, cases, className }) {
return getContent(cases[caseKey]); return getContent(cases[caseKey]);
} }
return React.cloneElement(cases[caseKey], { key: caseKey }); return React.cloneElement(cases[caseKey], { key: caseKey });
} } else if (cases.default) {
if (cases.default) {
if (typeof cases.default === 'string') { if (typeof cases.default === 'string') {
return getContent(cases.default); return getContent(cases.default);
} }
@@ -50,6 +49,7 @@ function SwitchContent({ expression, cases, className }) {
); );
} }
SwitchContent.propTypes = { SwitchContent.propTypes = {
expression: PropTypes.string, expression: PropTypes.string,
cases: PropTypes.objectOf(PropTypes.node).isRequired, cases: PropTypes.objectOf(PropTypes.node).isRequired,
@@ -61,4 +61,5 @@ SwitchContent.defaultProps = {
className: null, className: null,
}; };
export default SwitchContent; export default SwitchContent;

View File

@@ -2,11 +2,9 @@
.form-group { .form-group {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
h6, .h6 { h6, .h6 {
margin-bottom: .25rem; margin-bottom: .25rem;
} }
.btn-link { .btn-link {
line-height: 1.2; line-height: 1.2;
border: none; border: none;
@@ -20,10 +18,8 @@
position: sticky; position: sticky;
top: 1rem; top: 1rem;
} }
li { li {
margin-bottom: .5rem; margin-bottom: .5rem;
a { a {
text-decoration: underline; text-decoration: underline;
} }
@@ -34,24 +30,18 @@
@extend .h4; @extend .h4;
margin-bottom: map-get($spacers, 3); margin-bottom: map-get($spacers, 3);
} }
.account-section { .account-section {
// These properties together will shift the hashlink position // These properties together will shift the hashlink position
margin-bottom: map-get($spacers, 5); margin-bottom: map-get($spacers, 5);
padding-top: 1rem; padding-top: 1rem;
} }
.custom-switch { .custom-switch {
padding: 0; padding: 0;
max-width: 500px; max-width: 500px;
.custom-control-label { .custom-control-label {
left: 2.25rem; left: 2.25rem;
line-height: 1.6rem; line-height: 1.6rem;
} }
} }
.grayed-out{
opacity: 0.6; /* Real browsers */
filter: alpha(opacity = 60); /* MSIE */
}
} }

View File

@@ -1,175 +0,0 @@
import React, { useState, useEffect } from 'react';
import { connect, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import {
ActionRow,
Form,
ModalDialog,
StatefulButton,
} from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
closeForm,
resetDrafts,
saveSettings,
updateDraft,
} from '../data/actions';
import { certPreferenceSelector } from '../data/selectors';
import commonMessages from '../AccountSettingsPage.messages';
import messages from './messages';
function CertificatePreference({
intl,
fieldName,
originalFullName,
originalVerifiedName,
saveState,
useVerifiedNameForCerts,
verifiedNameEnabled,
}) {
if (!verifiedNameEnabled || !originalVerifiedName) {
// If the user doesn't have an approved verified name, do not display this component
return null;
}
const dispatch = useDispatch();
const [checked, setChecked] = useState(false);
const [modalIsOpen, setModalIsOpen] = useState(false);
const formId = 'useVerifiedNameForCerts';
function handleCheckboxChange() {
if (!checked) {
if (fieldName === 'verified_name') {
dispatch(updateDraft(formId, true));
} else {
dispatch(updateDraft(formId, false));
}
} else {
setModalIsOpen(true);
}
}
function handleCancel() {
setModalIsOpen(false);
dispatch(resetDrafts());
}
function handleModalChange(e) {
if (e.target.value === 'fullName') {
dispatch(updateDraft(formId, false));
} else {
dispatch(updateDraft(formId, true));
}
}
function handleSubmit(e) {
e.preventDefault();
if (saveState === 'pending') {
return;
}
dispatch(saveSettings(formId, useVerifiedNameForCerts));
}
useEffect(() => {
if (fieldName === 'verified_name') {
setChecked(useVerifiedNameForCerts);
} else {
setChecked(!useVerifiedNameForCerts);
}
}, [useVerifiedNameForCerts]);
useEffect(() => {
if (modalIsOpen && saveState === 'complete') {
setModalIsOpen(false);
dispatch(closeForm(fieldName));
}
}, [modalIsOpen, saveState]);
return (
<>
<Form.Checkbox className="mt-1 mb-4" checked={checked} onChange={handleCheckboxChange}>
{intl.formatMessage(messages['account.settings.field.name.checkbox.certificate.select'])}
</Form.Checkbox>
<ModalDialog
title={intl.formatMessage(messages['account.settings.field.name.modal.certificate.title'])}
isOpen={modalIsOpen}
onClose={handleCancel}
size="lg"
hasCloseButton
isFullscreenOnMobile
>
<Form onSubmit={handleSubmit}>
<ModalDialog.Header>
<ModalDialog.Title>
{intl.formatMessage(messages['account.settings.field.name.modal.certificate.title'])}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body className="overflow-hidden">
<Form.Group className="mb-4">
<Form.Label>
{intl.formatMessage(messages['account.settings.field.name.modal.certificate.select'])}
</Form.Label>
<Form.RadioSet
name={formId}
value={useVerifiedNameForCerts ? 'verifiedName' : 'fullName'}
onChange={handleModalChange}
>
<Form.Radio value="fullName">
{originalFullName}{' '}
({intl.formatMessage(messages['account.settings.field.name.modal.certificate.option.full'])})
</Form.Radio>
<Form.Radio value="verifiedName">
{originalVerifiedName}{' '}
({intl.formatMessage(messages['account.settings.field.name.modal.certificate.option.verified'])})
</Form.Radio>
</Form.RadioSet>
</Form.Group>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="outline-primary" disabled={saveState === 'pending'}>
{intl.formatMessage(commonMessages['account.settings.editable.field.action.cancel'])}
</ModalDialog.CloseButton>
<StatefulButton
type="submit"
state={saveState}
labels={{
default: intl.formatMessage(messages['account.settings.field.name.modal.certificate.button.choose']),
}}
disabledStates={[]}
/>
</ActionRow>
</ModalDialog.Footer>
</Form>
</ModalDialog>
</>
);
}
CertificatePreference.propTypes = {
intl: intlShape.isRequired,
fieldName: PropTypes.string.isRequired,
originalFullName: PropTypes.string,
originalVerifiedName: PropTypes.string,
saveState: PropTypes.string,
useVerifiedNameForCerts: PropTypes.bool,
verifiedNameEnabled: PropTypes.bool,
};
CertificatePreference.defaultProps = {
originalFullName: '',
originalVerifiedName: '',
saveState: null,
useVerifiedNameForCerts: false,
verifiedNameEnabled: false,
};
export default connect(certPreferenceSelector)(injectIntl(CertificatePreference));

View File

@@ -1,22 +0,0 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { handleRequestError } from '../../data/utils';
// eslint-disable-next-line import/prefer-default-export
export async function postVerifiedNameConfig(username, commitValues) {
const requestConfig = { headers: { Accept: 'application/json' } };
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name/config`;
const { useVerifiedNameForCerts } = commitValues;
const postValues = {
username,
use_verified_name_for_certs: useVerifiedNameForCerts,
};
const { data } = await getAuthenticatedHttpClient()
.post(requestUrl, postValues, requestConfig)
.catch(error => handleRequestError(error));
return data;
}

View File

@@ -1,36 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'account.settings.field.name.checkbox.certificate.select': {
id: 'account.settings.field.name.certificate.select',
defaultMessage: 'If checked, this name will appear on your certificates and public-facing records.',
description: 'Label for checkbox describing that the selected name will appear on the users certificates.',
},
'account.settings.field.name.modal.certificate.title': {
id: 'account.settings.field.name.modal.certificate.title',
defaultMessage: 'Choose a preferred name for certificates and public-facing records',
description: 'Title instructing the user to choose a preferred name.',
},
'account.settings.field.name.modal.certificate.select': {
id: 'account.settings.field.name.modal.certificate.select',
defaultMessage: 'Select a name',
description: 'Label instructing the user to select a name.',
},
'account.settings.field.name.modal.certificate.option.full': {
id: 'account.settings.field.name.modal.certificate.option.full',
defaultMessage: 'Full Name',
description: 'Option representing the users full name.',
},
'account.settings.field.name.modal.certificate.option.verified': {
id: 'account.settings.field.name.modal.certificate.option.verified',
defaultMessage: 'Verified Name',
description: 'Option representing the users verified name.',
},
'account.settings.field.name.modal.certificate.button.choose': {
id: 'account.settings.field.name.modal.certificate.button.choose',
defaultMessage: 'Choose name',
description: 'Button to confirm the users name choice.',
},
});
export default messages;

View File

@@ -1,174 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
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';
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
ReactDOM.createPortal = node => node;
import CertificatePreference from '../CertificatePreference'; // eslint-disable-line import/first
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 history = createMemoryHistory();
const IntlCertificatePreference = injectIntl(CertificatePreference);
const mockStore = configureStore();
describe('NameChange', () => {
let props = {};
let store = {};
const formId = 'useVerifiedNameForCerts';
const updateDraft = 'UPDATE_DRAFT';
const labelText = 'If checked, this name will appear on your certificates and public-facing records.';
const reduxWrapper = children => (
<Router history={history}>
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
</Router>
);
beforeEach(() => {
store = mockStore();
props = {
fieldName: 'name',
originalFullName: 'Ed X',
originalVerifiedName: 'edX Verified',
saveState: null,
useVerifiedNameForCerts: false,
verifiedNameEnabled: true,
intl: {},
};
auth.getAuthenticatedHttpClient = jest.fn(() => ({
patch: async () => ({
data: { status: 200 },
catch: () => {},
}),
}));
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3 }));
});
afterEach(() => jest.clearAllMocks());
it('does not render if there is no verified name', () => {
props = {
...props,
originalVerifiedName: '',
};
const wrapper = render(reduxWrapper(<IntlCertificatePreference {...props} />));
expect(wrapper).toMatchSnapshot();
});
it('does not trigger modal when checking empty checkbox, and updates draft immediately', () => {
props = {
...props,
useVerifiedNameForCerts: true,
};
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
expect(checkbox.checked).toEqual(false);
fireEvent.click(checkbox);
expect(screen.queryByRole('radiogroup')).toBeNull();
expect(mockDispatch).toHaveBeenCalledWith({
payload: { name: formId, value: false },
type: updateDraft,
});
});
it('triggers modal when attempting to uncheck checkbox', () => {
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
expect(checkbox.checked).toEqual(true);
fireEvent.click(checkbox);
expect(mockDispatch).not.toHaveBeenCalled();
screen.getByRole('radiogroup');
});
it('updates draft when changing radio value', () => {
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
fireEvent.click(checkbox);
const fullNameOption = screen.getByLabelText('Ed X (Full Name)');
const verifiedNameOption = screen.getByLabelText('edX Verified (Verified Name)');
expect(fullNameOption.checked).toEqual(true);
expect(verifiedNameOption.checked).toEqual(false);
fireEvent.click(verifiedNameOption);
expect(mockDispatch).toHaveBeenCalledWith({
payload: { name: formId, value: true },
type: updateDraft,
});
});
it('clears draft on cancel', () => {
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
fireEvent.click(checkbox);
const cancelButton = screen.getByText('Cancel');
fireEvent.click(cancelButton);
expect(mockDispatch).toHaveBeenCalledWith({ type: 'RESET_DRAFTS' });
expect(screen.queryByRole('radiogroup')).toBeNull();
});
it('submits', () => {
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
fireEvent.click(checkbox);
const submitButton = screen.getByText('Choose name');
fireEvent.click(submitButton);
expect(mockDispatch).toHaveBeenCalledWith({
payload: { formId, commitValues: false },
type: 'ACCOUNT_SETTINGS__SAVE_SETTINGS',
});
});
it('checks box for verified name', () => {
props = {
...props,
fieldName: 'verified_name',
useVerifiedNameForCerts: true,
};
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
expect(checkbox.checked).toEqual(true);
});
});

View File

@@ -1,62 +0,0 @@
// 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 />
</body>,
"container": <div />,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;

View File

@@ -97,6 +97,7 @@ class CoachingConsent extends React.Component {
if (get(data, 'status') === 200) { if (get(data, 'status') === 200) {
this.setState({ submissionSuccess: true }); this.setState({ submissionSuccess: true });
} }
} }
handleSubmit(e) { handleSubmit(e) {
@@ -134,27 +135,23 @@ class CoachingConsent extends React.Component {
case VIEWS.NOT_LOADED: case VIEWS.NOT_LOADED:
return <PageLoading srMessage="" />; return <PageLoading srMessage="" />;
case VIEWS.LOADED: case VIEWS.LOADED:
return ( return (<CoachingConsentForm
<CoachingConsentForm onSubmit={this.handleSubmit}
onSubmit={this.handleSubmit} declineCoaching={this.declineCoaching}
declineCoaching={this.declineCoaching} formErrors={this.state.formErrors}
formErrors={this.state.formErrors} formValues={this.props.formValues}
formValues={this.props.formValues} redirectUrl={this.state.redirectUrl}
redirectUrl={this.state.redirectUrl} profileDataManager={this.props.profileDataManager}
profileDataManager={this.props.profileDataManager} />);
/>
);
case VIEWS.SUCCESS_PENDING: case VIEWS.SUCCESS_PENDING:
return <PageLoading srMessage="Submitting..." />; return <PageLoading srMessage="Submitting..." />;
case VIEWS.SUCCESS: case VIEWS.SUCCESS:
return ( return (<SuccessMessage
<SuccessMessage continueUrl={this.state.redirectUrl}
continueUrl={this.state.redirectUrl} header={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.header'])}
header={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.header'])} message={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.message'])}
message={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.message'])} continue={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.continue'])}
continue={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.continue'])} />);
/>
);
case VIEWS.DECLINE_PENDING: case VIEWS.DECLINE_PENDING:
return <PageLoading srMessage="Redirecting..." />; return <PageLoading srMessage="Redirecting..." />;
case VIEWS.DECLINED: case VIEWS.DECLINED:
@@ -236,6 +233,7 @@ AutoRedirect.propTypes = {
CoachingConsent.defaultProps = { CoachingConsent.defaultProps = {
loaded: false, loaded: false,
saveState: undefined,
profileDataManager: null, profileDataManager: null,
}; };
@@ -261,6 +259,7 @@ CoachingConsent.propTypes = {
phone_number: PropTypes.object, phone_number: PropTypes.object,
}).isRequired, }).isRequired,
fetchSettings: PropTypes.func.isRequired, fetchSettings: PropTypes.func.isRequired,
saveState: PropTypes.string,
profileDataManager: PropTypes.string, profileDataManager: PropTypes.string,
}; };

View File

@@ -33,13 +33,12 @@ const CoachingForm = props => (
<div> <div>
<form onSubmit={props.onSubmit}> <form onSubmit={props.onSubmit}>
<div className="py-3"> <div className="py-3">
{!!props.profileDataManager && ( {
!!props.profileDataManager &&
<ManagedProfileAlert profileDataManager={props.profileDataManager} /> <ManagedProfileAlert profileDataManager={props.profileDataManager} />
)} }
<ErrorMessage message={props.formErrors.full_name} /> <ErrorMessage message={props.formErrors.full_name} />
<label className="h6" htmlFor="fullName"> <label className="h6" htmlFor="fullName">{props.intl.formatMessage(messages['account.settings.coaching.consent.label.name'])}</label>
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.name'])}
</label>
<Input <Input
type="text" type="text"
name="full-name" name="full-name"
@@ -50,9 +49,7 @@ const CoachingForm = props => (
</div> </div>
<div className="py-3"> <div className="py-3">
<ErrorMessage message={props.formErrors.phone_number} /> <ErrorMessage message={props.formErrors.phone_number} />
<label className="h6" htmlFor="phoneNumber"> <label className="h6" htmlFor="phoneNumber">{props.intl.formatMessage(messages['account.settings.coaching.consent.label.phone-number'])}</label>
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.phone-number'])}
</label>
<Input <Input
type="text" type="text"
name="phone_number" name="phone_number"
@@ -67,7 +64,7 @@ const CoachingForm = props => (
</div> </div>
<ErrorMessage message={props.formErrors.coaching} /> <ErrorMessage message={props.formErrors.coaching} />
<div className="d-flex flex-column align-items-center"> <div className="d-flex flex-column align-items-center">
<Button variant="outline-primary" className="w-100" type="submit"> <Button className="w-100 btn-outline-primary" type="submit">
{props.intl.formatMessage(messages['account.settings.coaching.consent.accept-coaching'])} {props.intl.formatMessage(messages['account.settings.coaching.consent.accept-coaching'])}
</Button> </Button>
</div> </div>
@@ -126,6 +123,7 @@ ErrorMessage.propTypes = {
ManagedProfileAlert.propTypes = { ManagedProfileAlert.propTypes = {
profileDataManager: PropTypes.string.isRequired, profileDataManager: PropTypes.string.isRequired,
intl: intlShape.isRequired,
}; };
export default injectIntl(CoachingForm); export default injectIntl(CoachingForm);

View File

@@ -8,6 +8,7 @@ import { editableFieldSelector } from '../data/selectors';
import { saveSettings, updateDraft, saveMultipleSettings } from '../data/actions'; import { saveSettings, updateDraft, saveMultipleSettings } from '../data/actions';
import EditableField from '../EditableField'; import EditableField from '../EditableField';
const CoachingToggle = props => ( const CoachingToggle = props => (
<> <>
<EditableField <EditableField
@@ -32,10 +33,10 @@ const CoachingToggle = props => (
formId: 'phone_number', formId: 'phone_number',
commitValues: props.phone_number, commitValues: props.phone_number,
}, },
], 'phone_number'); ]);
} }
return props.saveSettings('phone_number', props.phone_number); return props.saveSettings('phone_number', props.phone_number);
}} }}
/> />
<ValidationFormGroup <ValidationFormGroup
for="coachingConsent" for="coachingConsent"

View File

@@ -13,7 +13,9 @@ jest.mock('@edx/frontend-platform/auth');
const IntlCoachingConsent = injectIntl(CoachingConsent); const IntlCoachingConsent = injectIntl(CoachingConsent);
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ coachingConsentPageSelector: () => ({}) }))); jest.mock('../../data/selectors', () => {
return jest.fn().mockImplementation(() => ({ coachingConsentPageSelector: () => ({}) }));
});
const mockStore = configureStore(); const mockStore = configureStore();

View File

@@ -103,8 +103,10 @@ exports[`CoachingConsent disables name field on enterprise user 1`] = `
className="d-flex flex-column align-items-center" className="d-flex flex-column align-items-center"
> >
<button <button
className="w-100 btn btn-outline-primary" className="btn w-100 btn-outline-primary"
disabled={false} onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="submit" type="submit"
> >
Sign up for coaching Sign up for coaching
@@ -252,8 +254,10 @@ exports[`CoachingConsent should render 1`] = `
className="d-flex flex-column align-items-center" className="d-flex flex-column align-items-center"
> >
<button <button
className="w-100 btn btn-outline-primary" className="btn w-100 btn-outline-primary"
disabled={false} onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="submit" type="submit"
> >
Sign up for coaching Sign up for coaching

View File

@@ -9,7 +9,6 @@ export const OPEN_FORM = 'OPEN_FORM';
export const CLOSE_FORM = 'CLOSE_FORM'; export const CLOSE_FORM = 'CLOSE_FORM';
export const UPDATE_DRAFT = 'UPDATE_DRAFT'; export const UPDATE_DRAFT = 'UPDATE_DRAFT';
export const RESET_DRAFTS = 'RESET_DRAFTS'; export const RESET_DRAFTS = 'RESET_DRAFTS';
export const BEGIN_NAME_CHANGE = 'BEGIN_NAME_CHANGE';
// FETCH SETTINGS ACTIONS // FETCH SETTINGS ACTIONS
@@ -26,7 +25,6 @@ export const fetchSettingsSuccess = ({
thirdPartyAuthProviders, thirdPartyAuthProviders,
profileDataManager, profileDataManager,
timeZones, timeZones,
verifiedNameHistory,
}) => ({ }) => ({
type: FETCH_SETTINGS.SUCCESS, type: FETCH_SETTINGS.SUCCESS,
payload: { payload: {
@@ -34,7 +32,6 @@ export const fetchSettingsSuccess = ({
thirdPartyAuthProviders, thirdPartyAuthProviders,
profileDataManager, profileDataManager,
timeZones, timeZones,
verifiedNameHistory,
}, },
}); });
@@ -47,6 +44,7 @@ export const fetchSettingsReset = () => ({
type: FETCH_SETTINGS.RESET, type: FETCH_SETTINGS.RESET,
}); });
// FORM STATE ACTIONS // FORM STATE ACTIONS
export const openForm = formId => ({ export const openForm = formId => ({
@@ -71,10 +69,7 @@ export const resetDrafts = () => ({
type: RESET_DRAFTS, type: RESET_DRAFTS,
}); });
export const beginNameChange = (formId) => ({
type: BEGIN_NAME_CHANGE,
payload: { formId },
});
// SAVE SETTINGS ACTIONS // SAVE SETTINGS ACTIONS
export const saveSettings = (formId, commitValues) => ({ export const saveSettings = (formId, commitValues) => ({

View File

@@ -1,3 +1,4 @@
export const YEAR_OF_BIRTH_OPTIONS = (() => { export const YEAR_OF_BIRTH_OPTIONS = (() => {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const years = []; const years = [];
@@ -112,6 +113,91 @@ export function getStatesList(country) {
return country && COUNTRY_STATES_MAP[country.toUpperCase()]; return country && COUNTRY_STATES_MAP[country.toUpperCase()];
} }
export const DECLINED = 'declined';
export const SELF_DESCRIBE = 'self-describe'; export const SELF_DESCRIBE = 'self-describe';
export const DEMOGRAPHICS_GENDER_OPTIONS = [
'',
'woman',
'man',
'non-binary',
SELF_DESCRIBE,
];
export const OTHER = 'other'; export const OTHER = 'other';
export const DEMOGRAPHICS_ETHNICITY_OPTIONS = [
'american-indian-or-alaska-native',
'asian',
'black-or-african-american',
'hispanic-latin-spanish',
'middle-eastern-or-north-african',
'native-hawaiian-or-pacific-islander',
'white',
OTHER,
];
export const DEMOGRAPHICS_INCOME_OPTIONS = [
'',
'less-than-10k',
'10k-25k',
'25k-50k',
'50k-75k',
'over-100k',
'unsure',
];
export const DEMOGRAPHICS_MILITARY_HISTORY_OPTIONS = [
'',
'never-served',
'training',
'active',
'previously-active',
];
export const DEMOGRAPHICS_EDUCATION_LEVEL_OPTIONS = [
'',
'no-high-school',
'some-high-school',
'high-school-ged-equivalent',
'some-college',
'associates',
'bachelors',
'masters',
'professional',
'doctorate',
];
export const DEMOGRAPHICS_WORK_STATUS_OPTIONS = [
'',
'full-time',
'part-time',
'not-employed-looking',
'not-employed-not-looking',
'unable',
'retired',
'other',
];
export const DEMOGRAPHICS_WORK_SECTOR_OPTIONS = [
'',
'accommodation-food',
'administrative-support-waste-remediation',
'agriculture-forestry-fishing-hunting',
'arts-entertainment-recreation',
'construction',
'educational',
'finance-insurance',
'healthcare-social',
'information',
'management',
'manufacturing',
'mining-quarry-oil-gas',
'professional-scientific-technical',
'public-admin',
'real-estate',
'retail',
'transport-warehousing',
'utilities',
'trade',
'other',
];
export const DECLINED = 'declined';

View File

@@ -8,13 +8,11 @@ import {
UPDATE_DRAFT, UPDATE_DRAFT,
RESET_DRAFTS, RESET_DRAFTS,
SAVE_MULTIPLE_SETTINGS, SAVE_MULTIPLE_SETTINGS,
BEGIN_NAME_CHANGE,
} from './actions'; } from './actions';
import { reducer as deleteAccountReducer, DELETE_ACCOUNT } from '../delete-account'; import { reducer as deleteAccountReducer, DELETE_ACCOUNT } from '../delete-account';
import { reducer as siteLanguageReducer, FETCH_SITE_LANGUAGES } from '../site-language'; import { reducer as siteLanguageReducer, FETCH_SITE_LANGUAGES } from '../site-language';
import { reducer as resetPasswordReducer, RESET_PASSWORD } from '../reset-password'; import { reducer as resetPasswordReducer, RESET_PASSWORD } from '../reset-password';
import { reducer as nameChangeReducer, REQUEST_NAME_CHANGE } from '../name-change';
import { reducer as thirdPartyAuthReducer, DISCONNECT_AUTH } from '../third-party-auth'; import { reducer as thirdPartyAuthReducer, DISCONNECT_AUTH } from '../third-party-auth';
export const defaultState = { export const defaultState = {
@@ -33,13 +31,7 @@ export const defaultState = {
deleteAccount: deleteAccountReducer(), deleteAccount: deleteAccountReducer(),
siteLanguage: siteLanguageReducer(), siteLanguage: siteLanguageReducer(),
resetPassword: resetPasswordReducer(), resetPassword: resetPasswordReducer(),
nameChange: nameChangeReducer(),
thirdPartyAuth: thirdPartyAuthReducer(), thirdPartyAuth: thirdPartyAuthReducer(),
nameChangeModal: false,
verifiedName: null,
mostRecentVerifiedName: {},
verifiedNameHistory: {},
verifiedNameEnabled: false,
}; };
const reducer = (state = defaultState, action) => { const reducer = (state = defaultState, action) => {
@@ -56,15 +48,16 @@ const reducer = (state = defaultState, action) => {
case FETCH_SETTINGS.SUCCESS: case FETCH_SETTINGS.SUCCESS:
return { return {
...state, ...state,
values: { ...state.values, ...action.payload.values }, values: Object.assign({}, state.values, action.payload.values),
// Dump the providers into thirdPartyAuth. // Dump the providers into thirdPartyAuth.
thirdPartyAuth: { ...state.thirdPartyAuth, providers: action.payload.thirdPartyAuthProviders }, thirdPartyAuth: Object.assign({}, state.thirdPartyAuth, {
providers: action.payload.thirdPartyAuthProviders,
}),
profileDataManager: action.payload.profileDataManager, profileDataManager: action.payload.profileDataManager,
timeZones: action.payload.timeZones, timeZones: action.payload.timeZones,
loading: false, loading: false,
loaded: true, loaded: true,
loadingError: null, loadingError: null,
verifiedNameHistory: action.payload.verifiedNameHistory,
}; };
case FETCH_SETTINGS.FAILURE: case FETCH_SETTINGS.FAILURE:
return { return {
@@ -98,14 +91,15 @@ const reducer = (state = defaultState, action) => {
saveState: null, saveState: null,
errors: {}, errors: {},
drafts: {}, drafts: {},
nameChangeModal: false,
}; };
} }
return state; return state;
case UPDATE_DRAFT: case UPDATE_DRAFT:
return { return {
...state, ...state,
drafts: { ...state.drafts, [action.payload.name]: action.payload.value }, drafts: Object.assign({}, state.drafts, {
[action.payload.name]: action.payload.value,
}),
saveState: null, saveState: null,
errors: {}, errors: {},
}; };
@@ -116,15 +110,6 @@ const reducer = (state = defaultState, action) => {
drafts: {}, drafts: {},
}; };
case BEGIN_NAME_CHANGE:
return {
...state,
saveState: 'error',
nameChangeModal: {
formId: action.payload.formId,
},
};
case SAVE_SETTINGS.BEGIN: case SAVE_SETTINGS.BEGIN:
return { return {
...state, ...state,
@@ -135,18 +120,19 @@ const reducer = (state = defaultState, action) => {
return { return {
...state, ...state,
saveState: 'complete', saveState: 'complete',
values: { ...state.values, ...action.payload.values }, values: Object.assign({}, state.values, action.payload.values),
errors: {}, errors: {},
confirmationValues: { confirmationValues: Object.assign(
...state.confirmationValues, {},
...action.payload.confirmationValues, state.confirmationValues,
}, action.payload.confirmationValues,
),
}; };
case SAVE_SETTINGS.FAILURE: case SAVE_SETTINGS.FAILURE:
return { return {
...state, ...state,
saveState: 'error', saveState: 'error',
errors: { ...state.errors, ...action.payload.errors }, errors: Object.assign({}, state.errors, action.payload.errors),
}; };
case SAVE_SETTINGS.RESET: case SAVE_SETTINGS.RESET:
return { return {
@@ -175,7 +161,7 @@ const reducer = (state = defaultState, action) => {
return { return {
...state, ...state,
saveState: 'error', saveState: 'error',
errors: { ...state.errors, ...action.payload.errors }, errors: Object.assign({}, state.errors, action.payload.errors),
}; };
case FETCH_TIME_ZONES.SUCCESS: case FETCH_TIME_ZONES.SUCCESS:
@@ -216,15 +202,6 @@ const reducer = (state = defaultState, action) => {
resetPassword: resetPasswordReducer(state.resetPassword, action), resetPassword: resetPasswordReducer(state.resetPassword, action),
}; };
case REQUEST_NAME_CHANGE.BEGIN:
case REQUEST_NAME_CHANGE.SUCCESS:
case REQUEST_NAME_CHANGE.FAILURE:
case REQUEST_NAME_CHANGE.RESET:
return {
...state,
nameChange: nameChangeReducer(state.nameChange, action),
};
case DISCONNECT_AUTH.BEGIN: case DISCONNECT_AUTH.BEGIN:
case DISCONNECT_AUTH.SUCCESS: case DISCONNECT_AUTH.SUCCESS:
case DISCONNECT_AUTH.FAILURE: case DISCONNECT_AUTH.FAILURE:

View File

@@ -1,6 +1,4 @@
import { import { call, put, delay, takeEvery, all } from 'redux-saga/effects';
call, put, delay, takeEvery, all,
} from 'redux-saga/effects';
import { publish } from '@edx/frontend-platform'; import { publish } from '@edx/frontend-platform';
import { getLocale, handleRtl, LOCALE_CHANGED } from '@edx/frontend-platform/i18n'; import { getLocale, handleRtl, LOCALE_CHANGED } from '@edx/frontend-platform/i18n';
@@ -25,13 +23,11 @@ import {
saveMultipleSettingsBegin, saveMultipleSettingsBegin,
saveMultipleSettingsSuccess, saveMultipleSettingsSuccess,
saveMultipleSettingsFailure, saveMultipleSettingsFailure,
beginNameChange,
} from './actions'; } from './actions';
// Sub-modules // Sub-modules
import { saga as deleteAccountSaga } from '../delete-account'; import { saga as deleteAccountSaga } from '../delete-account';
import { saga as resetPasswordSaga } from '../reset-password'; import { saga as resetPasswordSaga } from '../reset-password';
import { saga as nameChangeSaga } from '../name-change';
import { import {
saga as siteLanguageSaga, saga as siteLanguageSaga,
patchPreferences, patchPreferences,
@@ -40,12 +36,7 @@ import {
import { saga as thirdPartyAuthSaga } from '../third-party-auth'; import { saga as thirdPartyAuthSaga } from '../third-party-auth';
// Services // Services
import { import { getSettings, patchSettings, getTimeZones } from './service';
getSettings,
patchSettings,
getTimeZones,
getVerifiedNameHistory,
} from './service';
export function* handleFetchSettings() { export function* handleFetchSettings() {
try { try {
@@ -61,16 +52,13 @@ export function* handleFetchSettings() {
userId, userId,
); );
const verifiedNameHistory = yield call(getVerifiedNameHistory); if (values.country) yield put(fetchTimeZones(values.country));
if (values.country) { yield put(fetchTimeZones(values.country)); }
yield put(fetchSettingsSuccess({ yield put(fetchSettingsSuccess({
values, values,
thirdPartyAuthProviders, thirdPartyAuthProviders,
profileDataManager, profileDataManager,
timeZones, timeZones,
verifiedNameHistory,
})); }));
} catch (e) { } catch (e) {
yield put(fetchSettingsFailure(e.message)); yield put(fetchSettingsFailure(e.message));
@@ -103,14 +91,11 @@ export function* handleSaveSettings(action) {
savedValues = yield call(patchSettings, username, commitData, userId); savedValues = yield call(patchSettings, username, commitData, userId);
} }
yield put(saveSettingsSuccess(savedValues, commitData)); yield put(saveSettingsSuccess(savedValues, commitData));
if (savedValues.country) { yield put(fetchTimeZones(savedValues.country)); } if (savedValues.country) yield put(fetchTimeZones(savedValues.country));
yield delay(1000); yield delay(1000);
yield put(closeForm(action.payload.formId)); yield put(closeForm(action.payload.formId));
} catch (e) { } catch (e) {
if (e.fieldErrors) { if (e.fieldErrors) {
if (e.fieldErrors.name?.includes('verification')) {
yield put(beginNameChange('name'));
}
yield put(saveSettingsFailure({ fieldErrors: e.fieldErrors })); yield put(saveSettingsFailure({ fieldErrors: e.fieldErrors }));
} else { } else {
yield put(saveSettingsFailure(e.message)); yield put(saveSettingsFailure(e.message));
@@ -119,6 +104,7 @@ export function* handleSaveSettings(action) {
} }
} }
// handles mutiple settings saved at once, in order, and stops executing on first failure. // handles mutiple settings saved at once, in order, and stops executing on first failure.
export function* handleSaveMultipleSettings(action) { export function* handleSaveMultipleSettings(action) {
try { try {
@@ -139,9 +125,6 @@ export function* handleSaveMultipleSettings(action) {
} }
} catch (e) { } catch (e) {
if (e.fieldErrors) { if (e.fieldErrors) {
if (e.fieldErrors.name?.includes('verification')) {
yield put(beginNameChange('name'));
}
yield put(saveMultipleSettingsFailure({ fieldErrors: e.fieldErrors })); yield put(saveMultipleSettingsFailure({ fieldErrors: e.fieldErrors }));
} else { } else {
yield put(saveMultipleSettingsFailure(e.message)); yield put(saveMultipleSettingsFailure(e.message));
@@ -155,6 +138,7 @@ export function* handleFetchTimeZones(action) {
yield put(fetchTimeZonesSuccess(response, action.payload.country)); yield put(fetchTimeZonesSuccess(response, action.payload.country));
} }
export default function* saga() { export default function* saga() {
yield takeEvery(FETCH_SETTINGS.BASE, handleFetchSettings); yield takeEvery(FETCH_SETTINGS.BASE, handleFetchSettings);
yield takeEvery(SAVE_SETTINGS.BASE, handleSaveSettings); yield takeEvery(SAVE_SETTINGS.BASE, handleSaveSettings);
@@ -164,7 +148,6 @@ export default function* saga() {
deleteAccountSaga(), deleteAccountSaga(),
siteLanguageSaga(), siteLanguageSaga(),
resetPasswordSaga(), resetPasswordSaga(),
nameChangeSaga(),
thirdPartyAuthSaga(), thirdPartyAuthSaga(),
]); ]);
} }

View File

@@ -1,6 +1,5 @@
import { createSelector, createStructuredSelector } from 'reselect'; import { createSelector, createStructuredSelector } from 'reselect';
import { siteLanguageListSelector, siteLanguageOptionsSelector } from '../site-language'; import { siteLanguageListSelector, siteLanguageOptionsSelector } from '../site-language';
import { compareVerifiedNamesByCreatedDate } from '../../utils';
export const storeName = 'accountSettings'; export const storeName = 'accountSettings';
@@ -8,75 +7,9 @@ export const accountSettingsSelector = state => ({ ...state[storeName] });
const editableFieldNameSelector = (state, props) => props.name; const editableFieldNameSelector = (state, props) => props.name;
const verifiedNameSettingsSelector = createSelector(
accountSettingsSelector,
accountSettings => ({
history: accountSettings.verifiedNameHistory.results,
verifiedNameEnabled: accountSettings?.verifiedNameHistory.verified_name_enabled,
useVerifiedNameForCerts: accountSettings?.verifiedNameHistory.use_verified_name_for_certs,
}),
);
const sortedVerifiedNameHistorySelector = createSelector(
verifiedNameSettingsSelector,
verifiedNameSettings => {
const { history } = verifiedNameSettings;
if (Array.isArray(history)) {
return history.sort(compareVerifiedNamesByCreatedDate);
}
return [];
},
);
const mostRecentVerifiedNameSelector = createSelector(
sortedVerifiedNameHistorySelector,
sortedHistory => (sortedHistory.length > 0 ? sortedHistory[0] : null),
);
const mostRecentApprovedVerifiedNameValueSelector = createSelector(
sortedVerifiedNameHistorySelector,
mostRecentVerifiedNameSelector,
(sortedHistory, mostRecentVerifiedName) => {
const approvedVerifiedNames = sortedHistory.filter(name => name.status === 'approved');
const approvedVerifiedName = approvedVerifiedNames.length > 0 ? approvedVerifiedNames[0] : null;
let verifiedName = null;
switch (mostRecentVerifiedName && mostRecentVerifiedName.status) {
case 'approved':
case 'denied':
case 'pending':
verifiedName = approvedVerifiedName;
break;
case 'submitted':
verifiedName = mostRecentVerifiedName;
break;
default:
verifiedName = null;
}
return verifiedName;
},
);
const valuesSelector = createSelector( const valuesSelector = createSelector(
accountSettingsSelector, accountSettingsSelector,
mostRecentApprovedVerifiedNameValueSelector, accountSettings => accountSettings.values,
(accountSettings, mostRecentApprovedVerifiedNameValue) => {
let useVerifiedNameForCerts = (
accountSettings.verifiedNameHistory?.use_verified_name_for_certs || false
);
if (Object.keys(accountSettings.confirmationValues).includes('useVerifiedNameForCerts')) {
useVerifiedNameForCerts = accountSettings.confirmationValues.useVerifiedNameForCerts;
}
return {
...accountSettings.values,
verified_name: mostRecentApprovedVerifiedNameValue?.verified_name,
useVerifiedNameForCerts,
};
},
); );
const draftsSelector = createSelector( const draftsSelector = createSelector(
@@ -117,11 +50,6 @@ const errorSelector = createSelector(
accountSettings => accountSettings.errors, accountSettings => accountSettings.errors,
); );
const nameChangeModalSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.nameChangeModal,
);
const saveStateSelector = createSelector( const saveStateSelector = createSelector(
accountSettingsSelector, accountSettingsSelector,
accountSettings => accountSettings.saveState, accountSettings => accountSettings.saveState,
@@ -141,20 +69,10 @@ export const profileDataManagerSelector = createSelector(
export const staticFieldsSelector = createSelector( export const staticFieldsSelector = createSelector(
accountSettingsSelector, accountSettingsSelector,
mostRecentVerifiedNameSelector, accountSettings => (accountSettings.profileDataManager ? ['name', 'email', 'country'] : []),
(accountSettings, verifiedName) => {
const staticFields = [];
if (accountSettings.profileDataManager) {
staticFields.push('name', 'email', 'country');
}
if (verifiedName && ['submitted'].includes(verifiedName.status)) {
staticFields.push('verifiedName');
}
return staticFields;
},
); );
/** /**
* If there's no draft present at all (undefined), use the original committed value. * If there's no draft present at all (undefined), use the original committed value.
*/ */
@@ -168,11 +86,7 @@ const formValuesSelector = createSelector(
(values, drafts) => { (values, drafts) => {
const formValues = {}; const formValues = {};
Object.entries(values).forEach(([name, value]) => { Object.entries(values).forEach(([name, value]) => {
if (typeof value === 'boolean') { formValues[name] = chooseFormValue(drafts[name], value) || '';
formValues[name] = chooseFormValue(drafts[name], value);
} else {
formValues[name] = chooseFormValue(drafts[name], value) || '';
}
}); });
return formValues; return formValues;
}, },
@@ -217,37 +131,21 @@ export const accountSettingsPageSelector = createSelector(
siteLanguageOptionsSelector, siteLanguageOptionsSelector,
siteLanguageSelector, siteLanguageSelector,
formValuesSelector, formValuesSelector,
valuesSelector,
draftsSelector,
errorSelector,
profileDataManagerSelector, profileDataManagerSelector,
staticFieldsSelector, staticFieldsSelector,
timeZonesSelector, timeZonesSelector,
countryTimeZonesSelector, countryTimeZonesSelector,
activeAccountSelector, activeAccountSelector,
nameChangeModalSelector,
mostRecentApprovedVerifiedNameValueSelector,
mostRecentVerifiedNameSelector,
sortedVerifiedNameHistorySelector,
verifiedNameSettingsSelector,
( (
accountSettings, accountSettings,
siteLanguageOptions, siteLanguageOptions,
siteLanguage, siteLanguage,
formValues, formValues,
committedValues,
drafts,
formErrors,
profileDataManager, profileDataManager,
staticFields, staticFields,
timeZoneOptions, timeZoneOptions,
countryTimeZoneOptions, countryTimeZoneOptions,
activeAccount, activeAccount,
nameChangeModal,
verifiedName,
mostRecentVerifiedName,
verifiedNameHistory,
verifiedNameSettings,
) => ({ ) => ({
siteLanguageOptions, siteLanguageOptions,
siteLanguage, siteLanguage,
@@ -258,41 +156,9 @@ export const accountSettingsPageSelector = createSelector(
countryTimeZoneOptions, countryTimeZoneOptions,
isActive: activeAccount, isActive: activeAccount,
formValues, formValues,
committedValues,
drafts,
formErrors,
profileDataManager, profileDataManager,
staticFields, staticFields,
tpaProviders: accountSettings.thirdPartyAuth.providers, tpaProviders: accountSettings.thirdPartyAuth.providers,
nameChangeModal,
verifiedName,
mostRecentVerifiedName,
verifiedNameHistory,
verifiedNameEnabled: verifiedNameSettings?.verifiedNameEnabled,
}),
);
export const certPreferenceSelector = createSelector(
verifiedNameSettingsSelector,
valuesSelector,
formValuesSelector,
mostRecentApprovedVerifiedNameValueSelector,
saveStateSelector,
errorSelector,
(
verifiedNameSettings,
committedValues,
formValues,
mostRecentApprovedVerifiedNameValue,
saveState,
errors,
) => ({
originalFullName: committedValues?.name || '',
originalVerifiedName: mostRecentApprovedVerifiedNameValue?.verified_name || '',
useVerifiedNameForCerts: formValues.useVerifiedNameForCerts || false,
saveState,
verifiedNameEnabled: verifiedNameSettings.verifiedNameEnabled || false,
formErrors: errors,
}), }),
); );
@@ -339,12 +205,3 @@ export const demographicsSectionSelector = createSelector(
formErrors: errors, formErrors: errors,
}), }),
); );
export const nameChangeSelector = createSelector(
accountSettingsSelector,
formValuesSelector,
(accountSettings, formValues) => ({
...accountSettings.nameChange,
formValues,
}),
);

View File

@@ -7,10 +7,8 @@ import isEmpty from 'lodash.isempty';
import { handleRequestError, unpackFieldErrors } from './utils'; import { handleRequestError, unpackFieldErrors } from './utils';
import { getThirdPartyAuthProviders } from '../third-party-auth'; import { getThirdPartyAuthProviders } from '../third-party-auth';
import { postVerifiedNameConfig } from '../certificate-preference/data/service';
import { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service'; import { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service';
import { getDemographics, getDemographicsOptions, patchDemographics } from '../demographics/data/service'; import { getDemographics, patchDemographics } from '../demographics/data/service';
import { DEMOGRAPHICS_FIELDS } from '../demographics/data/utils';
const SOCIAL_PLATFORMS = [ const SOCIAL_PLATFORMS = [
{ id: 'twitter', key: 'social_link_twitter' }, { id: 'twitter', key: 'social_link_twitter' },
@@ -45,9 +43,7 @@ function packAccountCommitData(commitData) {
SOCIAL_PLATFORMS.forEach(({ id, key }) => { SOCIAL_PLATFORMS.forEach(({ id, key }) => {
// Skip missing values. Empty strings are valid values and should be preserved. // Skip missing values. Empty strings are valid values and should be preserved.
if (commitData[key] === undefined) { if (commitData[key] === undefined) return;
return;
}
packedData.social_links = [{ platform: id, social_link: commitData[key] }]; packedData.social_links = [{ platform: id, social_link: commitData[key] }];
delete packedData[key]; delete packedData[key];
@@ -155,113 +151,29 @@ export async function getProfileDataManager(username, userRoles) {
return null; 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 getVerifiedNameEnabled() {
let data;
const client = getAuthenticatedHttpClient();
try {
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name_enabled`;
({ data } = await client.get(requestUrl));
} catch (error) {
return {};
}
return data;
}
export async function getVerifiedName() {
let data;
const client = getAuthenticatedHttpClient();
try {
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name`;
({ data } = await client.get(requestUrl));
} catch (error) {
return {};
}
return data;
}
export async function getVerifiedNameHistory() {
let data;
const client = getAuthenticatedHttpClient();
try {
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name/history`;
({ data } = await client.get(requestUrl));
} catch (error) {
return {};
}
return data;
}
export async function postVerifiedName(data) {
const requestConfig = { headers: { Accept: 'application/json' } };
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name`;
await getAuthenticatedHttpClient()
.post(requestUrl, data, requestConfig)
.catch(error => handleRequestError(error));
}
/** /**
* A single function to GET everything considered a setting. * A single function to GET everything considered a setting.
* Currently encapsulates Account, Preferences, Coaching, ThirdPartyAuth, and Demographics * Currently encapsulates Account, Preferences, Coaching, and ThirdPartyAuth
*/ */
export async function getSettings(username, userRoles, userId) { export async function getSettings(username, userRoles, userId) {
const [ const results = await Promise.all([
account,
preferences,
thirdPartyAuthProviders,
profileDataManager,
timeZones,
coaching,
shouldDisplayDemographicsQuestionsResponse,
demographics,
demographicsOptions,
] = await Promise.all([
getAccount(username), getAccount(username),
getPreferences(username), getPreferences(username),
getThirdPartyAuthProviders(), getThirdPartyAuthProviders(),
getProfileDataManager(username, userRoles), getProfileDataManager(username, userRoles),
getTimeZones(), getTimeZones(),
getConfig().COACHING_ENABLED && getCoachingPreferences(userId), getConfig().COACHING_ENABLED && getCoachingPreferences(userId),
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && shouldDisplayDemographicsQuestions(),
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographics(userId), getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographics(userId),
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographicsOptions(),
]); ]);
return { return {
...account, ...results[0],
...preferences, ...results[1],
thirdPartyAuthProviders, thirdPartyAuthProviders: results[2],
profileDataManager, profileDataManager: results[3],
timeZones, timeZones: results[4],
coaching, coaching: results[5],
shouldDisplayDemographicsSection: shouldDisplayDemographicsQuestionsResponse, ...results[6], // demographics
...demographics,
demographicsOptions,
}; };
} }
@@ -275,20 +187,11 @@ export async function patchSettings(username, commitValues, userId) {
// user/v1/preferences where it does update. This is the one we use. // user/v1/preferences where it does update. This is the one we use.
const preferenceKeys = ['time_zone']; const preferenceKeys = ['time_zone'];
const coachingKeys = ['coaching']; const coachingKeys = ['coaching'];
const demographicsKeys = DEMOGRAPHICS_FIELDS;
const certificateKeys = ['useVerifiedNameForCerts'];
const isDemographicsKey = (value, key) => key.includes('demographics'); const isDemographicsKey = (value, key) => key.includes('demographics');
const accountCommitValues = omit( const accountCommitValues = omit(commitValues, preferenceKeys, coachingKeys);
commitValues,
preferenceKeys,
coachingKeys,
demographicsKeys,
certificateKeys,
);
const preferenceCommitValues = pick(commitValues, preferenceKeys); const preferenceCommitValues = pick(commitValues, preferenceKeys);
const coachingCommitValues = pick(commitValues, coachingKeys); const coachingCommitValues = pick(commitValues, coachingKeys);
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey); const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
const certCommitValues = pick(commitValues, certificateKeys);
const patchRequests = []; const patchRequests = [];
if (!isEmpty(accountCommitValues)) { if (!isEmpty(accountCommitValues)) {
@@ -303,9 +206,6 @@ export async function patchSettings(username, commitValues, userId) {
if (!isEmpty(demographicsCommitValues)) { if (!isEmpty(demographicsCommitValues)) {
patchRequests.push(patchDemographics(userId, demographicsCommitValues)); patchRequests.push(patchDemographics(userId, demographicsCommitValues));
} }
if (!isEmpty(certCommitValues)) {
patchRequests.push(postVerifiedNameConfig(username, certCommitValues));
}
const results = await Promise.all(patchRequests); const results = await Promise.all(patchRequests);
// Assigns in order of requests. Preference keys // Assigns in order of requests. Preference keys

View File

@@ -0,0 +1,38 @@
import camelCase from 'lodash.camelcase';
import snakeCase from 'lodash.snakecase';
export function modifyObjectKeys(object, modify) {
// If the passed in object is not an object, return it.
if (
object === undefined ||
object === null ||
(typeof object !== 'object' && !Array.isArray(object))
) {
return object;
}
if (Array.isArray(object)) {
return object.map(value => modifyObjectKeys(value, modify));
}
// Otherwise, process all its keys.
const result = {};
Object.entries(object).forEach(([key, value]) => {
result[modify(key)] = modifyObjectKeys(value, modify);
});
return result;
}
export function camelCaseObject(object) {
return modifyObjectKeys(object, camelCase);
}
export function snakeCaseObject(object) {
return modifyObjectKeys(object, snakeCase);
}
export function convertKeyNames(object, nameMap) {
const transformer = key => (nameMap[key] === undefined ? key : nameMap[key]);
return modifyObjectKeys(object, transformer);
}

View File

@@ -0,0 +1,90 @@
import {
modifyObjectKeys,
camelCaseObject,
snakeCaseObject,
convertKeyNames,
} from './dataUtils';
describe('modifyObjectKeys', () => {
it('should use the provided modify function to change all keys in and object and its children', () => {
function meowKeys(key) {
return `${key}Meow`;
}
const result = modifyObjectKeys(
{
one: undefined,
two: null,
three: '',
four: 0,
five: NaN,
six: [1, 2, { seven: 'woof' }],
eight: { nine: { ten: 'bark' }, eleven: true },
},
meowKeys,
);
expect(result).toEqual({
oneMeow: undefined,
twoMeow: null,
threeMeow: '',
fourMeow: 0,
fiveMeow: NaN,
sixMeow: [1, 2, { sevenMeow: 'woof' }],
eightMeow: { nineMeow: { tenMeow: 'bark' }, elevenMeow: true },
});
});
});
describe('camelCaseObject', () => {
it('should make everything camelCase', () => {
const result = camelCaseObject({
what_now: 'brown cow',
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
'dot.dot.dot': 123,
});
expect(result).toEqual({
whatNow: 'brown cow',
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
dotDotDot: 123,
});
});
});
describe('snakeCaseObject', () => {
it('should make everything snake_case', () => {
const result = snakeCaseObject({
whatNow: 'brown cow',
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
'dot.dot.dot': 123,
});
expect(result).toEqual({
what_now: 'brown cow',
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
dot_dot_dot: 123,
});
});
});
describe('convertKeyNames', () => {
it('should replace the specified keynames', () => {
const result = convertKeyNames(
{
one: { two: { three: 'four' } },
five: 'six',
},
{
two: 'blue',
five: 'alive',
seven: 'heaven',
},
);
expect(result).toEqual({
one: { blue: { three: 'four' } },
alive: 'six',
});
});
});

View File

@@ -1,3 +1,9 @@
export {
camelCaseObject,
convertKeyNames,
modifyObjectKeys,
snakeCaseObject,
} from './dataUtils';
export { export {
AsyncActionType, AsyncActionType,
getModuleState, getModuleState,

View File

@@ -6,7 +6,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Hyperlink } from '@edx/paragon'; import { Hyperlink } from '@edx/paragon';
// Messages // Messages
import { getConfig } from '@edx/frontend-platform';
import messages from './messages'; import messages from './messages';
// Components // Components
@@ -23,14 +22,13 @@ const BeforeProceedingBanner = (props) => {
<FormattedMessage <FormattedMessage
id="account.settings.delete.account.before.proceeding" id="account.settings.delete.account.before.proceeding"
defaultMessage="Before proceeding, please {actionLink}." 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'." description="Error that appears if you are trying to delete your edX account, but something about your account needs attention first. The actionLink will be instructions, such as 'unlink your Facebook account'."
values={{ values={{
actionLink: ( actionLink: (
<Hyperlink destination={supportArticleUrl}> <Hyperlink destination={supportArticleUrl}>
{intl.formatMessage(messages[instructionMessageId])} {intl.formatMessage(messages[instructionMessageId])}
</Hyperlink> </Hyperlink>
), ),
siteName: getConfig().SITE_NAME,
}} }}
/> />
</Alert> </Alert>

View File

@@ -1,14 +1,11 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import { Button, Input, Modal, ValidationFormGroup } from '@edx/paragon';
Button, Input, Modal, ValidationFormGroup,
} from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { faExclamationCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { faExclamationCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages'; import messages from './messages';
import Alert from '../Alert'; import Alert from '../Alert';
import PrintingInstructions from './PrintingInstructions'; import PrintingInstructions from './PrintingInstructions';
@@ -36,9 +33,10 @@ export class ConfirmationModal extends Component {
return null; return null;
} }
const headerMessageId = this.getShortErrorMessageId(errorType); const headerMessageId = this.getShortErrorMessageId(errorType);
const detailsMessageId = reason === 'empty-password' const detailsMessageId =
? null reason === 'empty-password'
: 'account.settings.delete.account.error.unable.to.delete.details'; ? null
: 'account.settings.delete.account.error.unable.to.delete.details';
return ( return (
<Alert <Alert
@@ -66,18 +64,11 @@ export class ConfirmationModal extends Component {
const open = ['confirming', 'pending', 'failed'].includes(status); const open = ['confirming', 'pending', 'failed'].includes(status);
const passwordFieldId = 'passwordFieldId'; const passwordFieldId = 'passwordFieldId';
const invalidMessage = messages[this.getShortErrorMessageId(errorType)]; const invalidMessage = messages[this.getShortErrorMessageId(errorType)];
// 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.
const deleteAccountModalText2MessageKey = getConfig().SITE_NAME === 'edX'
? 'account.settings.delete.account.modal.text.2.edX'
: 'account.settings.delete.account.modal.text.2';
return ( return (
<Modal <Modal
open={open} open={open}
title={intl.formatMessage(messages['account.settings.delete.account.modal.header'])} title={intl.formatMessage(messages['account.settings.delete.account.modal.header'])}
body={( body={
<div> <div>
{this.renderError()} {this.renderError()}
<Alert <Alert
@@ -85,17 +76,9 @@ export class ConfirmationModal extends Component {
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />} icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
> >
<h6> <h6>
{intl.formatMessage( {intl.formatMessage(messages['account.settings.delete.account.modal.text.1'])}
messages['account.settings.delete.account.modal.text.1'],
{ siteName: getConfig().SITE_NAME },
)}
</h6> </h6>
<p> <p>{intl.formatMessage(messages['account.settings.delete.account.modal.text.2'])}</p>
{intl.formatMessage(
messages[deleteAccountModalText2MessageKey],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p> <p>
<PrintingInstructions /> <PrintingInstructions />
</p> </p>
@@ -117,9 +100,9 @@ export class ConfirmationModal extends Component {
/> />
</ValidationFormGroup> </ValidationFormGroup>
</div> </div>
)} }
buttons={[ buttons={[
<Button variant="danger" onClick={onSubmit}> <Button className="btn-danger" onClick={onSubmit}>
{intl.formatMessage(messages['account.settings.delete.account.modal.confirm.delete'])} {intl.formatMessage(messages['account.settings.delete.account.modal.confirm.delete'])}
</Button>, </Button>,
]} ]}

View File

@@ -24,13 +24,9 @@ import ConnectedSuccessModal from './SuccessModal';
import BeforeProceedingBanner from './BeforeProceedingBanner'; import BeforeProceedingBanner from './BeforeProceedingBanner';
export class DeleteAccount extends React.Component { export class DeleteAccount extends React.Component {
constructor(props) { state = {
super(props); password: '',
};
this.state = {
password: '',
};
}
handleSubmit = () => { handleSubmit = () => {
if (this.state.password === '') { if (this.state.password === '') {
@@ -60,36 +56,19 @@ export class DeleteAccount extends React.Component {
} = this.props; } = this.props;
const canDelete = isVerifiedAccount && !hasLinkedTPA; const canDelete = isVerifiedAccount && !hasLinkedTPA;
// 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.
const deleteAccountText2MessageKey = getConfig().SITE_NAME === 'edX'
? 'account.settings.delete.account.text.2.edX'
: 'account.settings.delete.account.text.2';
return ( return (
<div> <div>
<h2 className="section-heading"> <h2 className="section-heading">
{intl.formatMessage(messages['account.settings.delete.account.header'])} {intl.formatMessage(messages['account.settings.delete.account.header'])}
</h2> </h2>
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p> <p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
<p> <p>{intl.formatMessage(messages['account.settings.delete.account.text.1'])}</p>
{intl.formatMessage( <p>{intl.formatMessage(messages['account.settings.delete.account.text.2'])}</p>
messages['account.settings.delete.account.text.1'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
{intl.formatMessage(
messages[deleteAccountText2MessageKey],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p> <p>
<PrintingInstructions /> <PrintingInstructions />
</p> </p>
<p className="text-danger h6"> <p className="text-danger h6">
{intl.formatMessage(messages['account.settings.delete.account.text.warning'], {intl.formatMessage(messages['account.settings.delete.account.text.warning'])}
{ siteName: getConfig().SITE_NAME })}
</p> </p>
<p> <p>
<Hyperlink destination="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"> <Hyperlink destination="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings">
@@ -98,7 +77,7 @@ export class DeleteAccount extends React.Component {
</p> </p>
<p> <p>
<Button <Button
variant="outline-danger" className="btn-outline-danger"
onClick={canDelete ? this.props.deleteAccountConfirmation : null} onClick={canDelete ? this.props.deleteAccountConfirmation : null}
disabled={!canDelete} disabled={!canDelete}
> >

View File

@@ -2,39 +2,22 @@ import React from 'react';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon'; import { Hyperlink } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages'; import messages from './messages';
const PrintingInstructions = (props) => { const PrintingInstructions = (props) => {
const actionLink = ( const actionLink = (
<Hyperlink <Hyperlink
// TODO: What would a generic version of this link look like? Should destination="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
// CERTIFICATE_SHARING_HELP_URL really be a configuration variable? In the meantime,
// We've removed the link from the default message.
destination="https://support.edx.org/hc/en-us/sections/115004173027-Receive-and-Share-edX-Certificates"
> >
{props.intl.formatMessage(messages['account.settings.delete.account.text.3.link'])} {props.intl.formatMessage(messages['account.settings.delete.account.text.3.link'])}
</Hyperlink> </Hyperlink>
); );
// TODO: We lack a good way of providing custom language for a particular site. This is a hack
// to allow edx.org to mention MicroMasters certificates to fulfill its business requirements.
if (getConfig().SITE_NAME === 'edX') {
return (
<FormattedMessage
id="account.settings.delete.account.text.3.edX"
defaultMessage="You may also lose access to verified certificates and other program credentials like MicroMasters certificates. You can make a copy of these for your records before proceeding with deletion. {actionLink}."
description="A message in the user account deletion area warning users that deleting their account will prevent them from accessing their certificates. 'actionLink' is a HTML link with a full sentence that describes how to print a certificate."
values={{ actionLink }}
/>
);
}
return ( return (
<FormattedMessage <FormattedMessage
id="account.settings.delete.account.text.3" id="account.settings.delete.account.text.3"
defaultMessage="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." defaultMessage="You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}."
description="A message in the user account deletion area warning users that deleting their account will prevent them from accessing their certificates." description="A message in the user account deletion area"
values={{ actionLink }} values={{ actionLink }}
/> />
); );

View File

@@ -11,13 +11,13 @@ export const SuccessModal = (props) => {
<Modal <Modal
open={status === 'deleted'} open={status === 'deleted'}
title={intl.formatMessage(messages['account.settings.delete.account.modal.after.header'])} title={intl.formatMessage(messages['account.settings.delete.account.modal.after.header'])}
body={( body={
<div> <div>
<p className="h6"> <p className="h6">
{intl.formatMessage(messages['account.settings.delete.account.modal.after.text'])} {intl.formatMessage(messages['account.settings.delete.account.modal.after.text'])}
</p> </p>
</div> </div>
)} }
closeText={intl.formatMessage(messages['account.settings.delete.account.modal.after.button'])} closeText={intl.formatMessage(messages['account.settings.delete.account.modal.after.button'])}
renderHeaderCloseButton={false} renderHeaderCloseButton={false}
onClose={onClose} onClose={onClose}

View File

@@ -1,566 +1,439 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConfirmationModal should match default closed confirmation modal snapshot 1`] = ` exports[`ConfirmationModal should match default closed confirmation modal snapshot 1`] = `
Array [ <div>
<div <div
className="fade" className="fade"
role="presentation" role="presentation"
/>, />
<div <div
className="modal fade" className="modal js-close-modal-on-click fade"
role="presentation" onMouseDown={[Function]}
>
<div
aria-labelledby="id1"
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-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="id1"
>
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>
<span>
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.
</span>
</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>,
]
`;
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-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={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>
<span>
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.
</span>
</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" role="presentation"
> >
<div <div
aria-labelledby="id2" aria-labelledby="id2"
aria-modal={true} aria-modal={true}
className="modal-dialog" className=""
role="dialog" role="dialog"
tabIndex="-1"
> >
<div <div
data-focus-guard={true} className="modal-content"
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>
<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={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onTouchStart={[Function]}
onWheelCapture={[Function]}
> >
<div <div
className="modal-content" className="modal-header"
> >
<div <h2
className="modal-header" className="modal-title"
id="id2"
> >
<h2 Are you sure?
className="modal-title" </h2>
id="id2" </div>
<div
className="modal-body"
>
<div>
<div
className="alert d-flex align-items-start alert-warning mt-n2"
> >
Are you sure? <div>
</h2> <svg
</div> aria-hidden="true"
<div className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
className="modal-body" data-icon="exclamation-triangle"
> data-prefix="fas"
<div> focusable="false"
<div role="img"
className="alert d-flex align-items-start alert-warning mt-n2" style={Object {}}
> viewBox="0 0 576 512"
<div> xmlns="http://www.w3.org/2000/svg"
<svg >
aria-hidden="true" <path
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2" 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"
data-icon="exclamation-triangle" fill="currentColor"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}} style={Object {}}
viewBox="0 0 576 512" />
xmlns="http://www.w3.org/2000/svg" </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>
<span>
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.
</span>
</p>
</div>
</div> </div>
<div <div>
className="form-group" <h6>
> You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
<label </h6>
className="d-block" <p>
htmlFor="passwordFieldId" If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
> </p>
If you still wish to continue and delete your account, please enter your account password: <p>
</label> <span>
<input You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
aria-describedby="" <a
className="form-control" href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
id="passwordFieldId" onClick={[Function]}
name="password" target="_self"
onChange={[MockFunction]} >
type="password" follow the instructions for printing or downloading a certificate
value="fluffy bunnies" </a>
/> .
<strong </span>
className="invalid-feedback" </p>
id="passwordFieldId-invalid-feedback"
>
Unable to delete account
</strong>
</div> </div>
</div> </div>
</div> <div
<div className="form-group"
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
> >
Cancel <label
</button> className="d-block"
<button htmlFor="passwordFieldId"
className="btn btn-danger" >
disabled={false} If you still wish to continue and delete your account, please enter your account password:
onClick={[MockFunction]} </label>
type="button" <input
> aria-describedby=""
Yes, Delete className="form-control"
</button> 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> </div>
<div
className="modal-footer"
>
<button
className="btn btn-danger"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Yes, Delete
</button>
<button
className="btn js-close-modal-on-click btn-secondary"
id="paragonCloseModalButton1"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Cancel
</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>
</div>, </div>
] </div>
`;
exports[`ConfirmationModal should match empty password confirmation modal snapshot 1`] = `
<div>
<div
className="modal-backdrop show"
role="presentation"
/>
<div
className="modal js-close-modal-on-click show d-block"
onMouseDown={[Function]}
role="presentation"
>
<div
aria-labelledby="id6"
aria-modal={true}
className="modal-dialog"
role="dialog"
tabIndex="-1"
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id6"
>
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. edX 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 the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
</p>
<p>
<span>
You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
<a
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
onClick={[Function]}
target="_self"
>
follow the instructions for printing or downloading a certificate
</a>
.
</span>
</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-danger"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Yes, Delete
</button>
<button
className="btn js-close-modal-on-click btn-secondary"
id="paragonCloseModalButton5"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
<div>
<div
className="modal-backdrop show"
role="presentation"
/>
<div
className="modal js-close-modal-on-click show d-block"
onMouseDown={[Function]}
role="presentation"
>
<div
aria-labelledby="id4"
aria-modal={true}
className="modal-dialog"
role="dialog"
tabIndex="-1"
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id4"
>
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. edX 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 the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
</p>
<p>
<span>
You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
<a
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
onClick={[Function]}
target="_self"
>
follow the instructions for printing or downloading a certificate
</a>
.
</span>
</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-danger"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Yes, Delete
</button>
<button
className="btn js-close-modal-on-click btn-secondary"
id="paragonCloseModalButton3"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
`; `;

View File

@@ -11,24 +11,31 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
We're sorry to see you go! We're sorry to see you go!
</p> </p>
<p> <p>
Please note: 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. Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
</p> </p>
<p> <p>
Once your account is deleted, you cannot use it to take courses on localhost. Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
</p> </p>
<p> <p>
<span> <span>
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. You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
<a
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
onClick={[Function]}
target="_self"
>
follow the instructions for printing or downloading a certificate
</a>
.
</span> </span>
</p> </p>
<p <p
className="text-danger h6" className="text-danger h6"
> >
Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on localhost. Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.
</p> </p>
<p> <p>
<a <a
className="default-link standalone-link"
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings" href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
onClick={[Function]} onClick={[Function]}
target="_self" target="_self"
@@ -40,7 +47,9 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
<button <button
className="btn btn-outline-danger" className="btn btn-outline-danger"
disabled={false} disabled={false}
onClick={[MockFunction]} onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button" type="button"
> >
Delete My Account Delete My Account
@@ -60,24 +69,31 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
We're sorry to see you go! We're sorry to see you go!
</p> </p>
<p> <p>
Please note: 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. Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
</p> </p>
<p> <p>
Once your account is deleted, you cannot use it to take courses on localhost. Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
</p> </p>
<p> <p>
<span> <span>
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. You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
<a
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
onClick={[Function]}
target="_self"
>
follow the instructions for printing or downloading a certificate
</a>
.
</span> </span>
</p> </p>
<p <p
className="text-danger h6" className="text-danger h6"
> >
Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on localhost. Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.
</p> </p>
<p> <p>
<a <a
className="default-link standalone-link"
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings" href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
onClick={[Function]} onClick={[Function]}
target="_self" target="_self"
@@ -89,7 +105,9 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
<button <button
className="btn btn-outline-danger" className="btn btn-outline-danger"
disabled={true} disabled={true}
onClick={null} onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button" type="button"
> >
Delete My Account Delete My Account
@@ -121,7 +139,6 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
<span> <span>
Before proceeding, please Before proceeding, please
<a <a
className="default-link standalone-link"
href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-activate-my-account-" href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-activate-my-account-"
onClick={[Function]} onClick={[Function]}
target="_self" target="_self"
@@ -146,24 +163,31 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
We're sorry to see you go! We're sorry to see you go!
</p> </p>
<p> <p>
Please note: 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. Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
</p> </p>
<p> <p>
Once your account is deleted, you cannot use it to take courses on localhost. Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
</p> </p>
<p> <p>
<span> <span>
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. You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
<a
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
onClick={[Function]}
target="_self"
>
follow the instructions for printing or downloading a certificate
</a>
.
</span> </span>
</p> </p>
<p <p
className="text-danger h6" className="text-danger h6"
> >
Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on localhost. Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.
</p> </p>
<p> <p>
<a <a
className="default-link standalone-link"
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings" href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
onClick={[Function]} onClick={[Function]}
target="_self" target="_self"
@@ -175,7 +199,9 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
<button <button
className="btn btn-outline-danger" className="btn btn-outline-danger"
disabled={true} disabled={true}
onClick={null} onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button" type="button"
> >
Delete My Account Delete My Account
@@ -207,7 +233,6 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
<span> <span>
Before proceeding, please Before proceeding, please
<a <a
className="default-link standalone-link"
href="https://support.edx.org/hc/en-us/articles/207206067" href="https://support.edx.org/hc/en-us/articles/207206067"
onClick={[Function]} onClick={[Function]}
target="_self" target="_self"

View File

@@ -1,125 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SuccessModal should match default closed success modal snapshot 1`] = ` exports[`SuccessModal should match default closed success modal snapshot 1`] = `
Array [ <div>
<div <div
className="fade" className="fade"
role="presentation" role="presentation"
/>, />
<div <div
className="modal fade" className="modal js-close-modal-on-click fade"
role="presentation" onMouseDown={[Function]}
>
<div
aria-labelledby="id1"
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-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="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>
</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" role="presentation"
> >
<div <div
@@ -127,223 +16,61 @@ Array [
aria-modal={true} aria-modal={true}
className="" className=""
role="dialog" role="dialog"
tabIndex="-1"
> >
<div <div
data-focus-guard={true} className="modal-content"
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<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 <div
className="modal-content" className="modal-header"
> >
<div <h2
className="modal-header" className="modal-title"
id="id2"
> >
<h2 We're sorry to see you go! Your account will be deleted shortly.
className="modal-title" </h2>
id="id2" </div>
<div
className="modal-body"
>
<div>
<p
className="h6"
> >
We're sorry to see you go! Your account will be deleted shortly. 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.
</h2> </p>
</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>
<div
className="modal-footer"
>
<button
className="btn js-close-modal-on-click btn-secondary"
id="paragonCloseModalButton1"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>, </div>
] </div>
`; `;
exports[`SuccessModal should match default closed success modal snapshot 3`] = ` exports[`SuccessModal should match default closed success modal snapshot 2`] = `
Array [ <div>
<div <div
className="fade" className="fade"
role="presentation" role="presentation"
/>, />
<div <div
className="modal fade" className="modal js-close-modal-on-click fade"
role="presentation" onMouseDown={[Function]}
>
<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-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" role="presentation"
> >
<div <div
@@ -351,213 +78,234 @@ Array [
aria-modal={true} aria-modal={true}
className="" className=""
role="dialog" role="dialog"
tabIndex="-1"
> >
<div <div
data-focus-guard={true} className="modal-content"
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<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 <div
className="modal-content" className="modal-header"
> >
<div <h2
className="modal-header" className="modal-title"
id="id4"
> >
<h2 We're sorry to see you go! Your account will be deleted shortly.
className="modal-title" </h2>
id="id4" </div>
<div
className="modal-body"
>
<div>
<p
className="h6"
> >
We're sorry to see you go! Your account will be deleted shortly. 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.
</h2> </p>
</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>
<div
className="modal-footer"
>
<button
className="btn js-close-modal-on-click btn-secondary"
id="paragonCloseModalButton3"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>, </div>
] </div>
`; `;
exports[`SuccessModal should match open success modal snapshot 1`] = ` exports[`SuccessModal should match default closed success modal snapshot 3`] = `
Array [ <div>
<div <div
className="modal-backdrop show" className="fade"
role="presentation" role="presentation"
/>, />
<div <div
className="modal show d-block" className="modal js-close-modal-on-click fade"
onMouseDown={[Function]}
role="presentation" role="presentation"
> >
<div <div
aria-labelledby="id5" aria-labelledby="id6"
aria-modal={true}
className=""
role="dialog"
tabIndex="-1"
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id6"
>
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 js-close-modal-on-click btn-secondary"
id="paragonCloseModalButton5"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`SuccessModal should match default closed success modal snapshot 4`] = `
<div>
<div
className="fade"
role="presentation"
/>
<div
className="modal js-close-modal-on-click fade"
onMouseDown={[Function]}
role="presentation"
>
<div
aria-labelledby="id8"
aria-modal={true}
className=""
role="dialog"
tabIndex="-1"
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id8"
>
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 js-close-modal-on-click btn-secondary"
id="paragonCloseModalButton7"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`SuccessModal should match open success modal snapshot 1`] = `
<div>
<div
className="modal-backdrop show"
role="presentation"
/>
<div
className="modal js-close-modal-on-click show d-block"
onMouseDown={[Function]}
role="presentation"
>
<div
aria-labelledby="id10"
aria-modal={true} aria-modal={true}
className="modal-dialog" className="modal-dialog"
role="dialog" role="dialog"
tabIndex="-1"
> >
<div <div
data-focus-guard={true} className="modal-content"
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>
<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={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onTouchStart={[Function]}
onWheelCapture={[Function]}
> >
<div <div
className="modal-content" className="modal-header"
> >
<div <h2
className="modal-header" className="modal-title"
id="id10"
> >
<h2 We're sorry to see you go! Your account will be deleted shortly.
className="modal-title" </h2>
id="id5" </div>
<div
className="modal-body"
>
<div>
<p
className="h6"
> >
We're sorry to see you go! Your account will be deleted shortly. 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.
</h2> </p>
</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>
<div
className="modal-footer"
>
<button
className="btn js-close-modal-on-click btn-secondary"
id="paragonCloseModalButton9"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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={0}
/>
</div> </div>
</div>, </div>
] </div>
`; `;

View File

@@ -13,27 +13,22 @@ const messages = defineMessages({
}, },
'account.settings.delete.account.text.1': { 'account.settings.delete.account.text.1': {
id: 'account.settings.delete.account.text.1', id: 'account.settings.delete.account.text.1',
defaultMessage: 'Please note: Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.', defaultMessage: 'Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.',
description: 'A message in the user account deletion area', description: 'A message in the user account deletion area',
}, },
'account.settings.delete.account.text.2': { 'account.settings.delete.account.text.2': {
id: 'account.settings.delete.account.text.2', id: 'account.settings.delete.account.text.2',
defaultMessage: 'Once your account is deleted, you cannot use it to take courses on {siteName}.',
description: 'A message in the user account deletion area',
},
'account.settings.delete.account.text.2.edX': {
id: 'account.settings.delete.account.text.2.edX',
defaultMessage: 'Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.', defaultMessage: 'Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.',
description: 'A message in the user account deletion area', description: 'A message in the user account deletion area',
}, },
'account.settings.delete.account.text.3.link': { 'account.settings.delete.account.text.3.link': {
id: 'account.settings.delete.account.text.3.link', id: 'account.settings.delete.account.text.3.link',
defaultMessage: 'Follow these instructions for printing or downloading a certificate', defaultMessage: 'follow the instructions for printing or downloading a certificate',
description: 'This text is a link to a technical support page where users can learn how to print or download their certificates.', description: 'This text will be a link to a technical support page; it will go in the phrase If you want to make a copy of these for your records, ______ .',
}, },
'account.settings.delete.account.text.warning': { 'account.settings.delete.account.text.warning': {
id: 'account.settings.delete.account.text.warning', id: 'account.settings.delete.account.text.warning',
defaultMessage: 'Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on {siteName}.', defaultMessage: 'Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.',
description: 'A message in the user account deletion area', description: 'A message in the user account deletion area',
}, },
'account.settings.delete.account.text.change.instead': { 'account.settings.delete.account.text.change.instead': {
@@ -44,7 +39,7 @@ const messages = defineMessages({
'account.settings.delete.account.button': { 'account.settings.delete.account.button': {
id: 'account.settings.delete.account.button', id: 'account.settings.delete.account.button',
defaultMessage: 'Delete My Account', defaultMessage: 'Delete My Account',
description: 'Button label to permanently delete your platform account', description: 'Button label to permanently delete your edX account',
}, },
'account.settings.delete.account.please.activate': { 'account.settings.delete.account.please.activate': {
id: 'account.settings.delete.account.please.activate', id: 'account.settings.delete.account.please.activate',
@@ -63,16 +58,11 @@ const messages = defineMessages({
}, },
'account.settings.delete.account.modal.text.1': { 'account.settings.delete.account.modal.text.1': {
id: 'account.settings.delete.account.modal.text.1', id: 'account.settings.delete.account.modal.text.1',
defaultMessage: 'You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.', defaultMessage: 'You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.',
description: 'Messaging in the dialog asking user to confirm that they want to delete their entire account', description: 'Messaging in the dialog asking user to confirm that they want to delete their entire account',
}, },
'account.settings.delete.account.modal.text.2': { 'account.settings.delete.account.modal.text.2': {
id: 'account.settings.delete.account.modal.text.2', id: 'account.settings.delete.account.modal.text.2',
defaultMessage: 'If you proceed, you will be unable to use this account to take courses on {siteName}.',
description: 'Messaging in the dialog asking user to confirm that they want to delete their entire account',
},
'account.settings.delete.account.modal.text.2.edX': {
id: 'account.settings.delete.account.modal.text.2.edX',
defaultMessage: 'If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer\'s or university\'s system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.', defaultMessage: 'If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer\'s or university\'s system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.',
description: 'Messaging in the dialog asking user to confirm that they want to delete their entire account', description: 'Messaging in the dialog asking user to confirm that they want to delete their entire account',
}, },

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { CheckBox } from '@edx/paragon'; import { CheckBox } from '@edx/paragon';
import { DECLINED } from '../data/constants'; import { DECLINED } from '../data/constants';
const Checkboxes = (props) => { export const Checkboxes = (props) => {
const { const {
id, id,
options, options,
@@ -13,12 +13,12 @@ const Checkboxes = (props) => {
const [selected, setSelected] = useState(values); const [selected, setSelected] = useState(values);
useEffect(() => { useEffect(() => {
onChange(id, selected); onChange(id, selected)
}, [selected]); }, [selected])
const handleToggle = (value, option) => { const handleToggle = (value, option) => {
// If the user checked 'declined', uncheck all other options // If the user checked 'declined', uncheck all other options
if (value && option === DECLINED) { if (value && option == DECLINED) {
setSelected([DECLINED]); setSelected([DECLINED]);
return; return;
} }
@@ -33,47 +33,46 @@ const Checkboxes = (props) => {
if (!value) { if (!value) {
setSelected(selected.filter(i => i !== option)); setSelected(selected.filter(i => i !== option));
} }
}; }
const renderCheckboxes = () => options.map((option, index) => { const renderCheckboxes = () => {
const isFirst = index === 0; return options.map((option, index) => {
const isChecked = selected.includes(option.value); const isFirst = index == 0;
return ( const isChecked = selected.includes(option.value);
<div key={option.value} className="checkboxOption"> return (
<CheckBox <div key={index} className="checkboxOption">
type="checkbox" <CheckBox
id={option.value} type="checkbox"
name={option.value} id={option.value}
value={option.value} name={option.value}
checked={isChecked} value={option.value}
autoFocus={isFirst} checked={isChecked}
label={option.label} autoFocus={isFirst}
onChange={(value) => handleToggle(value, option.value)} label={option.label}
/> onChange={(value) => handleToggle(value, option.value)}
</div> />
); </div>
}); )
})
}
return ( return (
<div role="group"> <div role="group">
{renderCheckboxes()} {renderCheckboxes()}
</div> </div>
); )
}; }
Checkboxes.propTypes = { Checkboxes.propTypes = {
id: PropTypes.string.isRequired, id: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.shape({ options: PropTypes.array,
value: PropTypes.string, values: PropTypes.array,
label: PropTypes.string, onChange: PropTypes.func,
})),
values: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func.isRequired,
}; };
Checkboxes.defaultProps = { Checkboxes.defaultProps = {
options: [], options: [],
values: [], values: [],
}; }
export default Checkboxes; export default Checkboxes;

View File

@@ -1,134 +1,127 @@
import { getConfig } from '@edx/frontend-platform'; import {
DECLINED,
DEMOGRAPHICS_EDUCATION_LEVEL_OPTIONS,
DEMOGRAPHICS_ETHNICITY_OPTIONS,
DEMOGRAPHICS_GENDER_OPTIONS,
DEMOGRAPHICS_INCOME_OPTIONS,
DEMOGRAPHICS_MILITARY_HISTORY_OPTIONS,
DEMOGRAPHICS_WORK_SECTOR_OPTIONS,
DEMOGRAPHICS_WORK_STATUS_OPTIONS,
OTHER,
SELF_DESCRIBE,
} from '../data/constants';
import { import {
FormattedMessage, FormattedMessage,
injectIntl, injectIntl,
intlShape, intlShape,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { saveMultipleSettings, updateDraft } from '../data/actions';
import { Hyperlink, Input } from '@edx/paragon'; import Alert from '../Alert';
import Checkboxes from './Checkboxes';
import EditableField from '../EditableField';
import { Input } from '@edx/paragon';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { demographicsSectionSelector } from '../data/selectors';
import get from 'lodash.get'; import get from 'lodash.get';
import isEmpty from 'lodash.isempty'; import isEmpty from 'lodash.isempty';
import memoize from 'memoize-one'; 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'; import messages from './DemographicsSection.messages';
class DemographicsSection extends React.Component { class DemographicsSection extends React.Component {
// We check the `demographicsOptions` prop to see if it is empty before we attempt to extract and constructor(props, context) {
// format the available options for each question from the API response. super(props, context);
getApiOptions = memoize((demographicsOptions) => (this.hasRetrievedDemographicsOptions() && { this.alertRef = React.createRef();
demographicsGenderOptions: this.addDefaultOption('account.settings.field.demographics.gender.options.empty') }
.concat(demographicsOptions.actions.POST.gender.choices.map(key => ({
value: key.value, componentDidUpdate() {
label: key.display_name, if(!isEmpty(this.props.formErrors)) {
}))), this.alertRef.current.focus();
/* Ethnicity options don't need the blank/default option */ }
demographicsEthnicityOptions: demographicsOptions.actions.POST.user_ethnicity.child.children.ethnicity.choices.map( }
key => ({
value: key.value, getLocalizedOptions = memoize((locale) => ({
label: key.display_name, demographicsGenderOptions: DEMOGRAPHICS_GENDER_OPTIONS.map(key => ({
}), value: key,
), label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.gender.options.${key || 'empty'}`]),
demographicsIncomeOptions: this.addDefaultOption('account.settings.field.demographics.income.options.empty') })).concat(this.getDeclinedOption()),
.concat(demographicsOptions.actions.POST.income.choices.map(key => ({ demographicsEthnicityOptions: DEMOGRAPHICS_ETHNICITY_OPTIONS.map(key => ({
value: key.value, value: key,
label: key.display_name, label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.ethnicity.options.${key || 'empty'}`]),
}))), })).concat(this.getDeclinedOption()),
demographicsMilitaryHistoryOptions: this.addDefaultOption('account.settings.field.demographics.military_history.options.empty') demographicsIncomeOptions: DEMOGRAPHICS_INCOME_OPTIONS.map(key => ({
.concat(demographicsOptions.actions.POST.military_history.choices.map(key => ({ value: key,
value: key.value, label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.income.options.${key || 'empty'}`]),
label: key.display_name, })).concat(this.getDeclinedOption()),
}))), demographicsMilitaryHistoryOptions: DEMOGRAPHICS_MILITARY_HISTORY_OPTIONS.map(key => ({
demographicsEducationLevelOptions: this.addDefaultOption('account.settings.field.demographics.education_level.options.empty') value: key,
.concat(demographicsOptions.actions.POST.learner_education_level.choices.map(key => ({ label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.military_history.options.${key || 'empty'}`]),
value: key.value, })).concat(this.getDeclinedOption()),
label: key.display_name, demographicsEducationLevelOptions: DEMOGRAPHICS_EDUCATION_LEVEL_OPTIONS.map(key => ({
}))), value: key,
demographicsWorkStatusOptions: this.addDefaultOption('account.settings.field.demographics.work_status.options.empty') label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.education_level.options.${key || 'empty'}`]),
.concat(demographicsOptions.actions.POST.work_status.choices.map(key => ({ })).concat(this.getDeclinedOption()),
value: key.value, demographicsWorkStatusOptions: DEMOGRAPHICS_WORK_STATUS_OPTIONS.map(key => ({
label: key.display_name, value: key,
}))), label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.work_status.options.${key || 'empty'}`]),
demographicsWorkSectorOptions: this.addDefaultOption('account.settings.field.demographics.work_sector.options.empty') })).concat(this.getDeclinedOption()),
.concat(demographicsOptions.actions.POST.current_work_sector.choices.map(key => ({ demographicsWorkSectorOptions: DEMOGRAPHICS_WORK_SECTOR_OPTIONS.map(key => ({
value: key.value, value: key,
label: key.display_name, label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.work_sector.options.${key || 'empty'}`]),
}))), })).concat(this.getDeclinedOption()),
})); }));
ethnicityFieldDisplay = (demographicsEthnicityOptions) => { getDeclinedOption() {
let ethnicities = []; return [{
value: DECLINED,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.options.declined`])
}]
}
ethnicityFieldDisplay = () => {
if (get(this, 'props.formValues.demographics_user_ethnicity')) { if (get(this, 'props.formValues.demographics_user_ethnicity')) {
ethnicities = this.props.formValues.demographics_user_ethnicity; const ethnicities = this.props.formValues.demographics_user_ethnicity;
return ethnicities.map((e) => {
if (e == DECLINED) {
return this.props.intl.formatMessage(messages[`account.settings.field.demographics.options.declined`]);
}
return this.props.intl.formatMessage(messages[`account.settings.field.demographics.ethnicity.options.${e}`]);
}).join(", ")
} }
return ethnicities.map((e) => {
const matchingOption = demographicsEthnicityOptions.filter(option => option.value === e)[0];
return matchingOption && matchingOption.label;
}).join(', ');
} }
handleEditableFieldChange = (name, value) => { handleEditableFieldChange = (name, value) => {
this.props.updateDraft(name, value); this.props.updateDraft(name, value);
}; };
handleSubmit = (formId) => { handleSubmit = (formId, values) => {
// We have some custom fields in this section. Instead of relying on the // We have some custom fields in this section. Instead of relying on the
// submitted values, submit the values stored in 'drafts'. // submitted values, submit the values stored in 'drafts'.
const { drafts } = this.props; const drafts = this.props.drafts;
const settingsArray = Object.entries(drafts).map(([field, value]) => ({ const settingsArray = []
formId: field, for (let field in drafts) {
commitValues: value, settingsArray.push({
})); formId: field,
commitValues: drafts[field]
})
}
this.props.saveMultipleSettings(settingsArray, formId); 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 * 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 * display an Alert letting the user know that their info will not be retrieved or displayed
* cannot be updated. * and temporarily cannot be updated.
*/ */
renderDemographicsServiceIssueWarning() { renderDemographicsServiceIssueWarning() {
if (!isEmpty(this.props.formErrors.demographicsError) if (!isEmpty(this.props.formErrors)) {
|| !this.hasRetrievedDemographicsOptions()) {
return ( return (
<div <div
tabIndex="-1" tabIndex="-1"
ref={this.alertRef} ref={this.alertRef}>
>
<Alert className="alert alert-danger" role="alert"> <Alert className="alert alert-danger" role="alert">
<FormattedMessage <FormattedMessage
id="account.settings.message.demographics.service.issue" id="account.settings.message.demographics.service.issue"
@@ -138,8 +131,9 @@ class DemographicsSection extends React.Component {
</Alert> </Alert>
</div> </div>
); );
} else {
return null;
} }
return null;
} }
render() { render() {
@@ -156,162 +150,138 @@ class DemographicsSection extends React.Component {
demographicsEducationLevelOptions, demographicsEducationLevelOptions,
demographicsWorkStatusOptions, demographicsWorkStatusOptions,
demographicsWorkSectorOptions, demographicsWorkSectorOptions,
} = this.getApiOptions(this.props.formValues.demographicsOptions); } = this.getLocalizedOptions(this.context.locale);
const showSelfDescribe = this.props.formValues.demographics_gender === SELF_DESCRIBE; const showSelfDescribe = this.props.formValues.demographics_gender == SELF_DESCRIBE;
const showWorkStatusDescribe = this.props.formValues.demographics_work_status === OTHER; const showWorkStatusDescribe = this.props.formValues.demographics_work_status == OTHER;
return ( return (
<div className="account-section" id="demographics-information" ref={this.props.forwardRef}> <div className="account-section" id="demographics-information">
<h2 className="section-heading"> <h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.demographics.information'])} {this.props.intl.formatMessage(messages['account.settings.section.demographics.information'])}
</h2> </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()} {this.renderDemographicsServiceIssueWarning()}
{/*
If the demographicsOptions props are empty then there is no need to display the fields as <EditableField
the user will not have any choices available to select, nor will they be able to update name="demographics_gender"
their answers. type="select"
*/} value={this.props.formValues.demographics_gender}
{this.hasRetrievedDemographicsOptions() && ( userSuppliedValue={showSelfDescribe ? this.props.formValues.demographics_gender_description : null}
<div id="demographics-fields"> options={demographicsGenderOptions}
<EditableField label={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender'])}
name="demographics_gender" emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender.empty'])}
type="select" {...editableFieldProps}
value={this.props.formValues.demographics_gender} >
userSuppliedValue={showSelfDescribe ? this.props.formValues.demographics_gender_description : null} {showSelfDescribe &&
options={demographicsGenderOptions} <Input
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender'])} name='demographics_gender_description'
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender.empty'])} id='field-demographics_gender_description'
{...editableFieldProps} type='text'
> placeholder={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender_description.empty'])}
{showSelfDescribe && ( value={this.props.formValues.demographics_gender_description}
<Input onChange={(e) => this.handleEditableFieldChange(`demographics_gender_description`, e.target.value)}
name="demographics_gender_description" aria-label={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender_description'])}
id="field-demographics_gender_description" className="mt-1"
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" </EditableField>
type="select" <EditableField
value={this.props.formValues.demographics_military_history} name="demographics_user_ethnicity"
options={demographicsMilitaryHistoryOptions} type="select"
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.military_history'])} hidden
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.military_history.empty'])} value={this.ethnicityFieldDisplay()}
{...editableFieldProps} 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 }
name="demographics_learner_education_level" </EditableField>
type="select" <EditableField
value={this.props.formValues.demographics_learner_education_level} name="demographics_current_work_sector"
options={demographicsEducationLevelOptions} type="select"
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.learner_education_level'])} value={this.props.formValues.demographics_current_work_sector}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.learner_education_level.empty'])} options={demographicsWorkSectorOptions}
{...editableFieldProps} 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'])}
<EditableField {...editableFieldProps}
name="demographics_parent_education_level" />
type="select" <EditableField
value={this.props.formValues.demographics_parent_education_level} name="demographics_future_work_sector"
options={demographicsEducationLevelOptions} type="select"
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.parent_education_level'])} value={this.props.formValues.demographics_future_work_sector}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.parent_education_level.empty'])} options={demographicsWorkSectorOptions}
{...editableFieldProps} 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'])}
<EditableField {...editableFieldProps}
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> </div>
); )
} }
} };
DemographicsSection.propTypes = { DemographicsSection.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,
@@ -325,30 +295,11 @@ DemographicsSection.propTypes = {
demographics_work_status: PropTypes.string, demographics_work_status: PropTypes.string,
demographics_current_work_sector: PropTypes.string, demographics_current_work_sector: PropTypes.string,
demographics_future_work_sector: PropTypes.string, demographics_future_work_sector: PropTypes.string,
demographics_work_status_description: PropTypes.string,
demographics_gender_description: PropTypes.string,
demographicsOptions: PropTypes.object,
}).isRequired,
drafts: PropTypes.shape({
demographics_gender: PropTypes.string,
demographics_user_ethnicity: PropTypes.array,
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.object,
}).isRequired, }).isRequired,
formErrors: PropTypes.shape({ formErrors: PropTypes.shape({
demographicsError: PropTypes.string, demographicsError: PropTypes.string,
}).isRequired, }).isRequired,
forwardRef: PropTypes.func.isRequired, updateDraft: PropTypes.func.isRequired
updateDraft: PropTypes.func.isRequired,
saveMultipleSettings: PropTypes.func.isRequired,
}; };
export default connect(demographicsSectionSelector, { export default connect(demographicsSectionSelector, {

View File

@@ -1,13 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n'; import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({ const messages = defineMessages({
/* Demographics section heading */
'account.settings.section.demographics.information': { 'account.settings.section.demographics.information': {
id: 'account.settings.section.demographics.information', id: 'account.settings.section.demographics.information',
defaultMessage: 'Optional Information', defaultMessage: 'Optional Information',
description: 'The optional information section heading.', description: 'The optional information section heading.',
}, },
/* Gender identity */
'account.settings.field.demographics.gender': { 'account.settings.field.demographics.gender': {
id: 'account.settings.field.demographics.gender', id: 'account.settings.field.demographics.gender',
defaultMessage: 'Gender identity', defaultMessage: 'Gender identity',
@@ -23,6 +21,26 @@ const messages = defineMessages({
defaultMessage: 'Select a gender identity', defaultMessage: 'Select a gender identity',
description: 'Placeholder for the gender identity options dropdown.', description: 'Placeholder for the gender identity options dropdown.',
}, },
'account.settings.field.demographics.gender.options.woman': {
id: 'account.settings.field.demographics.gender.options.woman',
defaultMessage: 'Woman',
description: 'The label for the woman gender identity option.',
},
'account.settings.field.demographics.gender.options.man': {
id: 'account.settings.field.demographics.gender.options.man',
defaultMessage: 'Man',
description: 'The label for the man gender identity option.',
},
'account.settings.field.demographics.gender.options.non-binary': {
id: 'account.settings.field.demographics.gender.options.non-binary',
defaultMessage: 'Non-binary',
description: 'The label for the non-binary gender identity option.',
},
'account.settings.field.demographics.gender.options.self-describe': {
id: 'account.settings.field.demographics.gender.options.self-describe',
defaultMessage: 'Prefer to self-describe',
description: 'The label for self-describe gender identity option.',
},
'account.settings.field.demographics.gender_description': { 'account.settings.field.demographics.gender_description': {
id: 'account.settings.field.demographics.gender_description', id: 'account.settings.field.demographics.gender_description',
defaultMessage: 'Gender identity description', defaultMessage: 'Gender identity description',
@@ -33,7 +51,6 @@ const messages = defineMessages({
defaultMessage: 'Enter description', defaultMessage: 'Enter description',
description: 'Placeholder for empty account settings gender identity field.', description: 'Placeholder for empty account settings gender identity field.',
}, },
/* Ethnicity */
'account.settings.field.demographics.ethnicity': { 'account.settings.field.demographics.ethnicity': {
id: 'account.settings.field.demographics.ethnicity', id: 'account.settings.field.demographics.ethnicity',
defaultMessage: 'Race/Ethnicity identity', defaultMessage: 'Race/Ethnicity identity',
@@ -49,7 +66,46 @@ const messages = defineMessages({
defaultMessage: 'Select all that apply', // TODO: Is this the desired text? defaultMessage: 'Select all that apply', // TODO: Is this the desired text?
description: 'Placeholder for the ethnic background options field.', description: 'Placeholder for the ethnic background options field.',
}, },
/* Income */ 'account.settings.field.demographics.ethnicity.options.american-indian-or-alaska-native': {
id: 'account.settings.field.demographics.ethnicity.options.american-indian-or-alaska-native',
defaultMessage: 'American Indian or Alaska Native',
description: 'The label for the American Indian or Alaska Native ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.asian': {
id: 'account.settings.field.demographics.ethnicity.options.asian',
defaultMessage: 'Asian',
description: 'The label for the Asian ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.black-or-african-american': {
id: 'account.settings.field.demographics.ethnicity.options.black-or-african-american',
defaultMessage: 'Black or African American',
description: 'The label for the Black or African American ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish': {
id: 'account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish',
defaultMessage: 'Hispanic, Latin, or Spanish origin',
description: 'The label for the Hispanic, Latin, or Spanish origin ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african': {
id: 'account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african',
defaultMessage: 'Middle Eastern or North African',
description: 'The label for the Middle Eastern or North African ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander': {
id: 'account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander',
defaultMessage: 'Native Hawaiian or Other Pacific Islander',
description: 'The label for the Native Hawaiian or Other Pacific Islander ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.white': {
id: 'account.settings.field.demographics.ethnicity.options.white',
defaultMessage: 'White',
description: 'The label for the White ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.other': {
id: 'account.settings.field.demographics.ethnicity.options.other',
defaultMessage: 'Some other race, ethnicity, or origin',
description: 'The label for the Some other race, ethnicity, or origin ethnicity option.',
},
'account.settings.field.demographics.income': { 'account.settings.field.demographics.income': {
id: 'account.settings.field.demographics.income', id: 'account.settings.field.demographics.income',
defaultMessage: 'Family income', defaultMessage: 'Family income',
@@ -65,7 +121,36 @@ const messages = defineMessages({
defaultMessage: 'Select a family income range', defaultMessage: 'Select a family income range',
description: 'Placeholder for the household income dropdown.', description: 'Placeholder for the household income dropdown.',
}, },
/* Military history */ 'account.settings.field.demographics.income.options.less-than-10k': {
id: 'account.settings.field.demographics.income.options.less-than-10k',
defaultMessage: 'Less than US $10,000',
description: 'The label for the less than US $10,000 income option.',
},
'account.settings.field.demographics.income.options.10k-25k': {
id: 'account.settings.field.demographics.income.options.10k-25k',
defaultMessage: 'US $10,000 - $25,000',
description: 'The label for the US $10,000 - $25,000 income option.',
},
'account.settings.field.demographics.income.options.25k-50k': {
id: 'account.settings.field.demographics.income.options.25k-50k',
defaultMessage: 'US $25,000 - $50,000',
description: 'The label for the US $25,000 - $50,000 income option.',
},
'account.settings.field.demographics.income.options.50k-75k': {
id: 'account.settings.field.demographics.income.options.50k-75k',
defaultMessage: 'US $50,000 - $75,000',
description: 'The label for the US $50,000 - $75,000 income option.',
},
'account.settings.field.demographics.income.options.over-100k': {
id: 'account.settings.field.demographics.income.options.over-100k',
defaultMessage: 'Over US $100,000',
description: 'The label for the over US $100,000 income option.',
},
'account.settings.field.demographics.income.options.unsure': {
id: 'account.settings.field.demographics.income.options.unsure',
defaultMessage: 'I don\'t know',
description: 'The label for the I don\'t know income option.',
},
'account.settings.field.demographics.military_history': { 'account.settings.field.demographics.military_history': {
id: 'account.settings.field.demographics.military_history', id: 'account.settings.field.demographics.military_history',
defaultMessage: 'U.S. Military status', defaultMessage: 'U.S. Military status',
@@ -81,7 +166,26 @@ const messages = defineMessages({
defaultMessage: 'Select military status', defaultMessage: 'Select military status',
description: 'Placeholder for the military history dropdown.', description: 'Placeholder for the military history dropdown.',
}, },
/* Learner and family education level */ 'account.settings.field.demographics.military_history.options.never-served': {
id: 'account.settings.field.demographics.income.options.never-served',
defaultMessage: 'Never served in the military',
description: 'The label for the never served in the military military history option.',
},
'account.settings.field.demographics.military_history.options.training': {
id: 'account.settings.field.demographics.income.options.training',
defaultMessage: 'Only on active duty for training',
description: 'The label for the only on active duty for training military history option.',
},
'account.settings.field.demographics.military_history.options.active': {
id: 'account.settings.field.demographics.income.options.active',
defaultMessage: 'Now on active duty',
description: 'The label for the now on active duty military history option.',
},
'account.settings.field.demographics.military_history.options.previously-active': {
id: 'account.settings.field.demographics.income.options.previously-active',
defaultMessage: 'On active duty in the past, but not now',
description: 'The label for the on active duty in the past, but not now military history option.',
},
'account.settings.field.demographics.learner_education_level': { 'account.settings.field.demographics.learner_education_level': {
id: 'account.settings.field.demographics.learner_education_level', id: 'account.settings.field.demographics.learner_education_level',
defaultMessage: 'Your education level', defaultMessage: 'Your education level',
@@ -107,7 +211,56 @@ const messages = defineMessages({
defaultMessage: 'Select education level', defaultMessage: 'Select education level',
description: 'Placeholder for the education level options dropdown.', description: 'Placeholder for the education level options dropdown.',
}, },
/* Work status */ 'account.settings.field.demographics.education_level.options.no-high-school': {
id: 'account.settings.field.demographics.education_level.options.no-high-school',
defaultMessage: 'No High School',
description: 'The label for the no high school education level option.',
},
'account.settings.field.demographics.education_level.options.some-high-school': {
id: 'account.settings.field.demographics.education_level.options.some-high-school',
defaultMessage: 'Some High School',
description: 'The label for the some high school education level option.',
},
'account.settings.field.demographics.education_level.options.high-school-ged-equivalent': {
id: 'account.settings.field.demographics.education_level.options.high-school-ged-equivalent',
defaultMessage: 'High School diploma, GED, or equivalent',
description: 'The label for the high school diploma, GED, or equivalent education level option.',
},
'account.settings.field.demographics.education_level.options.some-college': {
id: 'account.settings.field.demographics.education_level.options.some-college',
defaultMessage: 'Some college, but no degree',
description: 'The label for the some college, but no degree education level option.',
},
'account.settings.field.demographics.education_level.options.some-college': {
id: 'account.settings.field.demographics.education_level.options.some-college',
defaultMessage: 'Some college, but no degree',
description: 'The label for the some college, but no degree education level option.',
},
'account.settings.field.demographics.education_level.options.associates': {
id: 'account.settings.field.demographics.education_level.options.associates',
defaultMessage: 'Associates degree',
description: 'The label for the Associates degree education level option.',
},
'account.settings.field.demographics.education_level.options.bachelors': {
id: 'account.settings.field.demographics.education_level.options.bachelors',
defaultMessage: 'Bachelors degree',
description: 'The label for the Bachelors degree education level option.',
},
'account.settings.field.demographics.education_level.options.masters': {
id: 'account.settings.field.demographics.education_level.options.masters',
defaultMessage: 'Masters degree',
description: 'The label for the Masters degree education level option.',
},
'account.settings.field.demographics.education_level.options.professional': {
id: 'account.settings.field.demographics.education_level.options.professional',
defaultMessage: 'Professional degree',
description: 'The label for the Professional degree education level option.',
},
'account.settings.field.demographics.education_level.options.doctorate': {
id: 'account.settings.field.demographics.education_level.options.doctorate',
defaultMessage: 'Doctorate degree',
description: 'The label for the Doctorate degree education level option.',
},
'account.settings.field.demographics.work_status': { 'account.settings.field.demographics.work_status': {
id: 'account.settings.field.demographics.work_status', id: 'account.settings.field.demographics.work_status',
defaultMessage: 'Employment status', defaultMessage: 'Employment status',
@@ -123,6 +276,41 @@ const messages = defineMessages({
defaultMessage: 'Select employment status', defaultMessage: 'Select employment status',
description: 'Placeholder for the work status options dropdown.', description: 'Placeholder for the work status options dropdown.',
}, },
'account.settings.field.demographics.work_status.options.full-time': {
id: 'account.settings.field.demographics.work_status.options.full-time',
defaultMessage: 'Employed, working full-time',
description: 'The label for the employed, working full-time work status option.',
},
'account.settings.field.demographics.work_status.options.part-time': {
id: 'account.settings.field.demographics.work_status.options.part-time',
defaultMessage: 'Employed, working part-time',
description: 'The label for the employed, working part-time work status option.',
},
'account.settings.field.demographics.work_status.options.not-employed-looking': {
id: 'account.settings.field.demographics.work_status.options.not-employed-looking',
defaultMessage: 'Not employed, looking for work',
description: 'The label for the not employed, looking for work work status option.',
},
'account.settings.field.demographics.work_status.options.not-employed-not-looking': {
id: 'account.settings.field.demographics.work_status.options.not-employed-not-looking',
defaultMessage: 'Not employed, not looking for work',
description: 'The label for the not employed, not looking for work work status option.',
},
'account.settings.field.demographics.work_status.options.unable': {
id: 'account.settings.field.demographics.work_status.options.unable',
defaultMessage: 'Unable to work',
description: 'The label for the unable to work work status option.',
},
'account.settings.field.demographics.work_status.options.retired': {
id: 'account.settings.field.demographics.work_status.options.retired',
defaultMessage: 'Retired',
description: 'The label for the retired work status option.',
},
'account.settings.field.demographics.work_status.options.other': {
id: 'account.settings.field.demographics.work_status.options.other',
defaultMessage: 'Other',
description: 'The label for the other work status option.',
},
'account.settings.field.demographics.work_status_description': { 'account.settings.field.demographics.work_status_description': {
id: 'account.settings.field.demographics.work_status_description', id: 'account.settings.field.demographics.work_status_description',
defaultMessage: 'Employment status description', defaultMessage: 'Employment status description',
@@ -133,7 +321,6 @@ const messages = defineMessages({
defaultMessage: 'Enter description', defaultMessage: 'Enter description',
description: 'Placeholder for empty account settings work status description field.', description: 'Placeholder for empty account settings work status description field.',
}, },
/* Work sector */
'account.settings.field.demographics.current_work_sector': { 'account.settings.field.demographics.current_work_sector': {
id: 'account.settings.field.demographics.current_work_sector', id: 'account.settings.field.demographics.current_work_sector',
defaultMessage: 'Current work industry', defaultMessage: 'Current work industry',
@@ -159,11 +346,110 @@ const messages = defineMessages({
defaultMessage: 'Select work industry', defaultMessage: 'Select work industry',
description: 'Placeholder for the work sector options dropdown.', description: 'Placeholder for the work sector options dropdown.',
}, },
/* Legal copy link text */ 'account.settings.field.demographics.work_sector.options.accommodation-food': {
'account.settings.section.demographics.why': { id: 'account.settings.field.demographics.work_sector.options.accommodation-food',
id: 'account.settings.section.demographics.why', defaultMessage: 'Accommodation and Food Services',
defaultMessage: 'Why does {siteName} collect this information?', description: 'The label for the Accommodation and Food Services work sector option.',
description: 'Link text for a link to external legal text', },
'account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation': {
id: 'account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation',
defaultMessage: 'Administrative and Support and Waste Management and Remediation Services',
description: 'The label for the Administrative and Support and Waste Management and Remediation Services work sector option.',
},
'account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting': {
id: 'account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting',
defaultMessage: 'Agriculture, Forestry, Fishing and Hunting',
description: 'The label for the Agriculture, Forestry, Fishing and Hunting work sector option.',
},
'account.settings.field.demographics.work_sector.options.arts-entertainment-recreation': {
id: 'account.settings.field.demographics.work_sector.options.arts-entertainment-recreation',
defaultMessage: 'Arts, Entertainment, and Recreation',
description: 'The label for the Arts, Entertainment, and Recreation work sector option.',
},
'account.settings.field.demographics.work_sector.options.construction': {
id: 'account.settings.field.demographics.work_sector.options.construction',
defaultMessage: 'Construction',
description: 'The label for the Construction work sector option.',
},
'account.settings.field.demographics.work_sector.options.educational': {
id: 'account.settings.field.demographics.work_sector.options.educational',
defaultMessage: 'Education Services',
description: 'The label for the Education Services work sector option.',
},
'account.settings.field.demographics.work_sector.options.finance-insurance': {
id: 'account.settings.field.demographics.work_sector.options.finance-insurance',
defaultMessage: 'Finance and Insurance',
description: 'The label for the Finance and Insurance work sector option.',
},
'account.settings.field.demographics.work_sector.options.healthcare-social': {
id: 'account.settings.field.demographics.work_sector.options.healthcare-social',
defaultMessage: 'Health Care and Social Assistance',
description: 'The label for the Health Care and Social Assistance work sector option.',
},
'account.settings.field.demographics.work_sector.options.information': {
id: 'account.settings.field.demographics.work_sector.options.information',
defaultMessage: 'Information',
description: 'The label for the Information work sector option.',
},
'account.settings.field.demographics.work_sector.options.management': {
id: 'account.settings.field.demographics.work_sector.options.management',
defaultMessage: 'Management of Companies and Enterprises',
description: 'The label for the Management of Companies and Enterprises work sector option.',
},
'account.settings.field.demographics.work_sector.options.manufacturing': {
id: 'account.settings.field.demographics.work_sector.options.manufacturing',
defaultMessage: 'Manufacturing',
description: 'The label for the Manufacturing work sector option.',
},
'account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas': {
id: 'account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas',
defaultMessage: 'Mining, Quarrying, and Oil and Gas Extraction',
description: 'The label for the Mining, Quarrying, and Oil and Gas Extraction work sector option.',
},
'account.settings.field.demographics.work_sector.options.professional-scientific-technical': {
id: 'account.settings.field.demographics.work_sector.options.professional-scientific-technical',
defaultMessage: 'Professional, Scientific, and Technical Services',
description: 'The label for the Professional, Scientific, and Technical Services work sector option.',
},
'account.settings.field.demographics.work_sector.options.public-admin': {
id: 'account.settings.field.demographics.work_sector.options.public-admin',
defaultMessage: 'Public Administration',
description: 'The label for the Public Administration work sector option.',
},
'account.settings.field.demographics.work_sector.options.real-estate': {
id: 'account.settings.field.demographics.work_sector.options.real-estate',
defaultMessage: 'Real Estate and Rental and Leasing',
description: 'The label for the Real Estate and Rental and Leasing work sector option.',
},
'account.settings.field.demographics.work_sector.options.retail': {
id: 'account.settings.field.demographics.work_sector.options.retail',
defaultMessage: 'Retail Trade',
description: 'The label for the Retail Trade work sector option.',
},
'account.settings.field.demographics.work_sector.options.transport-warehousing': {
id: 'account.settings.field.demographics.work_sector.options.transport-warehousing',
defaultMessage: 'Transportation and Warehousing',
description: 'The label for the Transportation and Warehousing work sector option.',
},
'account.settings.field.demographics.work_sector.options.utilities': {
id: 'account.settings.field.demographics.work_sector.options.utilities',
defaultMessage: 'Utilities',
description: 'The label for the Utilities work sector option.',
},
'account.settings.field.demographics.work_sector.options.trade': {
id: 'account.settings.field.demographics.work_sector.options.trade',
defaultMessage: 'Wholesale Trade',
description: 'The label for the Wholesale Trade work sector option.',
},
'account.settings.field.demographics.work_sector.options.other': {
id: 'account.settings.field.demographics.work_sector.options.other',
defaultMessage: 'Other',
description: 'The label for the Other work sector option.',
},
'account.settings.field.demographics.options.declined': {
id: 'account.settings.field.demographics.options.declined',
defaultMessage: 'Prefer not to respond',
description: 'The label for the declined option.',
}, },
}); });

View File

@@ -1,39 +1,21 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import get from 'lodash.get';
import { convertData, TO, FROM } from './utils'; import { convertData, TO, FROM } from './utils';
/** /**
* Utility method that attempts to extract errors from the response of a PATCH request in order to * If there was an error making the PATCH call then we create an apiError object containing a
* display a warning or otherwise meaningful message to the user. * 'demographicsError' fieldError. The content of the error itself isn't particularly important.
* This will trigger the `renderDemographicsServiceIssueWarningMessage()` (in
* DemographicsSection.jsx) to display an Alert to let the end-user know that there may be an
* issue communicating with the Demographics service.
* *
* @param {Error} error * @param {Error} error
*/ */
export function createDemographicsError(error) { export function createDemographicsError(error) {
const apiError = Object.create(error); const apiError = Object.create(error);
// If the error received has the `httpResponseData` field in it, then we should have reason to apiError.fieldErrors = {
// believe the Demographics service is alive and responding. Extract errors from fields where demographicsError: error.customAttributes.httpErrorType,
// 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; return apiError;
} }
@@ -43,13 +25,12 @@ export function createDemographicsError(error) {
* @param {Object} commitValues { demographics } * @param {Object} commitValues { demographics }
*/ */
export async function postDemographics(userId) { export async function postDemographics(userId) {
const requestConfig = { headers: { 'Content-Type': 'application/json' } };
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/`; const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/`;
const commitValues = { user: userId }; const commitValues = { user: userId };
let data = {}; let data = {};
({ data } = await getAuthenticatedHttpClient() ({ data } = await getAuthenticatedHttpClient()
.post(requestUrl, commitValues, requestConfig) .post(requestUrl, commitValues)
.catch((error) => { .catch((error) => {
const apiError = createDemographicsError(error); const apiError = createDemographicsError(error);
throw apiError; throw apiError;
@@ -119,22 +100,3 @@ export async function patchDemographics(userId, commitValues) {
return convertData(data, FROM); 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

@@ -1,18 +1,5 @@
export const TO = 'to'; export const TO = 'to';
export const FROM = 'from'; 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): // Frontend wants (example):
// demographics_user_ethnicity: ["asian", "white", "other"] // demographics_user_ethnicity: ["asian", "white", "other"]

View File

@@ -1,583 +1,128 @@
import * as auth from '@edx/frontend-platform/auth'; import * as auth from '@edx/frontend-platform/auth';
import * as selectors from '../../data/selectors';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import DemographicsSection from '../DemographicsSection';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import React from 'react'; import React from 'react';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import DemographicsSection from '../DemographicsSection';
jest.mock('@edx/frontend-platform/auth'); jest.mock('@edx/frontend-platform/auth');
const IntlDemographicsSection = injectIntl(DemographicsSection); const IntlDemographicsSection = injectIntl(DemographicsSection);
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ demographicsSectionSelector: () => ({}) }))); jest.mock('../../data/selectors', () => {
return jest.fn().mockImplementation(() => ({ demographicsSectionSelector: () => ({}) }));
});
const mockStore = configureStore(); const mockStore = configureStore();
describe('DemographicsSection', () => { describe('DemographicsSection', () => {
let props = {}; let props = {};
let store = {}; let store = {};
const reduxWrapper = children => ( const reduxWrapper = children => (
<IntlProvider locale="en"> <IntlProvider locale="en">
<Provider store={store}>{children}</Provider> <Provider store={store}>{children}</Provider>
</IntlProvider> </IntlProvider>
); );
beforeEach(() => { beforeEach(() => {
store = mockStore(); store = mockStore();
props = { props = {
updateDraft: jest.fn(), updateDraft: jest.fn(),
formValues: { formValues: {
demographics_gender: 'declined', demographics_gender: 'declined',
demographics_gender_description: '', demographics_gender_description: '',
demographics_user_ethnicity: [], demographics_user_ethnicity: [],
demographics_income: 'declined', demographics_income: 'declined',
demographics_military_history: 'declined', demographics_military_history: 'declined',
demographics_learner_education_level: 'declined', demographics_learner_education_level: 'declined',
demographics_parent_education_level: 'declined', demographics_parent_education_level: 'declined',
demographics_work_status: 'declined', demographics_work_status: 'declined',
demographics_work_status_description: '', demographics_work_status_description: '',
demographics_current_work_sector: 'declined', demographics_current_work_sector: 'declined',
demographics_future_work_sector: 'declined', demographics_future_work_sector: 'declined',
demographics_user: 1, 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: {},
}, };
formErrors: {}, auth.getAuthenticatedHttpClient = jest.fn(() => ({
intl: {}, patch: async () => ({
forwardRef: () => {}, data: { status: 200 },
drafts: {}, catch: () => {},
}; }),
auth.getAuthenticatedHttpClient = jest.fn(() => ({ }));
patch: async () => ({ auth.getAuthenticatedUser = jest.fn(() => ({ userId: 1 }));
data: { status: 200 }, });
catch: () => {},
}),
}));
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 1 }));
});
it('should render', () => { it('should render', () => {
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON(); const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it('should render an Alert if an error occurs', () => { it('should render an Alert if an error occurs', () => {
props = { props = {
...props, ...props,
formErrors: { formErrors: {
demographicsError: 'api-error', demographicsError: "api-error"
}, }
}; };
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON(); const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it('should set user input correctly when user provides gender self-description', () => {
props = {
...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 gender self-description', () => { it('should set user input correctly when user provides answers to work_status question', () => {
props = { props = {
...props, ...props,
formValues: { formValues: {
...props.formValues, demographics_work_status: 'other',
demographics_gender: 'self-describe', demographics_work_status_description: 'test',
demographics_gender_description: 'test', }
}, }
};
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON(); const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it('should set user input correctly when user provides answers to work_status question', () => { it('should render ethnicity text correctly', () => {
props = { props = {
...props, ...props,
formValues: { formValues: {
...props.formValues, demographics_user_ethnicity: ['asian']
demographics_work_status: 'other', }
demographics_work_status_description: 'test', }
},
}; const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON(); it('should render ethnicity correctly when multiple options are selected', () => {
expect(wrapper).toMatchSnapshot(); props = {
}); ...props,
formValues: {
demographics_user_ethnicity: ['hispanic-latin-spanish', 'white']
}
}
it('should render ethnicity text correctly', () => { const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
props = { expect(wrapper).toMatchSnapshot();
...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

@@ -10,632 +10,569 @@ exports[`DemographicsSection should render 1`] = `
> >
Optional Information Optional Information
</h2> </h2>
<p>
<a
className="default-link standalone-link"
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
target="_blank"
>
Why does localhost collect this information?
<span
className="d-inline-block align-text-top"
>
<span
className="pgn__icon"
style={
Object {
"height": "1em",
"width": "1em",
}
}
>
<svg
aria-hidden={true}
aria-label=""
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>
</span>
</a>
</p>
<div <div
id="demographics-fields" className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
> >
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Gender identity
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Gender identity <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Race/Ethnicity identity
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Race/Ethnicity identity <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add race/ethnicity identity
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Family income
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Family income <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 U.S. Military status
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
U.S. Military status <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Your education level
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Your education level <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Parents/Guardians education level
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Parents/Guardians education level <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Employment status
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Employment status <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Current work industry
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Current work industry <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Future work industry
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Future work industry <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -652,48 +589,6 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
> >
Optional Information Optional Information
</h2> </h2>
<p>
<a
className="default-link standalone-link"
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
target="_blank"
>
Why does localhost collect this information?
<span
className="d-inline-block align-text-top"
>
<span
className="pgn__icon"
style={
Object {
"height": "1em",
"width": "1em",
}
}
>
<svg
aria-hidden={true}
aria-label=""
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>
</span>
</a>
</p>
<div <div
tabIndex="-1" tabIndex="-1"
> >
@@ -709,658 +604,568 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
</div> </div>
</div> </div>
<div <div
id="demographics-fields" className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
> >
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Gender identity
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Gender identity <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div> </div>
</div>
`;
exports[`DemographicsSection should render an Alert when demographicsOptions props are empty 1`] = `
<div
className="account-section"
id="demographics-information"
>
<h2
className="section-heading"
>
Optional Information
</h2>
<p>
<a
className="default-link standalone-link"
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
target="_blank"
>
Why does localhost collect this information?
<span
className="d-inline-block align-text-top"
>
<span
className="pgn__icon"
style={
Object {
"height": "1em",
"width": "1em",
}
}
>
<svg
aria-hidden={true}
aria-label=""
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>
</span>
</a>
</p>
<div <div
tabIndex="-1" className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
> >
<div <div
className="alert d-flex align-items-start alert alert-danger" style={
Object {
"padding": ".1px 0",
}
}
> >
<div /> <div
<div> className="form-group"
<span> >
An error occurred attempting to retrieve or save your account information. Please try again later. <div
</span> className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -1377,625 +1182,625 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
> >
Optional Information Optional Information
</h2> </h2>
<p>
<a
className="default-link standalone-link"
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
target="_blank"
>
Why does localhost collect this information?
<span
className="d-inline-block align-text-top"
>
<span
className="pgn__icon"
style={
Object {
"height": "1em",
"width": "1em",
}
}
>
<svg
aria-hidden={true}
aria-label=""
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>
</span>
</a>
</p>
<div <div
id="demographics-fields" className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
> >
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Gender identity
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Gender identity <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add gender identity
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Race/Ethnicity identity
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Race/Ethnicity identity <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
Hispanic, Latin, or Spanish origin, White
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Family income
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Family income <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add family income
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 U.S. Military status
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
U.S. Military status <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add military status
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Your education level
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Your education level <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Parents/Guardians education level
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Parents/Guardians education level <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Employment status
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Employment status <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add employment status
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Current work industry
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Current work industry <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Future work industry
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Future work industry <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -2012,625 +1817,625 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
> >
Optional Information Optional Information
</h2> </h2>
<p>
<a
className="default-link standalone-link"
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
target="_blank"
>
Why does localhost collect this information?
<span
className="d-inline-block align-text-top"
>
<span
className="pgn__icon"
style={
Object {
"height": "1em",
"width": "1em",
}
}
>
<svg
aria-hidden={true}
aria-label=""
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>
</span>
</a>
</p>
<div <div
id="demographics-fields" className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
> >
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Gender identity
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Gender identity <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add gender identity
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Race/Ethnicity identity
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Race/Ethnicity identity <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
Asian
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Family income
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Family income <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add family income
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 U.S. Military status
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
U.S. Military status <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add military status
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Your education level
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Your education level <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Parents/Guardians education level
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Parents/Guardians education level <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Employment status
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Employment status <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add employment status
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Current work industry
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Current work industry <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Future work industry
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Future work industry <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -2647,632 +2452,625 @@ exports[`DemographicsSection should set user input correctly when user provides
> >
Optional Information Optional Information
</h2> </h2>
<p>
<a
className="default-link standalone-link"
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
target="_blank"
>
Why does localhost collect this information?
<span
className="d-inline-block align-text-top"
>
<span
className="pgn__icon"
style={
Object {
"height": "1em",
"width": "1em",
}
}
>
<svg
aria-hidden={true}
aria-label=""
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>
</span>
</a>
</p>
<div <div
id="demographics-fields" className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
> >
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Gender identity
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Gender identity <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add gender identity
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Race/Ethnicity identity
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Race/Ethnicity identity <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add race/ethnicity identity
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Family income
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Family income <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add family income
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 U.S. Military status
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
U.S. Military status <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add military status
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Your education level
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Your education level <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Parents/Guardians education level
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Parents/Guardians education level <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Employment status
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Employment status <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
Other: test
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Current work industry
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Current work industry <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Future work industry
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Future work industry <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -3289,632 +3087,625 @@ exports[`DemographicsSection should set user input correctly when user provides
> >
Optional Information Optional Information
</h2> </h2>
<p>
<a
className="default-link standalone-link"
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
target="_blank"
>
Why does localhost collect this information?
<span
className="d-inline-block align-text-top"
>
<span
className="pgn__icon"
style={
Object {
"height": "1em",
"width": "1em",
}
}
>
<svg
aria-hidden={true}
aria-label=""
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>
</span>
</a>
</p>
<div <div
id="demographics-fields" className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
> >
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Gender identity
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Gender identity <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
Prefer to self-describe: test
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Race/Ethnicity identity
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Race/Ethnicity identity <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add race/ethnicity identity
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Family income
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Family income <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add family income
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 U.S. Military status
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
U.S. Military status <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add military status
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Your education level
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Your education level <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Parents/Guardians education level
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Parents/Guardians education level <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Employment status
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Employment status <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add employment status
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Current work industry
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Current work industry <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div <div
className="pgn-transition-replace-group position-relative"
style={ style={
Object { Object {
"height": null, "padding": ".1px 0",
} }
} }
> >
<div <div
style={ className="form-group"
Object {
"padding": ".1px 0",
}
}
> >
<div <div
className="form-group" className="d-flex align-items-start"
> >
<div <h6
className="d-flex align-items-start" aria-level="3"
> >
<h6 Future work industry
aria-level="3" </h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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"
> >
Future work industry <path
</h6> 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"
<button fill="currentColor"
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 {}} style={Object {}}
viewBox="0 0 512 512" />
xmlns="http://www.w3.org/2000/svg" </svg>
> Edit
<path </button>
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>
<p>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,203 +0,0 @@
import React, { useEffect, useState } from 'react';
import { connect, useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Alert,
Button,
Col,
Form,
ModalDialog,
StatefulButton,
} from '@edx/paragon';
import { closeForm, saveSettingsReset } from '../data/actions';
import { nameChangeSelector } from '../data/selectors';
import { requestNameChange, requestNameChangeFailure, requestNameChangeReset } from './data/actions';
import messages from './messages';
function NameChangeModal({
targetFormId,
errors,
formValues,
intl,
saveState,
}) {
const dispatch = useDispatch();
const { push } = useHistory();
const { username } = getAuthenticatedUser();
const [verifiedNameInput, setVerifiedNameInput] = useState(formValues.verified_name || '');
const [confirmedWarning, setConfirmedWarning] = useState(false);
function resetLocalState() {
setConfirmedWarning(false);
dispatch(requestNameChangeReset());
}
function handleChange(e) {
setVerifiedNameInput(e.target.value);
}
function handleClose() {
resetLocalState();
dispatch(closeForm(targetFormId));
dispatch(saveSettingsReset());
}
function handleSubmit(e) {
e.preventDefault();
if (saveState === 'pending') {
return;
}
if (!verifiedNameInput) {
dispatch(requestNameChangeFailure({
verified_name: intl.formatMessage(messages['account.settings.name.change.error.valid.name']),
}));
} else {
const draftProfileName = targetFormId === 'name' ? formValues.name : null;
dispatch(requestNameChange(username, draftProfileName, verifiedNameInput));
}
}
useEffect(() => {
if (saveState === 'complete') {
handleClose();
push('/id-verification');
}
}, [saveState]);
function renderErrors() {
if (Object.keys(errors).length > 0) {
return (
<>
{Object.entries(errors).map(([key, value]) => (
<Form.Control.Feedback type="invalid" key={key}>
{
key === 'general_error'
? intl.formatMessage(messages['account.settings.name.change.error.general'])
: value
}
</Form.Control.Feedback>
))}
</>
);
}
return null;
}
function renderTitle() {
if (!confirmedWarning) {
return intl.formatMessage(messages['account.settings.name.change.title.id']);
}
return intl.formatMessage(messages['account.settings.name.change.title.begin']);
}
function renderBody() {
if (!confirmedWarning) {
return (
<Alert variant="warning">
<p>
{intl.formatMessage(messages['account.settings.name.change.warning.one'])}
</p>
<p>
{intl.formatMessage(messages['account.settings.name.change.warning.two'])}
</p>
</Alert>
);
}
return (
<Form.Group as={Col} isInvalid={Object.keys(errors).length > 0}>
<Form.Label>
{intl.formatMessage(messages['account.settings.name.change.id.name.label'])}
</Form.Label>
<Form.Control
type="text"
name="verifiedName"
placeholder={intl.formatMessage(messages['account.settings.name.change.id.name.placeholder'])}
value={verifiedNameInput}
onChange={handleChange}
/>
{renderErrors()}
</Form.Group>
);
}
function renderContinueButton() {
if (!confirmedWarning) {
return (
<Button variant="primary" onClick={() => setConfirmedWarning(true)}>
{intl.formatMessage(messages['account.settings.name.change.continue'])}
</Button>
);
}
return (
<StatefulButton
type="submit"
state={saveState}
labels={{
default: intl.formatMessage(messages['account.settings.name.change.continue']),
}}
disabledStates={[]}
/>
);
}
return (
<ModalDialog
title={renderTitle()}
isOpen
hasCloseButton={false}
onClose={handleClose}
>
<Form onSubmit={handleSubmit}>
<ModalDialog.Header>
<ModalDialog.Title>
{renderTitle()}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body className="mb-3 overflow-hidden">
{renderBody()}
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
{intl.formatMessage(messages['account.settings.name.change.cancel'])}
</ModalDialog.CloseButton>
{renderContinueButton()}
</ActionRow>
</ModalDialog.Footer>
</Form>
</ModalDialog>
);
}
NameChangeModal.propTypes = {
targetFormId: PropTypes.string.isRequired,
errors: PropTypes.shape({}).isRequired,
formValues: PropTypes.shape({
name: PropTypes.string,
verified_name: PropTypes.string,
}).isRequired,
saveState: PropTypes.string,
intl: intlShape.isRequired,
};
NameChangeModal.defaultProps = {
saveState: null,
};
export default connect(nameChangeSelector)(injectIntl(NameChangeModal));

View File

@@ -1,25 +0,0 @@
import { AsyncActionType } from '../../data/utils';
export const REQUEST_NAME_CHANGE = new AsyncActionType('ACCOUNT_SETTINGS', 'REQUEST_NAME_CHANGE');
export const requestNameChange = (username, profileName, verifiedName) => ({
type: REQUEST_NAME_CHANGE.BASE,
payload: { username, profileName, verifiedName },
});
export const requestNameChangeBegin = () => ({
type: REQUEST_NAME_CHANGE.BEGIN,
});
export const requestNameChangeSuccess = () => ({
type: REQUEST_NAME_CHANGE.SUCCESS,
});
export const requestNameChangeFailure = errors => ({
type: REQUEST_NAME_CHANGE.FAILURE,
payload: { errors },
});
export const requestNameChangeReset = () => ({
type: REQUEST_NAME_CHANGE.RESET,
});

View File

@@ -1,44 +0,0 @@
import { REQUEST_NAME_CHANGE } from './actions';
export const defaultState = {
saveState: null,
errors: {},
};
const reducer = (state = defaultState, action = null) => {
if (action !== null) {
switch (action.type) {
case REQUEST_NAME_CHANGE.BEGIN:
return {
...state,
saveState: 'pending',
errors: {},
};
case REQUEST_NAME_CHANGE.SUCCESS:
return {
...state,
saveState: 'complete',
};
case REQUEST_NAME_CHANGE.FAILURE:
return {
...state,
saveState: 'error',
errors: action.payload.errors || { general_error: 'A technical error occurred. Please try again.' },
};
case REQUEST_NAME_CHANGE.RESET:
return {
...state,
saveState: null,
errors: {},
};
default:
}
}
return state;
};
export default reducer;

View File

@@ -1,40 +0,0 @@
import { put, call, takeEvery } from 'redux-saga/effects';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { postVerifiedName } from '../../data/service';
import {
REQUEST_NAME_CHANGE,
requestNameChangeBegin,
requestNameChangeSuccess,
requestNameChangeFailure,
} from './actions';
import { postNameChange } from './service';
export function* handleRequestNameChange(action) {
let { name: profileName } = getAuthenticatedUser();
try {
yield put(requestNameChangeBegin());
if (action.payload.profileName) {
yield call(postNameChange, action.payload.profileName);
profileName = action.payload.profileName;
}
yield call(postVerifiedName, {
username: action.payload.username,
verified_name: action.payload.verifiedName,
profile_name: profileName,
});
yield put(requestNameChangeSuccess());
} catch (err) {
if (err.customAttributes?.httpErrorResponseData) {
yield put(requestNameChangeFailure(JSON.parse(err.customAttributes.httpErrorResponseData)));
} else {
yield put(requestNameChangeFailure());
}
}
}
export default function* saga() {
yield takeEvery(REQUEST_NAME_CHANGE.BASE, handleRequestNameChange);
}

View File

@@ -1,17 +0,0 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { handleRequestError } from '../../data/utils';
// eslint-disable-next-line import/prefer-default-export
export async function postNameChange(name) {
// Requests a pending name change, rather than saving the account name immediately
const requestConfig = { headers: { Accept: 'application/json' } };
const requestUrl = `${getConfig().LMS_BASE_URL}/api/user/v1/accounts/name_change/`;
const { data } = await getAuthenticatedHttpClient()
.post(requestUrl, { name }, requestConfig)
.catch(error => handleRequestError(error));
return data;
}

View File

@@ -1,4 +0,0 @@
export { default } from './NameChange';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';
export { REQUEST_NAME_CHANGE } from './data/actions';

View File

@@ -1,56 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'account.settings.name.change.title.id': {
id: 'account.settings.name.change.title.id',
defaultMessage: 'This name change requires identity verification',
description: 'Inform the user that changing their name requires identity verification',
},
'account.settings.name.change.title.begin': {
id: 'account.settings.name.change.title.begin',
defaultMessage: 'Before we begin',
description: 'Title before beginning the ID verification process',
},
'account.settings.name.change.warning.one': {
id: 'account.settings.name.change.warning.one',
defaultMessage: 'Warning: This action updates the name that appears on all certificates that have been earned on this account in the past and any certificates you are currently earning or will earn in the future.',
description: 'Warning informing the user that a name change will update the name on all of their certificates.',
},
'account.settings.name.change.warning.two': {
id: 'account.settings.name.change.warning.two',
defaultMessage: 'This action cannot be undone without verifying your identity.',
description: 'Warning informing the user that a name change cannot be undone without ID verification.',
},
'account.settings.name.change.id.name.label': {
id: 'account.settings.name.change.id.name.label',
defaultMessage: 'Enter your name as it appears on your government-issued ID.',
description: 'Form label instructing the user to enter the name on their ID.',
},
'account.settings.name.change.id.name.placeholder': {
id: 'account.settings.name.change.id.name.placeholder',
defaultMessage: 'Enter the name on your government ID',
description: 'Form label instructing the user to enter the name on their ID.',
},
'account.settings.name.change.error.valid.name': {
id: 'account.settings.name.change.error.valid.name',
defaultMessage: 'Please enter a valid name.',
description: 'Error that appears when the user doesnt enter a valid name.',
},
'account.settings.name.change.error.general': {
id: 'account.settings.name.change.error.general',
defaultMessage: 'A technical error occurred. Please try again.',
description: 'Generic error message.',
},
'account.settings.name.change.continue': {
id: 'account.settings.name.change.continue',
defaultMessage: 'Continue',
description: 'Continue button.',
},
'account.settings.name.change.cancel': {
id: 'account.settings.name.change.cancel',
defaultMessage: 'Cancel',
description: 'Cancel button.',
},
});
export default messages;

View File

@@ -1,172 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
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';
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
ReactDOM.createPortal = node => node;
import NameChange from '../NameChange'; // eslint-disable-line import/first
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(() => ({ nameChangeSelector: () => ({}) })));
const history = createMemoryHistory();
const IntlNameChange = injectIntl(NameChange);
const mockStore = configureStore();
describe('NameChange', () => {
let props = {};
let store = {};
const reduxWrapper = children => (
<Router history={history}>
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
</Router>
);
beforeEach(() => {
store = mockStore();
props = {
targetFormId: 'test_form',
errors: {},
formValues: {
name: 'edx edx',
verified_name: 'edX Verified',
},
saveState: null,
intl: {},
};
auth.getAuthenticatedHttpClient = jest.fn(() => ({
patch: async () => ({
data: { status: 200 },
catch: () => {},
}),
}));
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'edx' }));
});
afterEach(() => jest.clearAllMocks());
it('renders populated input after clicking continue if verified_name in form data', async () => {
const getInput = () => screen.queryByPlaceholderText('Enter the name on your government ID');
render(reduxWrapper(<IntlNameChange {...props} />));
expect(getInput()).toBeNull();
const continueButton = screen.getByText('Continue');
fireEvent.click(continueButton);
expect(getInput().value).toBe('edX Verified');
});
it('renders empty input after clicking continue if verified_name not in form data', async () => {
const getInput = () => screen.queryByPlaceholderText('Enter the name on your government ID');
const formProps = {
...props,
formValues: {
name: 'edx edx',
},
};
render(reduxWrapper(<IntlNameChange {...formProps} />));
const continueButton = screen.getByText('Continue');
fireEvent.click(continueButton);
expect(getInput().value).toBe('');
});
it('dispatches verifiedName on submit if targetForm is not "name"', async () => {
const dispatchData = {
payload: {
profileName: null,
username: 'edx',
verifiedName: 'Verified Name',
},
type: 'ACCOUNT_SETTINGS__REQUEST_NAME_CHANGE',
};
render(reduxWrapper(<IntlNameChange {...props} />));
const continueButton = screen.getByText('Continue');
fireEvent.click(continueButton);
const input = screen.getByPlaceholderText('Enter the name on your government ID');
fireEvent.change(input, { target: { value: 'Verified Name' } });
const submitButton = screen.getByText('Continue');
fireEvent.click(submitButton);
expect(mockDispatch).toHaveBeenCalledWith(dispatchData);
});
it('dispatches both profileName and verifiedName on submit if the targetForm is "name"', async () => {
const dispatchData = {
payload: {
profileName: 'edx edx',
username: 'edx',
verifiedName: 'Verified Name',
},
type: 'ACCOUNT_SETTINGS__REQUEST_NAME_CHANGE',
};
const formProps = {
...props,
targetFormId: 'name',
};
render(reduxWrapper(<IntlNameChange {...formProps} />));
const continueButton = screen.getByText('Continue');
fireEvent.click(continueButton);
const input = screen.getByPlaceholderText('Enter the name on your government ID');
fireEvent.change(input, { target: { value: 'Verified Name' } });
const submitButton = screen.getByText('Continue');
fireEvent.click(submitButton);
expect(mockDispatch).toHaveBeenCalledWith(dispatchData);
});
it('does not dispatch action while pending', async () => {
props.saveState = 'pending';
render(reduxWrapper(<IntlNameChange {...props} />));
const continueButton = screen.getByText('Continue');
fireEvent.click(continueButton);
const input = screen.getByPlaceholderText('Enter the name on your government ID');
fireEvent.change(input, { target: { value: 'Verified Name' } });
const submitButton = screen.getByText('Continue');
fireEvent.click(submitButton);
expect(mockDispatch).not.toHaveBeenCalled();
});
it('routes to IDV when name change request is successful', async () => {
props.saveState = 'complete';
render(reduxWrapper(<IntlNameChange {...props} />));
expect(history.location.pathname).toEqual('/id-verification');
});
});

View File

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

View File

@@ -22,7 +22,7 @@ const ResetPassword = (props) => {
</h6> </h6>
<p> <p>
<StatefulButton <StatefulButton
variant="link" className="btn-link"
state={status} state={status}
onClick={(e) => { onClick={(e) => {
// Swallow clicks if the state is pending. // Swallow clicks if the state is pending.

View File

@@ -1,8 +1,6 @@
import { put, call, takeEvery } from 'redux-saga/effects'; import { put, call, takeEvery } from 'redux-saga/effects';
import { import { resetPasswordBegin, resetPasswordForbidden, resetPasswordSuccess, RESET_PASSWORD } from './actions';
resetPasswordBegin, resetPasswordForbidden, resetPasswordSuccess, RESET_PASSWORD,
} from './actions';
import { postResetPassword } from './service'; import { postResetPassword } from './service';
function* handleResetPassword(action) { function* handleResetPassword(action) {

View File

@@ -12,8 +12,9 @@ export const siteLanguageListSelector = createSelector(
export const siteLanguageOptionsSelector = createSelector( export const siteLanguageOptionsSelector = createSelector(
siteLanguageSelector, siteLanguageSelector,
siteLanguage => siteLanguage.siteLanguageList.map(({ code, name }) => ({ siteLanguage =>
value: code, siteLanguage.siteLanguageList.map(({ code, name }) => ({
label: name, value: code,
})), label: name,
})),
); );

View File

@@ -1,7 +1,7 @@
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { convertKeyNames, snakeCaseObject } from '@edx/frontend-platform/utils';
import siteLanguageList from './constants'; import siteLanguageList from './constants';
import { snakeCaseObject, convertKeyNames } from '../data/utils';
export async function getSiteLanguageList() { export async function getSiteLanguageList() {
return siteLanguageList; return siteLanguageList;

View File

@@ -1,62 +0,0 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { BrowserRouter as Router } from 'react-router-dom';
import { mergeConfig, setConfig } from '@edx/frontend-platform';
import JumpNav from '../JumpNav';
const IntlJumpNav = injectIntl(JumpNav);
describe('JumpNav', () => {
mergeConfig({
ENABLE_DEMOGRAPHICS_COLLECTION: false,
});
let props = {};
beforeEach(() => {
props = {
intl: {},
displayDemographicsLink: false,
};
});
it('should not render Optional Information link', () => {
const tree = renderer.create((
// 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} />
</IntlProvider>
</Router>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
it('should render Optional Information link', () => {
setConfig({
ENABLE_DEMOGRAPHICS_COLLECTION: true,
});
props = {
...props,
displayDemographicsLink: true,
};
const tree = renderer.create((
// Same as previous test
<Router>
<IntlProvider locale="en">
<IntlJumpNav {...props} />
</IntlProvider>
</Router>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -1,194 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`JumpNav should not render Optional Information link 1`] = `
<div
className="jump-nav"
>
<ul
className="list-unstyled"
style={Object {}}
>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#basic-information"
onClick={[Function]}
style={Object {}}
>
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 link 1`] = `
<div
className="jump-nav"
>
<ul
className="list-unstyled"
style={Object {}}
>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#basic-information"
onClick={[Function]}
style={Object {}}
>
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

@@ -11,16 +11,14 @@ class ThirdPartyAuth extends Component {
onClickDisconnect = (e) => { onClickDisconnect = (e) => {
e.preventDefault(); e.preventDefault();
const providerId = e.currentTarget.getAttribute('data-provider-id'); const providerId = e.currentTarget.getAttribute('data-provider-id');
if (this.props.disconnectionStatuses[providerId] === 'pending') { if (this.props.disconnectionStatuses[providerId] === 'pending') return;
return;
}
const disconnectUrl = e.currentTarget.getAttribute('data-disconnect-url'); const disconnectUrl = e.currentTarget.getAttribute('data-disconnect-url');
this.props.disconnectAuth(disconnectUrl, providerId); this.props.disconnectAuth(disconnectUrl, providerId);
} }
renderUnconnectedProvider(url, name) { renderUnconnectedProvider(url, name) {
return ( return (
<> <React.Fragment>
<h6 aria-level="3">{name}</h6> <h6 aria-level="3">{name}</h6>
<Hyperlink destination={url} className="btn btn-outline-primary"> <Hyperlink destination={url} className="btn btn-outline-primary">
<FormattedMessage <FormattedMessage
@@ -30,7 +28,7 @@ class ThirdPartyAuth extends Component {
values={{ name }} values={{ name }}
/> />
</Hyperlink> </Hyperlink>
</> </React.Fragment>
); );
} }
@@ -38,7 +36,7 @@ class ThirdPartyAuth extends Component {
const hasError = this.props.errors[id]; const hasError = this.props.errors[id];
return ( return (
<> <React.Fragment>
<h6 aria-level="3"> <h6 aria-level="3">
{name} {name}
<span className="small font-weight-normal text-muted ml-2"> <span className="small font-weight-normal text-muted ml-2">
@@ -60,7 +58,7 @@ class ThirdPartyAuth extends Component {
) : null} ) : null}
<StatefulButton <StatefulButton
variant="link" className="btn-link"
state={this.props.disconnectionStatuses[id]} state={this.props.disconnectionStatuses[id]}
labels={{ labels={{
default: ( default: (
@@ -77,7 +75,7 @@ class ThirdPartyAuth extends Component {
data-disconnect-url={url} data-disconnect-url={url}
data-provider-id={id} data-provider-id={id}
/> />
</> </React.Fragment>
); );
} }
@@ -87,9 +85,9 @@ class ThirdPartyAuth extends Component {
return ( return (
<div className="form-group" key={id}> <div className="form-group" key={id}>
{ {
connected connected ?
? this.renderConnectedProvider(disconnectUrl, name, id) this.renderConnectedProvider(disconnectUrl, name, id) :
: this.renderUnconnectedProvider(connectUrl, name) this.renderUnconnectedProvider(connectUrl, name)
} }
</div> </div>
); );
@@ -100,15 +98,13 @@ class ThirdPartyAuth extends Component {
<FormattedMessage <FormattedMessage
id="account.settings.sso.no.providers" id="account.settings.sso.no.providers"
defaultMessage="No accounts can be linked at this time." defaultMessage="No accounts can be linked at this time."
description="Displayed when no third-party accounts are available for the user to link to their account on the platform." description="Displayed when no third party accounts are available to link an edX account to"
/> />
); );
} }
render() { render() {
if (this.props.providers === undefined) { if (this.props.providers === undefined) return null;
return null;
}
if (this.props.providers.length === 0) { if (this.props.providers.length === 0) {
return this.renderNoProviders(); return this.renderNoProviders();
@@ -118,6 +114,7 @@ class ThirdPartyAuth extends Component {
} }
} }
ThirdPartyAuth.propTypes = { ThirdPartyAuth.propTypes = {
providers: PropTypes.arrayOf(PropTypes.shape({ providers: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string, name: PropTypes.string,

BIN
src/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,23 +0,0 @@
import { useEffect, useState } from 'react';
// eslint-disable-next-line import/prefer-default-export
export function useAsyncCall(asyncFunc) {
const [isLoading, setIsLoading] = useState();
const [data, setData] = useState();
useEffect(
() => {
(async () => {
setIsLoading(true);
const response = await asyncFunc();
setIsLoading(false);
if (response) {
setData(response);
}
})();
},
[asyncFunc],
);
return [isLoading, data];
}

View File

@@ -1,363 +1,296 @@
{ {
"account.settings.message.duplicate.tpa.provider": "The {provider} account you selected is already linked to another {siteName} account.", "account.settings.message.duplicate.tpa.provider": "The {provider} account you selected is already linked to another edX account.",
"account.settings.message.managed.settings": "تتم إدارة إعدادات الملف الشخصي بواسطة {ManagerTitle}. اتصل بالمسؤول أو {support} للحصول على المساعدة.", "account.settings.message.managed.settings": "Your profile settings are managed by {managerTitle}. Contact your administrator or {support} for help.",
"account.settings.message.managed.settings.support": "الدعم", "account.settings.message.managed.settings.support": "support",
"account.settings.page.heading": "إعدادات الحساب", "account.settings.page.heading": "Account Settings",
"account.settings.loading.message": "جاري التحميل...", "account.settings.loading.message": "Loading...",
"account.settings.loading.error": "خطأ: {error}", "account.settings.loading.error": "Error: {error}",
"account.settings.banner.beta.language": "لقد قمت بتعيين لغتك الى {beta_language}، والتي لم تتم ترجمتها بالكامل حاليًا. يمكنك مساعدتنا في ترجمة هذه اللغة بالكامل من خلال الانضمام إلى مجتمع Transifex وإضافة ترجمات من الإنجليزية للمتعلمين الذين يتحدثون {beta_language}.", "account.settings.banner.beta.language": "You have set your language to {beta_language}, which is currently not fully translated. You can help us translate this language fully by joining the Transifex community and adding translations from English for learners that speak {beta_language}.",
"account.settings.banner.beta.language.action.switch.back": "العودة لـِ {previous_language}", "account.settings.banner.beta.language.action.switch.back": "Switch Back to {previous_language}",
"account.settings.banner.beta.language.action.help.translate": "المساعدة في الترجمة إلى {beta_language}", "account.settings.banner.beta.language.action.help.translate": "Help Translate into {beta_language}",
"account.settings.section.account.information": "معلومات الحساب", "account.settings.section.account.information": "Account Information",
"account.settings.section.account.information.description": "تتضمن هذه الإعدادات معلومات أساسية عن حسابك.", "account.settings.section.account.information.description": "These settings include basic information about your account.",
"account.settings.section.profile.information": "معلومات الملف الشخصي", "account.settings.section.profile.information": "Profile Information",
"account.settings.section.demographics.information": "معلومات اختيارية", "account.settings.section.demographics.information": "Optional Information",
"account.settings.section.site.preferences": "تفضيلات الموقع", "account.settings.section.site.preferences": "Site Preferences",
"account.settings.section.linked.accounts": "الحسابات المرتبطة", "account.settings.section.linked.accounts": "Linked Accounts",
"account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to {siteName}.", "account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to edX.",
"account.settings.field.username": "اسم المستخدم", "account.settings.field.username": "Username",
"account.settings.field.username.help.text": "The name that identifies you on {siteName}. You cannot change your username.", "account.settings.field.username.help.text": "The name that identifies you on edX. You cannot change your username.",
"account.settings.field.full.name": "الاسم الكامل", "account.settings.field.full.name": "Full name",
"account.settings.field.full.name.empty": "إضافة اسم", "account.settings.field.full.name.empty": "Add name",
"account.settings.field.full.name.help.text": "الاسم المستخدم للتحقق من هويتك والذي سوف يظهر على الشهادات الخاصة بك.", "account.settings.field.full.name.help.text": "The name that is used for ID verification and that appears on your certificates.",
"account.settings.field.full.name.help.text.non.certificate": "The name that appears on your public profile.", "account.settings.field.email": "Email address (Sign in)",
"account.settings.field.full.name.help.text.certificate": "This name is selected to appear on your certificates and public-facing records.", "account.settings.field.email.empty": "Add email address",
"account.settings.field.name.verified": "Verified name", "account.settings.field.email.confirmation": "Weve sent a confirmation message to {value}. Click the link in the message to update your email address.",
"account.settings.field.name.verified.help.text.verified": "This name has been verified by government ID.", "account.settings.field.email.help.text": "You receive messages from edX and course teams at this address.",
"account.settings.field.name.verified.help.text.certificate": "This name has been verified by government ID and selected to appear on your certificates and public-facing records.", "account.settings.field.secondary.email": "Recovery email address",
"account.settings.field.name.verified.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Verified name cannot be changed at this time.", "account.settings.field.secondary.email.empty": "Add a recovery email address",
"account.settings.field.name.verified.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Verified name cannot be changed at this time.", "account.settings.field.secondary.email.confirmation": "Weve sent a confirmation message to {value}. Click the link in the message to update your recovery email address.",
"account.settings.field.name.verified.verification.help": "Enter your name as it appears on your government-issued ID.", "account.settings.email.field.confirmation.header": "Pending confirmation",
"account.settings.field.full.name.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Full name cannot be changed at this time.", "account.settings.field.dob": "Year of birth",
"account.settings.field.full.name.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Full name cannot be changed at this time.", "account.settings.field.dob.empty": "Add year of birth",
"account.settings.field.name.verified.success.message": "Your identity verification request has successfully completed. You now have the option of selecting which name you prefer to appear on your certificates and public-records.", "account.settings.field.year_of_birth.options.empty": "Select a year of birth",
"account.settings.field.name.verified.success.message.header": "Your name change request is complete!", "account.settings.field.country": "Country",
"account.settings.field.name.verified.failure.message": "Your most recent identity verification attempt did not pass. Related account settings have been restored.", "account.settings.field.country.empty": "Add country",
"account.settings.field.name.verified.failure.message.header": "We were not able to verify your identity.", "account.settings.field.country.options.empty": "Select a Country",
"account.settings.field.name.verified.failure.message.help.link": "Learn more about ID verification", "account.settings.field.state": "State",
"account.settings.field.name.verified.submitted.message": "Your identity verification request has been submitted and usually takes between 24 and 48 hours to complete.", "account.settings.field.state.empty": "Add state",
"account.settings.field.name.verified.submitted.message.certificate": "When your request is approved, your updated name will appear on all associated certificates and public-facing records.", "account.settings.field.state.options.empty": "Select a State",
"account.settings.field.name.verified.submitted.message.header": "Your name change request is almost complete!", "account.settings.field.site.language": "Site language",
"account.settings.field.email": "البريد الالكتروني (الدخول)", "account.settings.field.site.language.help.text": "The language used throughout this site. This site is currently available in a limited number of languages.",
"account.settings.field.email.empty": "إضافة عنوان البريد الإلكتروني", "account.settings.field.education": "Education",
"account.settings.field.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر فوق الرابط في الرسالة لتحديث عنوان بريدك الإلكتروني.", "account.settings.field.education.empty": "Add level of education",
"account.settings.field.email.help.text": "You receive messages from {siteName} and course teams at this address.", "account.settings.field.education.levels.empty": "Select a level of education",
"account.settings.field.secondary.email": "عنوان البريد الإلكتروني للاسترداد", "account.settings.field.education.levels.p": "Doctorate",
"account.settings.field.secondary.email.empty": "إضافة عنوان البريد الإلكتروني للاسترداد", "account.settings.field.education.levels.m": "Master's or professional degree",
"account.settings.field.secondary.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر فوق الرابط في الرسالة لتحديث عنوان بريد الاسترداد الإلكتروني.", "account.settings.field.education.levels.b": "Bachelor's Degree",
"account.settings.email.field.confirmation.header": "تعليق عملية التأكيد", "account.settings.field.education.levels.a": "Associate's degree",
"account.settings.field.dob": "سنة الميلاد", "account.settings.field.education.levels.hs": "Secondary/high school",
"account.settings.field.dob.empty": "إضافة سنة الميلاد", "account.settings.field.education.levels.jhs": "Junior secondary/junior high/middle school",
"account.settings.field.year_of_birth.options.empty": "اختر سنة ميلاد", "account.settings.field.education.levels.el": "Elementary/primary school",
"account.settings.field.country": "الدولة", "account.settings.field.education.levels.none": "No formal education",
"account.settings.field.country.empty": "إضافة البلد ", "account.settings.field.education.levels.o": "Other education",
"account.settings.field.country.options.empty": "اختر دولة", "account.settings.field.gender": "Gender",
"account.settings.field.state": "الحالة", "account.settings.field.gender.empty": "Add gender",
"account.settings.field.state.empty": "إضافة منطقة", "account.settings.field.gender.options.empty": "Select a gender",
"account.settings.field.state.options.empty": "اختر منطقة", "account.settings.field.gender.options.f": "Female",
"account.settings.field.site.language": "لغة الموقع", "account.settings.field.gender.options.m": "Male",
"account.settings.field.site.language.help.text": "اللغة المستخدمة في كافة أقسام هذا الموقع. يتوفّر هذا الموقع حاليًا بعدد محدود من اللغات.", "account.settings.field.gender.options.o": "Other",
"account.settings.field.education": "المستوى التعليمي", "account.settings.field.language.proficiencies": "Spoken languages",
"account.settings.field.education.empty": "إضافة المستوى التعليمي", "account.settings.field.language.proficiencies.empty": "Add a spoken language",
"account.settings.field.education.levels.empty": "اختر المستوى التعليمي", "account.settings.field.language_proficiencies.options.empty": "Select a Language",
"account.settings.field.education.levels.p": "دكتوراه", "account.settings.field.time.zone": "Time zone",
"account.settings.field.education.levels.m": "ماجستير أو شهادة مهنيّة", "account.settings.field.time.zone.empty": "Set time zone",
"account.settings.field.education.levels.b": "بكالوريوس", "account.settings.field.time.zone.description": "Select the time zone for displaying course dates. If you do not specify a time zone, course dates, including assignment deadlines, will be displayed in your browsers local time zone.",
"account.settings.field.education.levels.a": "زمالة", "account.settings.field.time.zone.default": "Default (Local Time Zone)",
"account.settings.field.education.levels.hs": "شهادة الثانوية العامة", "account.settings.field.time.zone.all": "All time zones",
"account.settings.field.education.levels.jhs": "شهادة الثانوية الصغرى/الإعدادية/المرحلة المتوسّطة", "account.settings.field.time.zone.country": "Country time zones",
"account.settings.field.education.levels.el": "شهادة المدرسة الابتدائية", "account.settings.section.social.media": "Social Media Links",
"account.settings.field.education.levels.none": "لا يوجد تعليم رسمي", "account.settings.section.social.media.description": "Optionally, link your personal accounts to the social media icons on your edX profile.",
"account.settings.field.education.levels.o": "نوع آخر من التعليم", "account.settings.field.social.platform.name.linkedin": "LinkedIn",
"account.settings.field.gender": "الجنس", "account.settings.field.social.platform.name.linkedin.empty": "Add LinkedIn profile",
"account.settings.field.gender.empty": "إضافة الجنس", "account.settings.jump.nav.delete.account": "Delete My Account",
"account.settings.field.gender.options.empty": "اختر جنسًا", "account.settings.field.social.platform.name.twitter": "Twitter",
"account.settings.field.gender.options.f": "أنثى", "account.settings.field.social.platform.name.twitter.empty": "Add Twitter profile",
"account.settings.field.gender.options.m": "ذكر", "account.settings.field.social.platform.name.facebook": "Facebook",
"account.settings.field.gender.options.o": "آخر", "account.settings.field.social.platform.name.facebook.empty": "Add Facebook profile",
"account.settings.field.language.proficiencies": "لغة التحدّث", "account.settings.editable.field.action.save": "Save",
"account.settings.field.language.proficiencies.empty": "إضافة لغة التحدث", "account.settings.editable.field.action.cancel": "Cancel",
"account.settings.field.language_proficiencies.options.empty": "اختر لغة", "account.settings.editable.field.action.edit": "Edit",
"account.settings.field.time.zone": "المنطقة الزمنية", "account.settings.static.field.empty": "No value set. Contact your {enterprise} administrator to make changes.",
"account.settings.field.time.zone.empty": "ضبط المنطقة الزمنية", "account.settings.static.field.empty.no.admin": "No value set.",
"account.settings.field.time.zone.description": "حدد المنطقة الزمنية لعرض تواريخ المساق. إذا لم تحدد منطقة زمنية، فسيتم عرض تواريخ المساق، بما في ذلك المواعيد النهائية للواجب في المنطقة الزمنية المحلية في المتصفح.", "account.settings.coaching.consent.welcome.header": "Lets get started.",
"account.settings.field.time.zone.default": "الافتراضي (منطقة التوقيت المحلي)", "account.settings.coaching.consent.welcome.subheader": "We're here for you from start to finish",
"account.settings.field.time.zone.all": "جميع المناطق الزمنية", "account.settings.coaching.consent.description": "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.*",
"account.settings.field.time.zone.country": "المنطقة الزمنية للدولة", "account.settings.coaching.consent.text-messaging.disclaimer": "* 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.",
"account.settings.section.social.media": "روابط منصات التواصل الإجتماعي", "account.settings.coaching.consent.accept-coaching": "Sign up for coaching",
"account.settings.section.social.media.description": "Optionally, link your personal accounts to the social media icons on your {siteName} profile.", "account.settings.coaching.consent.decline-coaching": "I prefer not to be contacted with free coaching services",
"account.settings.field.social.platform.name.linkedin": "لينكد إن", "account.settings.coaching.consent.label.name": "Please confirm your name",
"account.settings.field.social.platform.name.linkedin.empty": "إضافة عنوان ملف لينكد إن ", "account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
"account.settings.jump.nav.delete.account": "احذف حسابي", "account.settings.coaching.consent.success.header": "Success!",
"account.settings.field.social.platform.name.twitter": "تويتر", "account.settings.coaching.consent.success.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
"account.settings.field.social.platform.name.twitter.empty": "إضافة عنوان صفحة تويتر", "account.settings.coaching.consent.success.continue": "Start my course",
"account.settings.field.social.platform.name.facebook": "فيسبوك", "account.settings.coaching.managed.support": "support",
"account.settings.field.social.platform.name.facebook.empty": "إضافة عنوان صفحة فيسبوك", "account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
"account.settings.editable.field.action.save": "حفظ", "account.settings.field.phone_number": "Phone Number",
"account.settings.editable.field.action.cancel": "إلغاء", "account.settings.field.phone_number.empty": "Add a phone number",
"account.settings.editable.field.action.edit": "تحرير", "account.settings.field.coaching_consent": "Coaching consent",
"account.settings.static.field.empty": "لم يتم تحديد قيمة، فضلًا اتصل بمدير {enterprise} لتعيين بعض التغييرات.", "account.settings.field.coaching_consent.tooltip": "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.",
"account.settings.static.field.empty.no.admin": "لم يتم تحديد قيمة", "account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
"account.settings.field.name.certificate.select": "If checked, this name will appear on your certificates and public-facing records.", "account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
"account.settings.field.name.modal.certificate.title": "Choose a preferred name for certificates and public-facing records", "account.settings.delete.account.header": "Delete My Account",
"account.settings.field.name.modal.certificate.select": "Select a name", "account.settings.delete.account.subheader": "We're sorry to see you go!",
"account.settings.field.name.modal.certificate.option.full": "Full Name", "account.settings.delete.account.text.1": "Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.",
"account.settings.field.name.modal.certificate.option.verified": "Verified Name", "account.settings.delete.account.text.2": "Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.",
"account.settings.field.name.modal.certificate.button.choose": "Choose name", "account.settings.delete.account.text.3.link": "follow the instructions for printing or downloading a certificate",
"account.settings.coaching.consent.welcome.header": "لنبدأ", "account.settings.delete.account.text.warning": "Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.",
"account.settings.coaching.consent.welcome.subheader": "نحن هنا لأجلك من البداية حتى النهاية", "account.settings.delete.account.text.change.instead": "Want to change your email, name, or password instead?",
"account.settings.coaching.consent.description": "تتضمن برامج البكالوريوس التدريب الذي يركز على مهنتك وتعليمك وكيفية تحقيق نتائج مبهرة من خلال التواصل الشخصي مع خبراء متمرسين. إذا كنت مهتمًا، فقدّم المعلومات أدناه وانقر فوق \"إرسال\"، وسيتصل بك شريكنا في التدريب عبر البريد الإلكتروني و/أو الرسائل النصية لمساعدتك على المضي قدمًا. تنطبق الشروط والأحكام.*", "account.settings.delete.account.button": "Delete My Account",
"account.settings.coaching.consent.text-messaging.disclaimer": "* يتم تضمين خدمات التدريب بدون أي تكلفة إضافية للمتعلمين الذين لديهم أرقام هواتف أمريكية. يتضمن التدريب رسائل نصية متكررة. قد تنطبق أسعار الرسائل والبيانات. إيقاف النص لإلغاء الاشتراك.", "account.settings.delete.account.please.activate": "activate your account",
"account.settings.coaching.consent.accept-coaching": "سجّل للاستفادة من خدمات التدريب", "account.settings.delete.account.please.unlink": "unlink all social media accounts",
"account.settings.coaching.consent.decline-coaching": "أفضّل عدم الاتصال بخدمات التدريب المجانية", "account.settings.delete.account.modal.header": "Are you sure?",
"account.settings.coaching.consent.label.name": "يرجى تأكيد الاسم", "account.settings.delete.account.modal.text.1": "You have selected \"Delete My Account\". Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.",
"account.settings.coaching.consent.label.phone-number": "فضلًا أدخل رقم الهاتف ", "account.settings.delete.account.modal.text.2": "If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.",
"account.settings.coaching.consent.success.header": "تمت العملية بنجاح", "account.settings.delete.account.modal.enter.password": "If you still wish to continue and delete your account, please enter your account password:",
"account.settings.coaching.consent.success.message": "لقد اشتركت في التدريب. يمكنك توقع استلام رسالة عبر البريد الإلكتروني أو الرسائل القصيرة في الأيام المقبلة.", "account.settings.delete.account.modal.confirm.delete": "Yes, Delete",
"account.settings.coaching.consent.success.continue": "البدء في مساقي", "account.settings.delete.account.modal.confirm.cancel": "Cancel",
"account.settings.coaching.managed.support": "الدعم", "account.settings.delete.account.error.unable.to.delete": "Unable to delete account",
"account.settings.coaching.managed.alert": "تتم إدارة اسمك بواسطة {ManagerTitle}. اتصل بالمسؤول للحصول على المساعدة.", "account.settings.delete.account.error.no.password": "A password is required",
"account.settings.field.phone_number": "رقم الهاتف", "account.settings.delete.account.error.invalid.password": "Password is incorrect",
"account.settings.field.phone_number.empty": "إضافة رقم الهاتف", "account.settings.delete.account.error.unable.to.delete.details": "Sorry, there was an error trying to process your request. Please try again later.",
"account.settings.field.coaching_consent": "اتفاقية التدريب", "account.settings.delete.account.modal.after.header": "We're sorry to see you go! Your account will be deleted shortly.",
"account.settings.field.coaching_consent.tooltip": "تتضمن برامج البكالوريوس التدريب القائم على الرسائل النصية الذي يساعدك على إقران التجارب التعليمية مع أهدافك المهنية من خلال النصائح الشخصية. يتم تضمين خدمات التدريب بدون أي تكلفة إضافية، وهي متوفرة للمتعلمين الذين لديهم أرقام هواتف نقالة أمريكية. تنطبق أسعار المراسلة القياسية. أرسل \"قف\" في أي وقت لإلغاء الرسائل.", "account.settings.delete.account.modal.after.text": "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.",
"account.settings.field.coaching_consent.error": "مطلوب رقم هاتف أمريكي صالح للدخول في التدريب", "account.settings.delete.account.modal.after.button": "Close",
"account.settings.delete.account.before.proceeding": "قبل المتابعة، يرجى {actionLink}.", "account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
"account.settings.delete.account.header": "احذف حسابي", "account.settings.field.demographics.gender": "Gender identity",
"account.settings.delete.account.subheader": "نأسف لذهابك!", "account.settings.field.demographics.gender.empty": "Add gender identity",
"account.settings.delete.account.text.1": "Please note: Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.", "account.settings.field.demographics.gender.options.empty": "Select a gender identity",
"account.settings.delete.account.text.2": "Once your account is deleted, you cannot use it to take courses on {siteName}.", "account.settings.field.demographics.gender.options.woman": "Woman",
"account.settings.delete.account.text.2.edX": "Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.", "account.settings.field.demographics.gender.options.man": "Man",
"account.settings.delete.account.text.3.link": "Follow these instructions for printing or downloading a certificate", "account.settings.field.demographics.gender.options.non-binary": "Non-binary",
"account.settings.delete.account.text.warning": "Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on {siteName}.", "account.settings.field.demographics.gender.options.self-describe": "Prefer to self-describe",
"account.settings.delete.account.text.change.instead": "هل تريد تغيير البريد الإلكتروني أو الاسم أو كلمة المرور بدلاً من ذلك؟", "account.settings.field.demographics.gender_description": "Gender identity description",
"account.settings.delete.account.button": "احذف حسابي", "account.settings.field.demographics.gender_description.empty": "Enter description",
"account.settings.delete.account.please.activate": "تنشيط حسابك", "account.settings.field.demographics.ethnicity": "Race/Ethnicity identity",
"account.settings.delete.account.please.unlink": "إلغاء ربط جميع حسابات التواصل الاجتماعي", "account.settings.field.demographics.ethnicity.empty": "Add race/ethnicity identity",
"account.settings.delete.account.modal.header": "هل أنت متأكد؟", "account.settings.field.demographics.ethnicity.options.empty": "Select all that apply",
"account.settings.delete.account.modal.text.1": "You have selected \"Delete My Account\". Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.", "account.settings.field.demographics.ethnicity.options.american-indian-or-alaska-native": "American Indian or Alaska Native",
"account.settings.delete.account.modal.text.2": "If you proceed, you will be unable to use this account to take courses on {siteName}.", "account.settings.field.demographics.ethnicity.options.asian": "Asian",
"account.settings.delete.account.modal.text.2.edX": "If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.", "account.settings.field.demographics.ethnicity.options.black-or-african-american": "Black or African American",
"account.settings.delete.account.modal.enter.password": "إذا كنت لا تزال ترغب في المتابعة وحذف حسابك ، فيرجى إدخال كلمة مرور حسابك:", "account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish": "Hispanic, Latin, or Spanish origin",
"account.settings.delete.account.modal.confirm.delete": "تعم، أحذف", "account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african": "Middle Eastern or North African",
"account.settings.delete.account.modal.confirm.cancel": "إلغاء", "account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander": "Native Hawaiian or Other Pacific Islander",
"account.settings.delete.account.error.unable.to.delete": "تعذر حذف الحساب", "account.settings.field.demographics.ethnicity.options.white": "White",
"account.settings.delete.account.error.no.password": "كلمة المرور مطلوبة", "account.settings.field.demographics.ethnicity.options.other": "Some other race, ethnicity, or origin",
"account.settings.delete.account.error.invalid.password": "كلمة المرور المدخلة غير صحيحة", "account.settings.field.demographics.income": "Family income",
"account.settings.delete.account.error.unable.to.delete.details": "عذراً ، حدث خطأ أثناء محاولة معالجة طلبك. الرجاء معاودة المحاولة في وقت لاحق.", "account.settings.field.demographics.income.empty": "Add family income",
"account.settings.delete.account.modal.after.header": "نأسف لذهابك! سيتم حذف حسابك قريبا.", "account.settings.field.demographics.income.options.empty": "Select a family income range",
"account.settings.delete.account.modal.after.text": "قد يستغرق حذف الحساب ، بما في ذلك الإزالة من قوائم البريد الإلكتروني ، بضعة أسابيع حتى تتم معالجته بالكامل من خلال نظامنا. إذا كنت ترغب في إلغاء الاشتراك في رسائل البريد الإلكتروني قبل ذلك الحين ، يرجى إلغاء الاشتراك من تذييل أي بريد إلكتروني.", "account.settings.field.demographics.income.options.less-than-10k": "Less than US $10,000",
"account.settings.delete.account.modal.after.button": "إغلاق ", "account.settings.field.demographics.income.options.10k-25k": "US $10,000 - $25,000",
"account.settings.delete.account.text.3.edX": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. You can make a copy of these for your records before proceeding with deletion. {actionLink}.", "account.settings.field.demographics.income.options.25k-50k": "US $25,000 - $50,000",
"account.settings.delete.account.text.3": "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.", "account.settings.field.demographics.income.options.50k-75k": "US $50,000 - $75,000",
"account.settings.message.demographics.service.issue": "حدث خطأ أثناء محاولة استرداد معلومات حسابك أو حفظها. يرجى المحاولة مرة أخرى لاحقًا.", "account.settings.field.demographics.income.options.over-100k": "Over US $100,000",
"account.settings.field.demographics.gender": "هوية الجنس", "account.settings.field.demographics.income.options.unsure": "I don't know",
"account.settings.field.demographics.gender.empty": "إضافة هوية الجنس", "account.settings.field.demographics.military_history": "U.S. Military status",
"account.settings.field.demographics.gender.options.empty": "حدد هوية الجنس", "account.settings.field.demographics.military_history.empty": "Add military status",
"account.settings.field.demographics.gender_description": "وصف هوية الجنس", "account.settings.field.demographics.military_history.options.empty": "Select military status",
"account.settings.field.demographics.gender_description.empty": "أدخل وصفًا", "account.settings.field.demographics.income.options.never-served": "Never served in the military",
"account.settings.field.demographics.ethnicity": "هوية العرق/ الأصل", "account.settings.field.demographics.income.options.training": "Only on active duty for training",
"account.settings.field.demographics.ethnicity.empty": "إضافة هوية العرق/الأصل", "account.settings.field.demographics.income.options.active": "Now on active duty",
"account.settings.field.demographics.ethnicity.options.empty": "فضلًا اختر جميع ما ينطبق", "account.settings.field.demographics.income.options.previously-active": "On active duty in the past, but not now",
"account.settings.field.demographics.income": "الدخل المادي الأسري", "account.settings.field.demographics.learner_education_level": "Your education level",
"account.settings.field.demographics.income.empty": "إضافة الدخل المادي الأسري", "account.settings.field.demographics.learner_education_level.empty": "Add education level",
"account.settings.field.demographics.income.options.empty": "حدد نطاق الدخل المادي للأسرة", "account.settings.field.demographics.parent_education_level": "Parents/Guardians education level",
"account.settings.field.demographics.military_history": "حالة الخدمة العسكرية في الولايات المتحدة الأمريكية", "account.settings.field.demographics.parent_education_level.empty": "Add education level",
"account.settings.field.demographics.military_history.empty": "إضافة حالة الخدمة العسكرية", "account.settings.field.demographics.education_level.options.empty": "Select education level",
"account.settings.field.demographics.military_history.options.empty": "اختر حالة الخدمة العسكرية", "account.settings.field.demographics.education_level.options.no-high-school": "No High School",
"account.settings.field.demographics.learner_education_level": "مؤهلك التعليمي", "account.settings.field.demographics.education_level.options.some-high-school": "Some High School",
"account.settings.field.demographics.learner_education_level.empty": "إضافة مؤهلك التعليمي", "account.settings.field.demographics.education_level.options.high-school-ged-equivalent": "High School diploma, GED, or equivalent",
"account.settings.field.demographics.parent_education_level": "مؤهل الوالدين/الآوصياء التعليمي", "account.settings.field.demographics.education_level.options.some-college": "Some college, but no degree",
"account.settings.field.demographics.parent_education_level.empty": "إضافة المؤهل التعليمي", "account.settings.field.demographics.education_level.options.associates": "Associates degree",
"account.settings.field.demographics.education_level.options.empty": "حدد موهلاً تعليميًا", "account.settings.field.demographics.education_level.options.bachelors": "Bachelors degree",
"account.settings.field.demographics.work_status": "الحالة الوظيفية", "account.settings.field.demographics.education_level.options.masters": "Masters degree",
"account.settings.field.demographics.work_status.empty": "إضافة الحالة الوظيفية", "account.settings.field.demographics.education_level.options.professional": "Professional degree",
"account.settings.field.demographics.work_status.options.empty": "فضلًا حدد حالتك الوظيفية", "account.settings.field.demographics.education_level.options.doctorate": "Doctorate degree",
"account.settings.field.demographics.work_status_description": "وصف الحالة الوظيفية", "account.settings.field.demographics.work_status": "Employment status",
"account.settings.field.demographics.work_status_description.empty": "أدخل وصفًا", "account.settings.field.demographics.work_status.empty": "Add employment status",
"account.settings.field.demographics.current_work_sector": "مجال العمل الحالي", "account.settings.field.demographics.work_status.options.empty": "Select employment status",
"account.settings.field.demographics.current_work_sector.empty": "إضافة مجال العمل", "account.settings.field.demographics.work_status.options.full-time": "Employed, working full-time",
"account.settings.field.demographics.future_work_sector": "مجال العمل المستقبلي", "account.settings.field.demographics.work_status.options.part-time": "Employed, working part-time",
"account.settings.field.demographics.future_work_sector.empty": "إضافة مجال العمل", "account.settings.field.demographics.work_status.options.not-employed-looking": "Not employed, looking for work",
"account.settings.field.demographics.work_sector.options.empty": "حدد مجال العمل", "account.settings.field.demographics.work_status.options.not-employed-not-looking": "Not employed, not looking for work",
"account.settings.section.demographics.why": "Why does {siteName} collect this information?", "account.settings.field.demographics.work_status.options.unable": "Unable to work",
"account.settings.name.change.title.id": "This name change requires identity verification", "account.settings.field.demographics.work_status.options.retired": "Retired",
"account.settings.name.change.title.begin": "Before we begin", "account.settings.field.demographics.work_status.options.other": "Other",
"account.settings.name.change.warning.one": "Warning: This action updates the name that appears on all certificates that have been earned on this account in the past and any certificates you are currently earning or will earn in the future.", "account.settings.field.demographics.work_status_description": "Employment status description",
"account.settings.name.change.warning.two": "This action cannot be undone without verifying your identity.", "account.settings.field.demographics.work_status_description.empty": "Enter description",
"account.settings.name.change.id.name.label": "Enter your name as it appears on your government-issued ID.", "account.settings.field.demographics.current_work_sector": "Current work industry",
"account.settings.name.change.id.name.placeholder": "Enter the name on your government ID", "account.settings.field.demographics.current_work_sector.empty": "Add work industry",
"account.settings.name.change.error.valid.name": "Please enter a valid name.", "account.settings.field.demographics.future_work_sector": "Future work industry",
"account.settings.name.change.error.general": "A technical error occurred. Please try again.", "account.settings.field.demographics.future_work_sector.empty": "Add work industry",
"account.settings.name.change.continue": "Continue", "account.settings.field.demographics.work_sector.options.empty": "Select work industry",
"account.settings.name.change.cancel": "Cancel", "account.settings.field.demographics.work_sector.options.accommodation-food": "Accommodation and Food Services",
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في نص الرابط. الرجاء التحقق من الرابط والمحاولة مجددا.", "account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation": "Administrative and Support and Waste Management and Remediation Services",
"account.settings.editable.field.password.reset.button.confirmation.support.link": "الدعم الفني", "account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting": "Agriculture, Forestry, Fishing and Hunting",
"account.settings.editable.field.password.reset.button.confirmation": "لقد أرسلنا رسالة إلى {email}. انقر فوق الرابط في الرسالة لإعادة تعيين كلمة المرور. إذا لم يتم استلام الرسالة؟ اتصل بـ {technicalSupportLink}.", "account.settings.field.demographics.work_sector.options.arts-entertainment-recreation": "Arts, Entertainment, and Recreation",
"account.settings.editable.field.password.reset.button": "تغيير كلمة المرور", "account.settings.field.demographics.work_sector.options.construction": "Construction",
"account.settings.editable.field.password.reset.button.forbidden": "طلبك السابق قيد التقدم، يرجى إعادة المحاولة بعد لحظات قليلة.", "account.settings.field.demographics.work_sector.options.educational": "Education Services",
"account.settings.editable.field.password.reset.label": "كلمة المرور", "account.settings.field.demographics.work_sector.options.finance-insurance": "Finance and Insurance",
"account.settings.sso.link.account": "تسجيل الدخول كـ {name}", "account.settings.field.demographics.work_sector.options.healthcare-social": "Health Care and Social Assistance",
"account.settings.sso.account.connected": "مربوط", "account.settings.field.demographics.work_sector.options.information": "Information",
"account.settings.sso.account.disconnect.error": "حدثت مشكلة أثناء قطع اتصال هذا الحساب، اتصل بالدعم عند استمرار المشكلة.", "account.settings.field.demographics.work_sector.options.management": "Management of Companies and Enterprises",
"account.settings.sso.unlink.account": "إلغاء ربط حساب {name} ", "account.settings.field.demographics.work_sector.options.manufacturing": "Manufacturing",
"account.settings.sso.no.providers": "لا يمكن ربط أية حسابات حاليًا", "account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas": "Mining, Quarrying, and Oil and Gas Extraction",
"id.verification.access.blocked.denied": "لا يمكنك التحقق من هويتك في الوقت الحالي. إذا لم تقم بعد بتنشيط حسابك، فيرجى التحقق من مجلد البريد المهمل للحصول على رسالة التفعيل من {email}.", "account.settings.field.demographics.work_sector.options.professional-scientific-technical": "Professional, Scientific, and Technical Services",
"id.verification.next": "التالي", "account.settings.field.demographics.work_sector.options.public-admin": "Public Administration",
"id.verification.support": "support", "account.settings.field.demographics.work_sector.options.real-estate": "Real Estate and Rental and Leasing",
"id.verification.continue.upload": "Continue with Upload", "account.settings.field.demographics.work_sector.options.retail": "Retail Trade",
"id.verification.example.card.alt": "مثال على بطاقة هوية صحيحة بالاسم الكامل وصورة.", "account.settings.field.demographics.work_sector.options.transport-warehousing": "Transportation and Warehousing",
"id.verification.requirements.title": "متطلبات التحقق من الصورة", "account.settings.field.demographics.work_sector.options.utilities": "Utilities",
"id.verification.requirements.description": "In order to complete Photo Verification, you will need the following:", "account.settings.field.demographics.work_sector.options.trade": "Wholesale Trade",
"id.verification.requirements.card.device.title": "جهاز مزود بكاميرا", "account.settings.field.demographics.work_sector.options.other": "Other",
"id.verification.requirements.card.device.allow": "موافق", "account.settings.field.demographics.options.declined": "Prefer not to respond",
"id.verification.requirements.card.id.title": "Photo Identification Card", "error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"id.verification.requirements.card.id.text": "You need a valid identification card that contains your full name and photo, such as a drivers license or passport.", "account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
"id.verification.privacy.title": "بيانات الخصوصية.", "account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
"id.verification.privacy.need.photo.question": "Why does {siteName} need my photo?", "account.settings.editable.field.password.reset.button": "Reset Password",
"id.verification.privacy.need.photo.answer": "نستخدم صور التحقق الخاصة بك لتأكيد هويتك والتأكد من صحة شهادتك.", "account.settings.editable.field.password.reset.button.forbidden": "Your previous request is in progress, please try again in few moments.",
"id.verification.privacy.do.with.photo.question": "What does {siteName} do with this photo?", "account.settings.editable.field.password.reset.label": "Password",
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on {siteName} after the verification process is complete.", "account.settings.sso.link.account": "Sign in with {name}",
"id.verification.access.blocked.title": "التحقق من الهوية", "account.settings.sso.account.connected": "Linked",
"id.verification.access.blocked.enrollment": "أنت الآن ملتحق بمساق يتطلب التحقق من الهوية.", "account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
"id.verification.access.blocked.pending": "لقد قمت بالفعل بإرسال معلومات التحقق الخاصة بك. ستصلك رسالة على لوحة المعلومات عند اكتمال عملية التحقق (عادةً خلال 5 أيام).", "account.settings.sso.unlink.account": "Unlink {name} account",
"id.verification.photo.take": "التقاط صورة ", "account.settings.sso.no.providers": "No accounts can be linked at this time.",
"id.verification.photo.retake": "Retake Photo?", "id.verification.next": "Next",
"id.verification.photo.enable.detection": "تمكين خاصية التعرف على الوجه", "id.verification.requirements.title": "Photo Verification Requirements",
"id.verification.photo.enable.detection.portrait.help.text": "عند تحديد هذا الخيار، فسيظهر مربع حول وجهك. يمكن رؤية وجهك بوضوح إذا كان المربع المحيط به أزرق. إذا لم يكن وجهك في وضع جيد أو إذا لم يكن قابلاً للاكتشاف، فسيكون المربع باللون الأحمر.", "id.verification.requirements.description": "In order to complete Photo Verification online, you will need the following:",
"id.verification.photo.enable.detection.id.help.text": "عند تحديد هذا الخيار، فسيظهر مربع حول صورة وجهك في بطاقة الهوية. يمكن رؤية وجهك بوضوح إذا كان المربع المحيط به أزرق. إذا لم يكن وجهك في وضع جيد أو إذا لم يكن قابلاً للاكتشاف، فسيكون المربع باللون الأحمر.", "id.verification.requirements.card.device.title": "Device with Camera",
"id.verification.photo.feedback.correct": "وضع الوجه جيد.", "id.verification.requirements.card.device.allow": "Allow",
"id.verification.photo.feedback.two.faces": "تم تحديد أكثر من وجه.", "id.verification.requirements.card.id.title": "Photo Identification",
"id.verification.photo.feedback.no.faces": "لم يتم تحديد أي وجه.", "id.verification.requirements.card.id.text": "You need a valid ID that contains your full name and photo.",
"id.verification.photo.feedback.top.left": "وضع خاطئ. أعلى اليسار.", "id.verification.privacy.title": "Privacy Information",
"id.verification.photo.feedback.top.center": "وضع خاطئ. أعلى الوسط.", "id.verification.privacy.need.photo.question": "Why does edX need my photo?",
"id.verification.photo.feedback.top.right": "وضع خاطئ. أعلى اليمين.", "id.verification.privacy.need.photo.answer": "We use your verification photos to confirm your identity and ensure the validity of your certificate.",
"id.verification.photo.feedback.center.left": "وضع خاطئ. وسط اليسار.", "id.verification.privacy.do.with.photo.question": "What does edX do with this photo?",
"id.verification.photo.feedback.center.center": "وضع خاطئ. قريب جدًا من الكاميرا.", "id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
"id.verification.photo.feedback.center.right": "وضع خاطئ. وسط اليمين.", "id.verification.existing.request.title": "Identity Verification",
"id.verification.photo.feedback.bottom.left": "وضع خاطئ. أسفل اليسار.", "id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).",
"id.verification.photo.feedback.bottom.center": "وضع خاطئ. أسفل الوسط.", "id.verification.existing.request.denied.text": "You cannot verify your identity at this time.",
"id.verification.photo.feedback.bottom.right": "وضع خاطئ. أسفل اليمين.", "id.verification.photo.take": "Take Photo",
"id.verification.camera.access.title": "صلاحيات الكاميرا", "id.verification.photo.retake": "Retake Photo",
"id.verification.camera.access.title.success": "تمكين الوصول للكاميرا", "id.verification.camera.access.title": "Camera Permissions",
"id.verification.camera.access.title.failed": "تعذّر الوصول للكاميرا.", "id.verification.camera.access.click.allow": "Please make sure to click \"Allow\"",
"id.verification.camera.access.click.allow": "فضلًا تأكد من اختيار الأمر \"السماح\"", "id.verification.camera.access.enable": "Enable Camera",
"id.verification.camera.access.enable": "تمكين الكاميرا", "id.verification.camera.access.problems": "Having problems?",
"id.verification.camera.access.problems": "هل تواجه أية مشكلة؟", "id.verification.camera.access.skip": "Skip and upload image files instead",
"id.verification.camera.access.skip": "قم بتخطي ملفات الصور وتحميلها بدلاً من ذلك", "id.verification.camera.access.success": "Looks like your camera is working and ready.",
"id.verification.camera.access.success": "يبدو أن الكاميرا تعمل وجاهزة.", "id.verification.camera.access.failure": "It looks like we're unable to access your camera. You will need to upload image files of you and your photo id.",
"id.verification.camera.access.failure": "يبدو أننا غير قادرين على الوصول إلى الكاميرا. ستحتاج إلى تحميل ملفات الصور الخاصة بك و معرّف الصور الخاص بك.", "id.verification.photo.tips.title": "Helpful Photo Tips",
"id.verification.camera.access.failure.temporary": "يبدو أننا غير قادرين على الوصول إلى الكاميرا. يرجى التحقق من أن كاميرا الويب متصلة ومن أنك سمحت للمتصفح بالوصول إليها.", "id.verification.photo.tips.description": "Next, we'll need you to take a photo of your face. Please review the helpful tips below.",
"id.verification.camera.access.failure.temporary.chrome": "تمكين الوصول للكاميرا في متصفّح كروم: ", "id.verification.photo.tips.list.title": "Photo Tips",
"id.verification.camera.access.failure.temporary.chrome.step1": "فتح متصفّح كروم.", "id.verification.photo.tips.list.description": "To take a successful photo, make sure that:",
"id.verification.camera.access.failure.temporary.chrome.step2": "توجه إلى المزيد > الإعدادات.", "id.verification.photo.tips.list.well.lit": "Your face is well-lit.",
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "لنظام ويندوز: Alt+F أو Alt+E أو F10 متبوعاً بـ SPACEBAR", "id.verification.photo.tips.list.inside.frame": "Your entire face fits inside the frame.",
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "لنظام ماك: Command+,", "id.verification.portrait.photo.title.camera": "Take Your Photo",
"id.verification.camera.access.failure.temporary.chrome.step3": "ضمن علامة التبويب \"الخصوصية والأمان\"، حدد \"إعدادات الموقع\" ثم \"الكاميرا\".", "id.verification.portrait.photo.title.upload": "Upload Your Portrait Photo",
"id.verification.camera.access.failure.temporary.chrome.step4": "ضمن \"قائمة الحظر\"، ابحث عن \"edx.org\" وحدده.", "id.verification.portrait.photo.preview.alt": "Preview of photo of user's face.",
"id.verification.camera.access.failure.temporary.chrome.step5": "في القسم \"الصلاحيات\"، قم بتحديث صلاحيات الكاميرا إلى \"السماح\".", "id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.",
"id.verification.camera.access.failure.temporary.ie11": "تمكين الوصول للكاميرا في متصفح انترنت إكسبلورر:", "id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.camera.access.failure.temporary.ie11.step1": "افتح إدارة إعدادات Flash Player عن طريق الانتقال إلى إعدادات Windows > لوحة التحكم > Flash Player.", "id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?",
"id.verification.camera.access.failure.temporary.ie11.step2": "حدد علامة التبويب \"Camera and Mic\" (الكاميرا والميكروفون)، ثم حدد الزر \"Camera and Microphone Settings by Site\" (إعدادات الكاميرا والميكروفون حسب الموقع).", "id.verification.camera.help.sight.answer": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.access.failure.temporary.ie11.step3": "اختر \"edx.org\" من قائمة مواقع ويب وقم بتغيير الصلاحيات من خلال تحديد \"السماح\" في القائمة المنسدلة.", "id.verification.camera.help.head.question": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.access.failure.temporary.firefox": "لتمكين الوصول للكاميرا في متصفّح فايرفوكس: ", "id.verification.camera.help.head.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.camera.access.failure.temporary.firefox.step1": "فتح متصفّح فايرفوكس.", "id.verification.id.tips.title": "Helpful ID Tips",
"id.verification.camera.access.failure.temporary.firefox.step2": "أدخل \"about:preferences\" في شريط عنوان الرابط.", "id.verification.id.tips.description": "Next you'll need an eligible ID photo, make sure that:",
"id.verification.camera.access.failure.temporary.firefox.step3": "حدد علامة التبويب \"الخصوصية والأمان\"، وانتقل إلى القسم \"الصلاحيات\".", "id.verification.id.tips.list.well.lit": "Your ID is well-lit.",
"id.verification.camera.access.failure.temporary.firefox.step4": "بجوار \"Camera\" (الكاميرا)، حدد \"Settings…\" (الإعدادات…) الزر.", "id.verification.id.tips.list.clear": "Ensure that you can see your photo and clearly read your name.",
"id.verification.camera.access.failure.temporary.firefox.step5": "في شريط البحث أدخل \"edx.org.\"", "id.verification.id.photo.title.camera": "Take ID Photo",
"id.verification.camera.access.failure.temporary.firefox.step6": "في عمود الحالة لـ \"edx.org,\" حدد \"السماح\" من القائمة المنسدلة.", "id.verification.id.photo.title.upload": "Upload Your ID Photo",
"id.verification.camera.access.failure.temporary.firefox.step7": "اختر \"حفظ التغييرات.\"", "id.verification.id.photo.preview.alt": "Preview of photo ID.",
"id.verification.camera.access.failure.temporary.safari": "تمكين الوصول للكاميرا في متصفّح سفاري: ", "id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo.",
"id.verification.camera.access.failure.temporary.safari.step1": "فتح متصفّح سفاري.", "id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.camera.access.failure.temporary.safari.step2": "انقر فوق قائمة تطبيق سفاري، ثم حدد \"Preferences\" (التفضيلات). يمكنك أيضاً استخدام الأمرCommand+ كاختصار للوحة المفاتيح", "id.verification.account.name.title": "Account Name Check",
"id.verification.camera.access.failure.temporary.safari.step3": "حدد علامة التبويب \"مواقع ويب\" ثم حدد \"كاميرا\".", "id.verification.account.name.instructions": "Please check the Account Name below to ensure it matches the name on your ID. If not, click \"Edit\".",
"id.verification.camera.access.failure.temporary.safari.step4": "حدد \"edx.org\" وقم بتغيير صلاحيات الكاميرا إلى \"السماح\".", "id.verification.account.name.warning.prefix": "Please Note:",
"id.verification.camera.access.failure.unsupported": "It looks like your browser does not support camera access.", "id.verification.account.name.settings": "Account Settings",
"id.verification.camera.access.failure.unsupported.chrome.explanation": "The Chrome browser currently does not support camera access on iOS devices, such as iPhones and iPads.", "id.verification.account.name.label": "Name on ID",
"id.verification.camera.access.failure.unsupported.instructions": "Please use another browser to complete Identity Verification.", "id.verification.account.name.edit": "Edit",
"id.verification.photo.tips.title": "تلميحات مفيدة للصورة", "id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
"id.verification.photo.tips.description": "بعد ذلك، سنحتاج منك التقاط صورة لوجهك. يرجى مراجعة التلميحات المفيدة أدناه.", "id.verification.account.name.save": "Save",
"id.verification.photo.tips.list.title": "تلميحات الصورة", "id.verification.review.title": "Review Your Photos",
"id.verification.photo.tips.list.description": "لالتقاط صورة ناجحة، يُرجى التأكّد ممّا يلي:", "id.verification.review.description": "Make sure we can verify your identity with the photos and information you have provided.",
"id.verification.photo.tips.list.well.lit": "أنّ الإضاءة جيّدة على وجهك.", "id.verification.review.portrait.label": "Your Portrait",
"id.verification.photo.tips.list.inside.frame": "أنّ وجهك داخل إطار الصورة بالكامل.", "id.verification.review.portrait.alt": "Photo of your face to be submitted.",
"id.verification.portrait.photo.title.camera": "التقط صورة لنفسك", "id.verification.review.portrait.retake": "Retake Portrait Photo",
"id.verification.portrait.photo.title.upload": "ارفع صورتك", "id.verification.review.id.label": "Your Photo ID",
"id.verification.portrait.photo.preview.alt": "معاينة صورة وجه المستخدم.", "id.verification.review.id.alt": "Photo of your ID to be submitted.",
"id.verification.portrait.photo.instructions.camera": "عندما يكون وجهك في موضعه، استخدم زر التقاط صورة أدناه لالتقاط الصورة.", "id.verification.review.id.retake": "Retake ID Photo",
"id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. Supported formats: ", "id.verification.review.confirm": "Confirm",
"id.verification.camera.help.sight.question": "ماذا لو لم أتمكن من رؤية صورة الكاميرا ؟ أو إذا لم أتمكن من رؤية صورتي لتحديد أي جانب مرئي؟", "id.verification.submitted.title": "Identity Verification in Progress",
"id.verification.camera.help.sight.answer.portrait": "قد تتمكن من إكمال إجراء التقاط الصور من دون مساعدة، ولكن قد يتطلب الأمر بضع محاولات ضبط وضع الكاميرا بشكل صحيح. يختلف وضع الكاميرا المثالي باختلاف الكمبيوتر، ولكن بشكل عام، يكون أفضل موضع للتصوير في الرأس هو 12 إلى 18 بوصة (30-45 سم) تقريبًا من الكاميرا، مع وضع الرأس في المنتصف بالنسبة إلى شاشة الكمبيوتر. إذا تم رفض الصور التي ترسلها، فحاول تحريك اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة.", "id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days). In the meantime, you can still access all available course content.",
"id.verification.camera.help.sight.answer.id": "قد تتمكن من إكمال إجراء التقاط الصور من دون مساعدة، ولكن قد يتطلب الأمر بضع محاولات لضبط وضع الكاميرا بشكل صحيح. يختلف الوضع الأمثل للكاميرا باختلاف جهاز الكمبيوتر، ولكن بشكل عام، يكون أفضل وضع لصورة بطاقة تعريف من 8 إلى 12 بوصة (من 20 إلى 30 سم) عن الكاميرا، مع وضع بطاقة الهوية في الوسط بالنسبة للكاميرا. إذا تم رفض الصور التي ترسلها، فحاول تحريك اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة. إن السبب الأكثر شيوعاً للرفض هو عدم القدرة على قراءة النص الموجود على بطاقة الهوية.", "id.verification.submitted.return": "Return to Your Dashboard",
"id.verification.camera.help.difficulty.question.portrait": "ماذا لو واجهت صعوبة في تثبيت رأسي في الموضع المناسب للكاميرا؟", "id.verification.account.name.warning": "{prefix} Any edit to your name will be saved to your account and can be reviewed on {accountSettings}.",
"id.verification.camera.help.difficulty.question.id": "ماذا لو واجهت صعوبة في تثبيت بطاقة هويتي في الموضع المناسب للكاميرا؟", "id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
"id.verification.camera.help.difficulty.answer": "If you require assistance with taking a photo for submission, contact {siteName} support for additional suggestions.", "id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}."
"id.verification.camera.help.upload.question": "What if I want to upload a photo instead?",
"id.verification.camera.help.upload.answer": "On the next page you will have the option to switch to upload mode. By selecting that option, you will be able to upload a photo instead.",
"id.verification.id.photo.unclear.question": "Is your ID card image not clear or too blurry?",
"id.verification.id.tips.title": "Helpful Identification Card Tips",
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid identification card that includes your full name and photo, such as a drivers license or passport. Please have your ID ready.",
"id.verification.id.tips.list.well.lit": "Your identification card is well-lit.",
"id.verification.id.tips.list.clear": "تأكد من قدرتك على رؤية صورتك وقراءة اسمك بوضوح.",
"id.verification.id.photo.title.camera": "Take a Photo of Your Identification Card",
"id.verification.id.photo.title.upload": "Upload a Photo of Your Identification Card",
"id.verification.id.photo.preview.alt": "معاينة صورة الهوية.",
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo. Please use a passport, drivers license, or another identification card that includes your full name and a picture of your face.",
"id.verification.id.photo.instructions.upload": "Please upload a photo of your identification card. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ",
"id.verification.id.photo.instructions.upload.error.invalidFileType": "The file you have selected is not a supported image type. Please choose from the following formats: ",
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "The file you have selected is too large. Please try again with a file less than 10MB.",
"id.verification.account.name.title": "التحقق من اسم الحساب",
"id.verification.name.check.title": "Double-Check Your Name",
"id.verification.account.name.instructions": "يجب أن يكون الاسم الموجود على حسابك والاسم الموجود على المعرّف الخاص بك متطابقًا تمامًا. إذا لم يكن الأمر كذلك، فيرجى النقر فوق \"لا\" لتحديث اسم حسابك.",
"id.verification.name.check.instructions": "Does the name below match the name on your government-issued ID? If not, update the name below to match your goverment-issued ID.",
"id.verification.name.check.mismatch.information": "If the name below does not match your government-issued ID, your identity verification will be denied.",
"id.verification.account.name.radio.label": "هل يتطابق الاسم الموجود على هويتك مع اسم الحساب أدناه؟",
"id.verification.name.check.radio.label": "Select an option",
"id.verification.account.name.radio.yes": "نعم",
"id.verification.name.check.radio.yes": "Yes, the name below matches my ID",
"id.verification.account.name.radio.no": "لا",
"id.verification.name.check.radio.no": "No, the name below does not match my ID",
"id.verification.account.name.error": "يرجى تحديث اسم الحساب لمطابقة الاسم على بطاقة الهوية.",
"id.verification.account.name.warning.prefix": "يُرجى الملاحظة:",
"id.verification.account.name.settings": "إعدادات الحساب",
"id.verification.account.name.label": "اسم الحساب",
"id.verification.name.label": "Name",
"id.verification.account.name.photo.alt": "صورة من هويتك للتقديم.",
"id.verification.account.name.save": "حفظ ثم التالي",
"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": "Your Identification Card",
"id.verification.review.id.alt": "Photo of your identification card to be submitted.",
"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": "One or more of the files you have uploaded is in an unsupported format. Please choose from the following: ",
"id.verification.review.error": "{siteName} Support Page",
"id.verification.submitted.title": "جارِ التحقق من الهوية",
"id.verification.submitted.text": "لقد تلقينا معلوماتك وجاري الآن العمل على التحقق من هويتك. ستصلك رسالة على لوحة المعلومات عند اكتمال عملية التحقق (عادةً خلال 5 أيام). في غضون ذلك، لا يزال بإمكانك الوصول إلى كل محتوى المساق المتوفر.",
"id.verification.return.dashboard": "العودة إلى لوحة المعلومات",
"id.verification.return.course": "العودة للمساق",
"id.verification.photo.upload.help.title": "Upload a Photo Instead",
"id.verification.photo.camera.help.title": "Use Your Camera Instead",
"id.verification.photo.upload.help.text": "If you are having trouble using the photo capture above, you may want to upload a photo instead. To upload a photo, click the button below.",
"id.verification.photo.camera.help.text": "If you are having trouble uploading a photo above, you may want to use your camera instead. To use your camera, click the button below.",
"id.verification.upload.help.button": "Switch to Upload Mode",
"id.verification.camera.help.button": "Switch to Camera Mode",
"id.verification.choose.mode.title": "Photo Requirements Options",
"id.verification.choose.mode.hep.text": "To complete verification, please select one of the following options to submit photos. You will be able to switch between these options throughout the process if needed.",
"id.verification.choose.mode.radio.upload": "Upload photos from my device",
"id.verification.choose.mode.radio.camera": "Take pictures using my camera",
"id.verification.account.name.managed.alert": "Your profile settings are managed by {managerTitle}, so you are not allowed to update your name. Please contact your {profileDataManager} administrator or {support} for help.",
"id.verification.request.camera.access.instructions": "لالتقاط صورة باستخدام كاميرا الويب، قد تتلقى طلب المتصفح للوصول إلى الكاميرا. {clickAllow}",
"id.verification.requirements.account.managed.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help before completing the Photo Verification process.",
"id.verification.requirements.card.device.text": "أنت بحاجة إلى جهاز مزود بكاميرا. إذا تلقيت طلب المتصفح للوصول إلى الكاميرا، فيرجى التأكد من النقر فوق {السماح}.",
"id.verification.account.name.summary.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help.",
"idv.submission.alert.error": "\nواجهنا خطأ فني أثناء محاولة رفع طلب التحقق من الهوية\nقد تكون هذه مشكلة مؤقتة، لذا يرجى المحاولة مرة أخرى بعد بضع دقائق\nعند استمرار المشكلة يرجى الانتقال إلى {support_link} للحصول على المساعدة",
"id.verification.account.name.edit": "Edit {sr}"
} }

View File

@@ -1,5 +1,5 @@
{ {
"account.settings.message.duplicate.tpa.provider": "The {provider} account you selected is already linked to another {siteName} account.", "account.settings.message.duplicate.tpa.provider": "La cuenta de {provider} seleccionada ya está vinculada con otra cuenta de edX. ",
"account.settings.message.managed.settings": "Los ajustes en el perfil son administrados por {managerTitle}. Contacte su administrador o {support} para obtener ayuda.", "account.settings.message.managed.settings": "Los ajustes en el perfil son administrados por {managerTitle}. Contacte su administrador o {support} para obtener ayuda.",
"account.settings.message.managed.settings.support": "soporte", "account.settings.message.managed.settings.support": "soporte",
"account.settings.page.heading": "Configuración de cuenta", "account.settings.page.heading": "Configuración de cuenta",
@@ -11,37 +11,19 @@
"account.settings.section.account.information": "Información de la cuenta", "account.settings.section.account.information": "Información de la cuenta",
"account.settings.section.account.information.description": "Estas configuraciones incluyen información básica sobre tu cuenta.", "account.settings.section.account.information.description": "Estas configuraciones incluyen información básica sobre tu cuenta.",
"account.settings.section.profile.information": "Información del perfil", "account.settings.section.profile.information": "Información del perfil",
"account.settings.section.demographics.information": "Información opcional", "account.settings.section.demographics.information": "Optional Information",
"account.settings.section.site.preferences": "Preferencias del sitio", "account.settings.section.site.preferences": "Preferencias del sitio",
"account.settings.section.linked.accounts": "Cuentas vinculadas", "account.settings.section.linked.accounts": "Cuentas vinculadas",
"account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to {siteName}.", "account.settings.section.linked.accounts.description": "Puedes vincular tus cuentas de redes sociales para simplificar el proceso de iniciar sesión en edX.",
"account.settings.field.username": "Nombre de usuario", "account.settings.field.username": "Nombre de usuario",
"account.settings.field.username.help.text": "The name that identifies you on {siteName}. You cannot change your username.", "account.settings.field.username.help.text": "El nombre que lo identifica en edX. No podrá cambiar el nombre de usuario.",
"account.settings.field.full.name": "Nombre completo", "account.settings.field.full.name": "Nombre completo",
"account.settings.field.full.name.empty": "Añade nombre", "account.settings.field.full.name.empty": "Añade nombre",
"account.settings.field.full.name.help.text": "El nombre que es usado para la verificación de identidad y aparece en sus certificados.", "account.settings.field.full.name.help.text": "El nombre que es usado para la verificación de identidad y aparece en sus certificados.",
"account.settings.field.full.name.help.text.non.certificate": "The name that appears on your public profile.",
"account.settings.field.full.name.help.text.certificate": "This name is selected to appear on your certificates and public-facing records.",
"account.settings.field.name.verified": "Verified name",
"account.settings.field.name.verified.help.text.verified": "This name has been verified by government ID.",
"account.settings.field.name.verified.help.text.certificate": "This name has been verified by government ID and selected to appear on your certificates and public-facing records.",
"account.settings.field.name.verified.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Verified name cannot be changed at this time.",
"account.settings.field.name.verified.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Verified name cannot be changed at this time.",
"account.settings.field.name.verified.verification.help": "Enter your name as it appears on your government-issued ID.",
"account.settings.field.full.name.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Full name cannot be changed at this time.",
"account.settings.field.full.name.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Full name cannot be changed at this time.",
"account.settings.field.name.verified.success.message": "Your identity verification request has successfully completed. You now have the option of selecting which name you prefer to appear on your certificates and public-records.",
"account.settings.field.name.verified.success.message.header": "Your name change request is complete!",
"account.settings.field.name.verified.failure.message": "Your most recent identity verification attempt did not pass. Related account settings have been restored.",
"account.settings.field.name.verified.failure.message.header": "We were not able to verify your identity.",
"account.settings.field.name.verified.failure.message.help.link": "Más información sobre la verificación de ID",
"account.settings.field.name.verified.submitted.message": "Your identity verification request has been submitted and usually takes between 24 and 48 hours to complete.",
"account.settings.field.name.verified.submitted.message.certificate": "When your request is approved, your updated name will appear on all associated certificates and public-facing records.",
"account.settings.field.name.verified.submitted.message.header": "Your name change request is almost complete!",
"account.settings.field.email": "Correo electrónico (Ingresar)", "account.settings.field.email": "Correo electrónico (Ingresar)",
"account.settings.field.email.empty": "Agregar correo electrónico", "account.settings.field.email.empty": "Agregar correo electrónico",
"account.settings.field.email.confirmation": "Le enviamos un mensaje de confirmación a {value}. Hacer click en la liga del mensaje para actualizar su correo electrónico.", "account.settings.field.email.confirmation": "Le enviamos un mensaje de confirmación a {value}. Hacer click en la liga del mensaje para actualizar su correo electrónico.",
"account.settings.field.email.help.text": "You receive messages from {siteName} and course teams at this address.", "account.settings.field.email.help.text": "Recibes mensajes de edX y equipos del curso en esta dirección.",
"account.settings.field.secondary.email": "Correo electrónico de recuperación", "account.settings.field.secondary.email": "Correo electrónico de recuperación",
"account.settings.field.secondary.email.empty": "Agregar un correo electrónico de recuperación", "account.settings.field.secondary.email.empty": "Agregar un correo electrónico de recuperación",
"account.settings.field.secondary.email.confirmation": "Le enviamos un mensaje de confirmación a {value}. Hacer click en la liga del mensaje para actualizar su correo electrónico.", "account.settings.field.secondary.email.confirmation": "Le enviamos un mensaje de confirmación a {value}. Hacer click en la liga del mensaje para actualizar su correo electrónico.",
@@ -75,7 +57,7 @@
"account.settings.field.gender.options.f": "Femenino", "account.settings.field.gender.options.f": "Femenino",
"account.settings.field.gender.options.m": "Masculino", "account.settings.field.gender.options.m": "Masculino",
"account.settings.field.gender.options.o": "Otro", "account.settings.field.gender.options.o": "Otro",
"account.settings.field.language.proficiencies": "Idioma hablado", "account.settings.field.language.proficiencies": "Idiomas hablados",
"account.settings.field.language.proficiencies.empty": "Agregar un idioma hablado", "account.settings.field.language.proficiencies.empty": "Agregar un idioma hablado",
"account.settings.field.language_proficiencies.options.empty": "Selecciona lenguaje", "account.settings.field.language_proficiencies.options.empty": "Selecciona lenguaje",
"account.settings.field.time.zone": "Zona horaria", "account.settings.field.time.zone": "Zona horaria",
@@ -85,7 +67,7 @@
"account.settings.field.time.zone.all": "Todas las zonas horarias", "account.settings.field.time.zone.all": "Todas las zonas horarias",
"account.settings.field.time.zone.country": "Zonas horarias", "account.settings.field.time.zone.country": "Zonas horarias",
"account.settings.section.social.media": "Enlaces de redes sociales", "account.settings.section.social.media": "Enlaces de redes sociales",
"account.settings.section.social.media.description": "Optionally, link your personal accounts to the social media icons on your {siteName} profile.", "account.settings.section.social.media.description": "Opcionalmente, conecte sus cuentas personales a los iconos de redes sociales en su perfil de edX.",
"account.settings.field.social.platform.name.linkedin": "LinkedIn", "account.settings.field.social.platform.name.linkedin": "LinkedIn",
"account.settings.field.social.platform.name.linkedin.empty": "Agregar perfil de LinkedIn", "account.settings.field.social.platform.name.linkedin.empty": "Agregar perfil de LinkedIn",
"account.settings.jump.nav.delete.account": "Eliminar mi cuenta", "account.settings.jump.nav.delete.account": "Eliminar mi cuenta",
@@ -98,12 +80,6 @@
"account.settings.editable.field.action.edit": "Editar", "account.settings.editable.field.action.edit": "Editar",
"account.settings.static.field.empty": "No hay valor establecido. Contacte su administrador {enterprise} para hacer cambios.", "account.settings.static.field.empty": "No hay valor establecido. Contacte su administrador {enterprise} para hacer cambios.",
"account.settings.static.field.empty.no.admin": "No hay valor establecido.", "account.settings.static.field.empty.no.admin": "No hay valor establecido.",
"account.settings.field.name.certificate.select": "If checked, this name will appear on your certificates and public-facing records.",
"account.settings.field.name.modal.certificate.title": "Choose a preferred name for certificates and public-facing records",
"account.settings.field.name.modal.certificate.select": "Select a name",
"account.settings.field.name.modal.certificate.option.full": "Full Name",
"account.settings.field.name.modal.certificate.option.verified": "Verified Name",
"account.settings.field.name.modal.certificate.button.choose": "Choose name",
"account.settings.coaching.consent.welcome.header": "Empecemos", "account.settings.coaching.consent.welcome.header": "Empecemos",
"account.settings.coaching.consent.welcome.subheader": "Estamos aquí para ustede desde el inicio hasta el final", "account.settings.coaching.consent.welcome.subheader": "Estamos aquí para ustede desde el inicio hasta el final",
"account.settings.coaching.consent.description": "Los programas de MicroBachelors incluyen entrenamiento que se enfoca en su carrera, educación y cómo logrará resultados a través de la comunicación individual con un profesional experimentado. Si está interesado, proporcione la información a continuación y haga clic en \"Enviar\", y nuestro socio asesor se comunicará con usted por correo electrónico y / o mensaje de texto para ayudarlo a avanzar. Los términos y Condiciones aplican.*", "account.settings.coaching.consent.description": "Los programas de MicroBachelors incluyen entrenamiento que se enfoca en su carrera, educación y cómo logrará resultados a través de la comunicación individual con un profesional experimentado. Si está interesado, proporcione la información a continuación y haga clic en \"Enviar\", y nuestro socio asesor se comunicará con usted por correo electrónico y / o mensaje de texto para ayudarlo a avanzar. Los términos y Condiciones aplican.*",
@@ -113,7 +89,7 @@
"account.settings.coaching.consent.label.name": "Por favor confirme su nombre", "account.settings.coaching.consent.label.name": "Por favor confirme su nombre",
"account.settings.coaching.consent.label.phone-number": "Ingrese su número de teléfono móvil", "account.settings.coaching.consent.label.phone-number": "Ingrese su número de teléfono móvil",
"account.settings.coaching.consent.success.header": "¡Éxito!", "account.settings.coaching.consent.success.header": "¡Éxito!",
"account.settings.coaching.consent.success.message": "Estás inscrito para coaching. Puedes esperar un mensaje por correo electrónico o SMS en los próximos días.", "account.settings.coaching.consent.success.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
"account.settings.coaching.consent.success.continue": "Iniciar mi curso", "account.settings.coaching.consent.success.continue": "Iniciar mi curso",
"account.settings.coaching.managed.support": "soporte", "account.settings.coaching.managed.support": "soporte",
"account.settings.coaching.managed.alert": "{ManagerTitle} administra su Nombre. Póngase en contacto con su administrador para obtener ayuda.", "account.settings.coaching.managed.alert": "{ManagerTitle} administra su Nombre. Póngase en contacto con su administrador para obtener ayuda.",
@@ -125,72 +101,116 @@
"account.settings.delete.account.before.proceeding": "Antes de continuar, por favor {actionLink}.", "account.settings.delete.account.before.proceeding": "Antes de continuar, por favor {actionLink}.",
"account.settings.delete.account.header": "Eliminar mi cuenta", "account.settings.delete.account.header": "Eliminar mi cuenta",
"account.settings.delete.account.subheader": "¡Sentimos que te vayas!", "account.settings.delete.account.subheader": "¡Sentimos que te vayas!",
"account.settings.delete.account.text.1": "Please note: Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.", "account.settings.delete.account.text.1": "Cuidado: la eliminación de tu cuenta y datos personales es permanente e irreversible. edX no podrá recuperar ni tu cuenta ni los datos eliminados.",
"account.settings.delete.account.text.2": "Once your account is deleted, you cannot use it to take courses on {siteName}.", "account.settings.delete.account.text.2": "Una vez su cuenta haya sido eliminada, no la podrá usar para tomar cursos en la app de edX, edx.org o en cualquier otro sitio administrado por edX. Esto incluye el acceso a edx.org desde el sistema de su empleador o universidad y el acceso a páginas privadas ofrecidas por MIT Open Learning, Wharton Executive Education y Harvard Medical School.",
"account.settings.delete.account.text.2.edX": "Una vez su cuenta haya sido eliminada, no la podrá usar para tomar cursos en la app de edX, edx.org o en cualquier otro sitio administrado por edX. Esto incluye el acceso a edx.org desde el sistema de su empleador o universidad y el acceso a páginas privadas ofrecidas por MIT Open Learning, Wharton Executive Education y Harvard Medical School.", "account.settings.delete.account.text.3.link": "siga las instrucciones para imprimir o descargar el certificado",
"account.settings.delete.account.text.3.link": "Follow these instructions for printing or downloading a certificate", "account.settings.delete.account.text.warning": "Warning: La eliminación de la cuenta es permanente. Por favor lee la información de más arriba con atención antes de proceder. Esta es una acción irreversible, y no podrás volver a usar el mismo correo electrónico en edX.",
"account.settings.delete.account.text.warning": "Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on {siteName}.",
"account.settings.delete.account.text.change.instead": "En lugar de eso, ¿quieres cambiar tu correo electrónico, nombre o contraseña?", "account.settings.delete.account.text.change.instead": "En lugar de eso, ¿quieres cambiar tu correo electrónico, nombre o contraseña?",
"account.settings.delete.account.button": "Eliminar mi cuenta", "account.settings.delete.account.button": "Eliminar mi cuenta",
"account.settings.delete.account.please.activate": "activar su cuenta", "account.settings.delete.account.please.activate": "activar su cuenta",
"account.settings.delete.account.please.unlink": "Desvincular todas las cuentas de redes sociales.", "account.settings.delete.account.please.unlink": "Desvincular todas las cuentas de redes sociales.",
"account.settings.delete.account.modal.header": "¿Está seguro?", "account.settings.delete.account.modal.header": "¿Está seguro?",
"account.settings.delete.account.modal.text.1": "You have selected \"Delete My Account\". Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.", "account.settings.delete.account.modal.text.1": "Has seleccionado “Eliminar mi cuenta”. La eliminación de tu cuenta y datos personales es permanente e irreversible. edX no será capaz de recuperar tu cuenta o los datos que se hayan borrado.",
"account.settings.delete.account.modal.text.2": "If you proceed, you will be unable to use this account to take courses on {siteName}.", "account.settings.delete.account.modal.text.2": "Si procedes, no será posible usar esta cuenta para tomar cursos ni en la aplicación móvil de edX, ni en edx.org, ni en cualquier otro sitio hospedado por edX. Esto incluye el acceso a edx.org desde el sistema de tu empleador o universidad, y el acceso a sitios privados ofrecidos por MIT Open Learning, Wharton Executive Education, y Harvard Medical School.",
"account.settings.delete.account.modal.text.2.edX": "Si procedes, no será posible usar esta cuenta para tomar cursos ni en la aplicación móvil de edX, ni en edx.org, ni en cualquier otro sitio hospedado por edX. Esto incluye el acceso a edx.org desde el sistema de tu empleador o universidad, y el acceso a sitios privados ofrecidos por MIT Open Learning, Wharton Executive Education, y Harvard Medical School.",
"account.settings.delete.account.modal.enter.password": "Si deseas continuar y eliminar tu cuenta, por favor introduce la contraseña de tu cuenta:", "account.settings.delete.account.modal.enter.password": "Si deseas continuar y eliminar tu cuenta, por favor introduce la contraseña de tu cuenta:",
"account.settings.delete.account.modal.confirm.delete": "Si, Eliminar", "account.settings.delete.account.modal.confirm.delete": "Si, Eliminar",
"account.settings.delete.account.modal.confirm.cancel": "Cancelar", "account.settings.delete.account.modal.confirm.cancel": "Cancelar",
"account.settings.delete.account.error.unable.to.delete": "No es posible eliminar esta cuenta", "account.settings.delete.account.error.unable.to.delete": "No es posible eliminar esta cuenta",
"account.settings.delete.account.error.no.password": "Se requiere una contraseña", "account.settings.delete.account.error.no.password": "Se requiere una contraseña",
"account.settings.delete.account.error.invalid.password": "Contraseña incorrecta", "account.settings.delete.account.error.invalid.password": "Password is incorrect",
"account.settings.delete.account.error.unable.to.delete.details": "Ocurrió un error al procesar tu solicitud. Por favor, intente nuevamente más tarde.", "account.settings.delete.account.error.unable.to.delete.details": "Ocurrió un error al procesar tu solicitud. Por favor, intente nuevamente más tarde.",
"account.settings.delete.account.modal.after.header": "¡Sentimos que te vayas! Tu cuenta será eliminada en breve.", "account.settings.delete.account.modal.after.header": "¡Sentimos que te vayas! Tu cuenta será eliminada en breve.",
"account.settings.delete.account.modal.after.text": "La eliminación de cuenta, incluyendo la eliminación de las listas de correo electrónico, puede tardar unas semanas en procesarse totalmente en nuestro sistema. Si quieres renunciar a recibir correos antes de que la eliminación se haya completado, por favor date de baja mediante el enlace que aparece al final de los correos.", "account.settings.delete.account.modal.after.text": "La eliminación de cuenta, incluyendo la eliminación de las listas de correo electrónico, puede tardar unas semanas en procesarse totalmente en nuestro sistema. Si quieres renunciar a recibir correos antes de que la eliminación se haya completado, por favor date de baja mediante el enlace que aparece al final de los correos.",
"account.settings.delete.account.modal.after.button": "Cerrar", "account.settings.delete.account.modal.after.button": "Cerrar",
"account.settings.delete.account.text.3.edX": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. You can make a copy of these for your records before proceeding with deletion. {actionLink}.", "account.settings.delete.account.text.3": "Puede que también pierdas el acceso a los certificados verificados y otros certificados de programas como los de los MicroMasters. Si quieres hacer una copia de dichos certificados para tus archivos antes de proceder a la eliminación, {actionLink}.",
"account.settings.delete.account.text.3": "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.", "account.settings.field.demographics.gender": "Gender identity",
"account.settings.message.demographics.service.issue": "Ocurrió un error al intentar recuperar o guardar la información de tu cuenta. Por favor inténtalo más tarde.", "account.settings.field.demographics.gender.empty": "Add gender identity",
"account.settings.field.demographics.gender": "Identidad de género", "account.settings.field.demographics.gender.options.empty": "Select a gender identity",
"account.settings.field.demographics.gender.empty": "Añade identidad de género", "account.settings.field.demographics.gender.options.woman": "Woman",
"account.settings.field.demographics.gender.options.empty": "Selecciona una identidad de género", "account.settings.field.demographics.gender.options.man": "Man",
"account.settings.field.demographics.gender_description": "Descripción de identidad de género", "account.settings.field.demographics.gender.options.non-binary": "Non-binary",
"account.settings.field.demographics.gender_description.empty": "Ingresa descripción", "account.settings.field.demographics.gender.options.self-describe": "Prefer to self-describe",
"account.settings.field.demographics.ethnicity": "Identidad étnica/raza", "account.settings.field.demographics.gender_description": "Gender identity description",
"account.settings.field.demographics.ethnicity.empty": "Añade identidad étnica/raza", "account.settings.field.demographics.gender_description.empty": "Enter description",
"account.settings.field.demographics.ethnicity.options.empty": "Selecciona todas las que apliquen", "account.settings.field.demographics.ethnicity": "Race/Ethnicity identity",
"account.settings.field.demographics.income": "Ingreso familiar", "account.settings.field.demographics.ethnicity.empty": "Add race/ethnicity identity",
"account.settings.field.demographics.income.empty": "Añade ingreso familiar", "account.settings.field.demographics.ethnicity.options.empty": "Select all that apply",
"account.settings.field.demographics.income.options.empty": "Selecciona un rango de ingreso familiar", "account.settings.field.demographics.ethnicity.options.american-indian-or-alaska-native": "American Indian or Alaska Native",
"account.settings.field.demographics.military_history": "Estatus militar en EE.UU.", "account.settings.field.demographics.ethnicity.options.asian": "Asian",
"account.settings.field.demographics.military_history.empty": "Añade estatus militar", "account.settings.field.demographics.ethnicity.options.black-or-african-american": "Black or African American",
"account.settings.field.demographics.military_history.options.empty": "Selecciona estatus militar", "account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish": "Hispanic, Latin, or Spanish origin",
"account.settings.field.demographics.learner_education_level": "Tu nivel educacional", "account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african": "Middle Eastern or North African",
"account.settings.field.demographics.learner_education_level.empty": "Añade nivel educacional", "account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander": "Native Hawaiian or Other Pacific Islander",
"account.settings.field.demographics.parent_education_level": "Nivel educacional de padres/tutores", "account.settings.field.demographics.ethnicity.options.white": "White",
"account.settings.field.demographics.parent_education_level.empty": "Añade nivel educacional", "account.settings.field.demographics.ethnicity.options.other": "Some other race, ethnicity, or origin",
"account.settings.field.demographics.education_level.options.empty": "Selecciona nivel educacional", "account.settings.field.demographics.income": "Family income",
"account.settings.field.demographics.work_status": "Estatus laboral", "account.settings.field.demographics.income.empty": "Add family income",
"account.settings.field.demographics.work_status.empty": "Añade estatus laboral", "account.settings.field.demographics.income.options.empty": "Select a family income range",
"account.settings.field.demographics.work_status.options.empty": "Selecciona estatus laboral", "account.settings.field.demographics.income.options.less-than-10k": "Less than US $10,000",
"account.settings.field.demographics.work_status_description": "Descripción estatus laboral", "account.settings.field.demographics.income.options.10k-25k": "US $10,000 - $25,000",
"account.settings.field.demographics.work_status_description.empty": "Ingresa descripción", "account.settings.field.demographics.income.options.25k-50k": "US $25,000 - $50,000",
"account.settings.field.demographics.current_work_sector": "Área profesional actual", "account.settings.field.demographics.income.options.50k-75k": "US $50,000 - $75,000",
"account.settings.field.demographics.current_work_sector.empty": "Añade área profesional", "account.settings.field.demographics.income.options.over-100k": "Over US $100,000",
"account.settings.field.demographics.future_work_sector": "Área profesional futura", "account.settings.field.demographics.income.options.unsure": "I don't know",
"account.settings.field.demographics.future_work_sector.empty": "Añade área profesional", "account.settings.field.demographics.military_history": "U.S. Military status",
"account.settings.field.demographics.work_sector.options.empty": "Selecciona área profesional", "account.settings.field.demographics.military_history.empty": "Add military status",
"account.settings.section.demographics.why": "Why does {siteName} collect this information?", "account.settings.field.demographics.military_history.options.empty": "Select military status",
"account.settings.name.change.title.id": "This name change requires identity verification", "account.settings.field.demographics.income.options.never-served": "Never served in the military",
"account.settings.name.change.title.begin": "Before we begin", "account.settings.field.demographics.income.options.training": "Only on active duty for training",
"account.settings.name.change.warning.one": "Warning: This action updates the name that appears on all certificates that have been earned on this account in the past and any certificates you are currently earning or will earn in the future.", "account.settings.field.demographics.income.options.active": "Now on active duty",
"account.settings.name.change.warning.two": "This action cannot be undone without verifying your identity.", "account.settings.field.demographics.income.options.previously-active": "On active duty in the past, but not now",
"account.settings.name.change.id.name.label": "Enter your name as it appears on your government-issued ID.", "account.settings.field.demographics.learner_education_level": "Your education level",
"account.settings.name.change.id.name.placeholder": "Enter the name on your government ID", "account.settings.field.demographics.learner_education_level.empty": "Add education level",
"account.settings.name.change.error.valid.name": "Please enter a valid name.", "account.settings.field.demographics.parent_education_level": "Parents/Guardians education level",
"account.settings.name.change.error.general": "A technical error occurred. Please try again.", "account.settings.field.demographics.parent_education_level.empty": "Add education level",
"account.settings.name.change.continue": "Continue", "account.settings.field.demographics.education_level.options.empty": "Select education level",
"account.settings.name.change.cancel": "Cancel", "account.settings.field.demographics.education_level.options.no-high-school": "No High School",
"account.settings.field.demographics.education_level.options.some-high-school": "Some High School",
"account.settings.field.demographics.education_level.options.high-school-ged-equivalent": "High School diploma, GED, or equivalent",
"account.settings.field.demographics.education_level.options.some-college": "Some college, but no degree",
"account.settings.field.demographics.education_level.options.associates": "Associates degree",
"account.settings.field.demographics.education_level.options.bachelors": "Bachelors degree",
"account.settings.field.demographics.education_level.options.masters": "Masters degree",
"account.settings.field.demographics.education_level.options.professional": "Professional degree",
"account.settings.field.demographics.education_level.options.doctorate": "Doctorate degree",
"account.settings.field.demographics.work_status": "Employment status",
"account.settings.field.demographics.work_status.empty": "Add employment status",
"account.settings.field.demographics.work_status.options.empty": "Select employment status",
"account.settings.field.demographics.work_status.options.full-time": "Employed, working full-time",
"account.settings.field.demographics.work_status.options.part-time": "Employed, working part-time",
"account.settings.field.demographics.work_status.options.not-employed-looking": "Not employed, looking for work",
"account.settings.field.demographics.work_status.options.not-employed-not-looking": "Not employed, not looking for work",
"account.settings.field.demographics.work_status.options.unable": "Unable to work",
"account.settings.field.demographics.work_status.options.retired": "Retired",
"account.settings.field.demographics.work_status.options.other": "Other",
"account.settings.field.demographics.work_status_description": "Employment status description",
"account.settings.field.demographics.work_status_description.empty": "Enter description",
"account.settings.field.demographics.current_work_sector": "Current work industry",
"account.settings.field.demographics.current_work_sector.empty": "Add work industry",
"account.settings.field.demographics.future_work_sector": "Future work industry",
"account.settings.field.demographics.future_work_sector.empty": "Add work industry",
"account.settings.field.demographics.work_sector.options.empty": "Select work industry",
"account.settings.field.demographics.work_sector.options.accommodation-food": "Accommodation and Food Services",
"account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation": "Administrative and Support and Waste Management and Remediation Services",
"account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting": "Agriculture, Forestry, Fishing and Hunting",
"account.settings.field.demographics.work_sector.options.arts-entertainment-recreation": "Arts, Entertainment, and Recreation",
"account.settings.field.demographics.work_sector.options.construction": "Construction",
"account.settings.field.demographics.work_sector.options.educational": "Education Services",
"account.settings.field.demographics.work_sector.options.finance-insurance": "Finance and Insurance",
"account.settings.field.demographics.work_sector.options.healthcare-social": "Health Care and Social Assistance",
"account.settings.field.demographics.work_sector.options.information": "Information",
"account.settings.field.demographics.work_sector.options.management": "Management of Companies and Enterprises",
"account.settings.field.demographics.work_sector.options.manufacturing": "Manufacturing",
"account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas": "Mining, Quarrying, and Oil and Gas Extraction",
"account.settings.field.demographics.work_sector.options.professional-scientific-technical": "Professional, Scientific, and Technical Services",
"account.settings.field.demographics.work_sector.options.public-admin": "Public Administration",
"account.settings.field.demographics.work_sector.options.real-estate": "Real Estate and Rental and Leasing",
"account.settings.field.demographics.work_sector.options.retail": "Retail Trade",
"account.settings.field.demographics.work_sector.options.transport-warehousing": "Transportation and Warehousing",
"account.settings.field.demographics.work_sector.options.utilities": "Utilities",
"account.settings.field.demographics.work_sector.options.trade": "Wholesale Trade",
"account.settings.field.demographics.work_sector.options.other": "Other",
"account.settings.field.demographics.options.declined": "Prefer not to respond",
"error.notfound.message": "La página que estas buscando no está disponible o hay un error en la URL. Por favor, comprueba la URL y vuelve a intentarlo.", "error.notfound.message": "La página que estas buscando no está disponible o hay un error en la URL. Por favor, comprueba la URL y vuelve a intentarlo.",
"account.settings.editable.field.password.reset.button.confirmation.support.link": "soporte técnico", "account.settings.editable.field.password.reset.button.confirmation.support.link": "soporte técnico",
"account.settings.editable.field.password.reset.button.confirmation": "Hemos mandado un mensaje a {email}. Haz clic en el enlace en el mensaje para restablecer tu contraseña. ¿No recibiste el mensaje? Contáctate con {technicalSupportLink}.", "account.settings.editable.field.password.reset.button.confirmation": "Hemos mandado un mensaje a {email}. Haz clic en el enlace en el mensaje para restablecer tu contraseña. ¿No recibiste el mensaje? Contáctate con {technicalSupportLink}.",
@@ -202,162 +222,75 @@
"account.settings.sso.account.disconnect.error": "Hubo un problema al desconectar esta Cuenta. Si el problema persiste, contacte soporte.", "account.settings.sso.account.disconnect.error": "Hubo un problema al desconectar esta Cuenta. Si el problema persiste, contacte soporte.",
"account.settings.sso.unlink.account": "Desvincular la cuenta de {accountName} ", "account.settings.sso.unlink.account": "Desvincular la cuenta de {accountName} ",
"account.settings.sso.no.providers": "No se pueden vincular cuentas en este momento.", "account.settings.sso.no.providers": "No se pueden vincular cuentas en este momento.",
"id.verification.access.blocked.denied": "No puedes verificar tu identidad en este momento. Si aún tienes que activar tu cuenta, revisa tu carpeta de correo no deseado y busca el correo electrónico de activación de {email}.", "id.verification.next": "Next",
"id.verification.next": "Siguiente", "id.verification.requirements.title": "Photo Verification Requirements",
"id.verification.support": "soporte", "id.verification.requirements.description": "In order to complete Photo Verification online, you will need the following:",
"id.verification.continue.upload": "Continue with Upload", "id.verification.requirements.card.device.title": "Device with Camera",
"id.verification.example.card.alt": "Ejemplo de un documento de identidad válido con foto y nombre completo.", "id.verification.requirements.card.device.allow": "Allow",
"id.verification.requirements.title": "Requerimientos de verificación por foto", "id.verification.requirements.card.id.title": "Photo Identification",
"id.verification.requirements.description": "In order to complete Photo Verification, you will need the following:", "id.verification.requirements.card.id.text": "You need a valid ID that contains your full name and photo.",
"id.verification.requirements.card.device.title": "Dispositivo con cámara", "id.verification.privacy.title": "Privacy Information",
"id.verification.requirements.card.device.allow": "Permitir", "id.verification.privacy.need.photo.question": "Why does edX need my photo?",
"id.verification.requirements.card.id.title": "Photo Identification Card", "id.verification.privacy.need.photo.answer": "We use your verification photos to confirm your identity and ensure the validity of your certificate.",
"id.verification.requirements.card.id.text": "You need a valid identification card that contains your full name and photo, such as a drivers license or passport.", "id.verification.privacy.do.with.photo.question": "What does edX do with this photo?",
"id.verification.privacy.title": "Información de privacidad", "id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
"id.verification.privacy.need.photo.question": "Why does {siteName} need my photo?", "id.verification.existing.request.title": "Identity Verification",
"id.verification.privacy.need.photo.answer": "Utilizamos tus fotos de verificación para confirmar tu identidad y garantizar la validez de tu certificado.", "id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).",
"id.verification.privacy.do.with.photo.question": "What does {siteName} do with this photo?", "id.verification.existing.request.denied.text": "You cannot verify your identity at this time.",
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on {siteName} after the verification process is complete.", "id.verification.photo.take": "Take Photo",
"id.verification.access.blocked.title": "Verificación de identidad", "id.verification.photo.retake": "Retake Photo",
"id.verification.access.blocked.enrollment": "Actualmente, no estás inscrito en un curso que requiera verificación de identidad.", "id.verification.camera.access.title": "Camera Permissions",
"id.verification.access.blocked.pending": "Ya has enviado tu información de verificación de identidad. Recibirás un mensaje en tu panel principal cuando el proceso de verificación esté completado (usualmente dentro de los 5 días).", "id.verification.camera.access.click.allow": "Please make sure to click \"Allow\"",
"id.verification.photo.take": "Tomar la foto", "id.verification.camera.access.enable": "Enable Camera",
"id.verification.photo.retake": "¿Tomar nuevamente la foto?", "id.verification.camera.access.problems": "Having problems?",
"id.verification.photo.enable.detection": "Habilitar la detección de rostro", "id.verification.camera.access.skip": "Skip and upload image files instead",
"id.verification.photo.enable.detection.portrait.help.text": "Si está marcada, aparecerá un cuadro alrededor de tu cara. Tu rostro se puede ver claramente si el cuadro que lo rodea es azul. Si Tu cara no está en una buena posición o es indetectable, el cuadro será rojo.", "id.verification.camera.access.success": "Looks like your camera is working and ready.",
"id.verification.photo.enable.detection.id.help.text": "Si está marcada, aparecerá una casilla alrededor de la cara de tu documento de identificación. La cara se puede ver claramente si la caja que la rodea es azul. Si la cara no está en una buena posición o es indetectable, el cuadro será rojo.", "id.verification.camera.access.failure": "It looks like we're unable to access your camera. You will need to upload image files of you and your photo id.",
"id.verification.photo.feedback.correct": "La cara está en una buena posición.", "id.verification.photo.tips.title": "Helpful Photo Tips",
"id.verification.photo.feedback.two.faces": "Más de un rostro detectado.", "id.verification.photo.tips.description": "Next, we'll need you to take a photo of your face. Please review the helpful tips below.",
"id.verification.photo.feedback.no.faces": "No se detectó el rostro.", "id.verification.photo.tips.list.title": "Photo Tips",
"id.verification.photo.feedback.top.left": "Posición incorrecta. Arriba a la izquierda.", "id.verification.photo.tips.list.description": "To take a successful photo, make sure that:",
"id.verification.photo.feedback.top.center": "Posición incorrecta. Centro Superior.", "id.verification.photo.tips.list.well.lit": "Your face is well-lit.",
"id.verification.photo.feedback.top.right": "Posición incorrecta. Parte superior derecha.", "id.verification.photo.tips.list.inside.frame": "Your entire face fits inside the frame.",
"id.verification.photo.feedback.center.left": "Posición incorrecta. Centro izquierda.", "id.verification.portrait.photo.title.camera": "Take Your Photo",
"id.verification.photo.feedback.center.center": "Posición incorrecta. Demasiado cerca de la cámara.", "id.verification.portrait.photo.title.upload": "Upload Your Portrait Photo",
"id.verification.photo.feedback.center.right": "Posición incorrecta. Centro derecha.", "id.verification.portrait.photo.preview.alt": "Preview of photo of user's face.",
"id.verification.photo.feedback.bottom.left": "Posición incorrecta. Abajo a la izquierda.", "id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.",
"id.verification.photo.feedback.bottom.center": "Posición incorrecta. Parte inferior central.", "id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.photo.feedback.bottom.right": "Posición incorrecta. Abajo a la derecha.", "id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?",
"id.verification.camera.access.title": "Permisos de la cámara", "id.verification.camera.help.sight.answer": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.access.title.success": "Acceso a la cámara habilitado", "id.verification.camera.help.head.question": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.access.title.failed": "Acceso a la cámara no habilitado", "id.verification.camera.help.head.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.camera.access.click.allow": "Por favor asegúrate de hacer clic en \"Permitir\"", "id.verification.id.tips.title": "Helpful ID Tips",
"id.verification.camera.access.enable": "Habilitar cámara", "id.verification.id.tips.description": "Next you'll need an eligible ID photo, make sure that:",
"id.verification.camera.access.problems": "¿Tienes problemas?", "id.verification.id.tips.list.well.lit": "Your ID is well-lit.",
"id.verification.camera.access.skip": "Omitir y cargar un archivo de imagen", "id.verification.id.tips.list.clear": "Ensure that you can see your photo and clearly read your name.",
"id.verification.camera.access.success": "Parece que tu cámara está funcionando y está lista.", "id.verification.id.photo.title.camera": "Take ID Photo",
"id.verification.camera.access.failure": "Parece que no podemos acceder a tu cámara. Deberás cargar archivos de imagen de tu rostro y tu identificación con foto.", "id.verification.id.photo.title.upload": "Upload Your ID Photo",
"id.verification.camera.access.failure.temporary": "Parece que no podemos acceder a tu cámara. Verifica que tu cámara web esté conectada y que permitiste que tu navegador acceda a ella.", "id.verification.id.photo.preview.alt": "Preview of photo ID.",
"id.verification.camera.access.failure.temporary.chrome": "Para habilitar el acceso a la cámara en Chrome:", "id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo.",
"id.verification.camera.access.failure.temporary.chrome.step1": "Abrir Chrome.", "id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.camera.access.failure.temporary.chrome.step2": "Navega a Más > Configuración.", "id.verification.account.name.title": "Account Name Check",
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "Para Windows: Alt + F, Alt + E o F10 seguido de la barra espaciadora", "id.verification.account.name.instructions": "Please check the Account Name below to ensure it matches the name on your ID. If not, click \"Edit\".",
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "Para Mac: Comando +,", "id.verification.account.name.warning.prefix": "Please Note:",
"id.verification.camera.access.failure.temporary.chrome.step3": "En la pestaña \"Privacidad y seguridad\", selecciona \"Configuración del sitio\" y luego \"Cámara.\"", "id.verification.account.name.settings": "Account Settings",
"id.verification.camera.access.failure.temporary.chrome.step4": "En \"Bloqueado\", busca \"edx.org\" y selecciónalo.", "id.verification.account.name.label": "Name on ID",
"id.verification.camera.access.failure.temporary.chrome.step5": "En la sección \"Permisos\", actualiza los permisos de la cámara a \"Permitir\".", "id.verification.account.name.edit": "Edit",
"id.verification.camera.access.failure.temporary.ie11": "Para habilitar el acceso a la cámara en Internet Explorer:", "id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
"id.verification.camera.access.failure.temporary.ie11.step1": "Abre el Administrador de configuración de Flash Player navegando a Configuración de Windows > Panel de control > Flash Player.", "id.verification.account.name.save": "Save",
"id.verification.camera.access.failure.temporary.ie11.step2": "Selecciona la pestaña \"Cámara y micrófono,\" y luego selecciona el botón \"Configuración de cámara y micrófono por sitio.\"", "id.verification.review.title": "Review Your Photos",
"id.verification.camera.access.failure.temporary.ie11.step3": "Elige \"edx.org\" de la lista de sitios web y cambia los permisos seleccionando \"Permitir\" en el menú desplegable.", "id.verification.review.description": "Make sure we can verify your identity with the photos and information you have provided.",
"id.verification.camera.access.failure.temporary.firefox": "Para habilitar el acceso a la cámara en Firefox:", "id.verification.review.portrait.label": "Your Portrait",
"id.verification.camera.access.failure.temporary.firefox.step1": "Abrir Firefox.", "id.verification.review.portrait.alt": "Photo of your face to be submitted.",
"id.verification.camera.access.failure.temporary.firefox.step2": "Coloca \"about:preferences\" en la barra del URL.", "id.verification.review.portrait.retake": "Retake Portrait Photo",
"id.verification.camera.access.failure.temporary.firefox.step3": "Selecciona la pestaña \"Privacidad y seguridad\" y navega hasta la sección \"Permisos.\"", "id.verification.review.id.label": "Your Photo ID",
"id.verification.camera.access.failure.temporary.firefox.step4": "Junto a \"Cámara,\" selecciona el botón \"Configuración...\"", "id.verification.review.id.alt": "Photo of your ID to be submitted.",
"id.verification.camera.access.failure.temporary.firefox.step5": "En la barra de búsqueda, ingresa \"edx.org\".", "id.verification.review.id.retake": "Retake ID Photo",
"id.verification.camera.access.failure.temporary.firefox.step6": "En la columna de estado de \"edx.org\", selecciona \"Permitir\" en el menú desplegable.", "id.verification.review.confirm": "Confirm",
"id.verification.camera.access.failure.temporary.firefox.step7": "Selecciona \"Guardar cambios.\"", "id.verification.submitted.title": "Identity Verification in Progress",
"id.verification.camera.access.failure.temporary.safari": "Para habilitar el acceso a la cámara en Safari:", "id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days). In the meantime, you can still access all available course content.",
"id.verification.camera.access.failure.temporary.safari.step1": "Abrir Safari.", "id.verification.submitted.return": "Return to Your Dashboard",
"id.verification.camera.access.failure.temporary.safari.step2": "Haz clic en el menú de la aplicación Safari y luego selecciona \"Preferencias\". También puedes utilizar Command +, como método abreviado de teclado.", "id.verification.account.name.warning": "{prefix} Any edit to your name will be saved to your account and can be reviewed on {accountSettings}.",
"id.verification.camera.access.failure.temporary.safari.step3": "Selecciona la pestaña \"Sitios web\" y luego selecciona \"Cámara\".", "id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
"id.verification.camera.access.failure.temporary.safari.step4": "Selecciona \"edx.org\" y cambia los permisos de la cámara a \"Permitir.\"", "id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}."
"id.verification.camera.access.failure.unsupported": "It looks like your browser does not support camera access.",
"id.verification.camera.access.failure.unsupported.chrome.explanation": "The Chrome browser currently does not support camera access on iOS devices, such as iPhones and iPads.",
"id.verification.camera.access.failure.unsupported.instructions": "Please use another browser to complete Identity Verification.",
"id.verification.photo.tips.title": "Consejos útiles de fotos",
"id.verification.photo.tips.description": "A continuación, necesitaremos que tomes una foto de tu rostro. Por favor, revisa los siguientes consejos útiles.",
"id.verification.photo.tips.list.title": "Consejos para fotos",
"id.verification.photo.tips.list.description": "Para tomar la foto correctamente, asegúrate de: ",
"id.verification.photo.tips.list.well.lit": "El rostro esté bien iluminado",
"id.verification.photo.tips.list.inside.frame": "Tu cara está completamente dentro del marco de la foto.",
"id.verification.portrait.photo.title.camera": "Toma una foto de ti mismo",
"id.verification.portrait.photo.title.upload": "Sube una foto tuya",
"id.verification.portrait.photo.preview.alt": "Previsualización de la foto con el rostro del usuario",
"id.verification.portrait.photo.instructions.camera": "Cuando tu rostro esté en posición, usa el botón Tomar foto a continuación para tomar tu foto.",
"id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. Supported formats: ",
"id.verification.camera.help.sight.question": "¿Qué pasa si no puedo ver la imagen de la cámara o si no puedo ver mi foto para determinar qué lado es visible?",
"id.verification.camera.help.sight.answer.portrait": "Es posible que puedas completar el procedimiento de captura de imágenes sin ayuda, pero es posible que necesites un par de intentos de envío para que la cámara se coloque correctamente. La posición óptima de la cámara varía con cada computadora, pero generalmente la mejor distancia para una foto de rostro es aproximadamente a 12 a 18 pulgadas (30 a 45 centímetros) de la cámara, con la cabeza centrada en relación con la pantalla de la computadora. Si las fotos que envías son rechazadas, intenta mover la computadora o la orientación de la cámara para cambiar el ángulo de iluminación.",
"id.verification.camera.help.sight.answer.id": "Es posible que puedas completar el procedimiento de captura de imágenes sin ayuda, pero es posible que necesites un par de intentos de envío para que la cámara se coloque correctamente. El posicionamiento óptimo de la cámara varía con cada computadora pero, generalmente, la mejor distancia para una foto de un documento de identificación es a 8 a 12 pulgadas (20 a 30 centímetros) de la cámara, con el documento de identificación centrado en relación con la cámara. Si las fotos que envías son rechazadas, intenta mover la computadora o la orientación de la cámara para cambiar el ángulo de iluminación. La razón más común de rechazo es la imposibilidad de leer el texto del documento de identidad.",
"id.verification.camera.help.difficulty.question.portrait": "¿Qué sucede si tengo dificultades para mantener la cabeza en posición con respecto a la cámara?",
"id.verification.camera.help.difficulty.question.id": "¿Qué sucede si tengo dificultades para mantener mi identificación en posición con respecto a la cámara?",
"id.verification.camera.help.difficulty.answer": "If you require assistance with taking a photo for submission, contact {siteName} support for additional suggestions.",
"id.verification.camera.help.upload.question": "What if I want to upload a photo instead?",
"id.verification.camera.help.upload.answer": "On the next page you will have the option to switch to upload mode. By selecting that option, you will be able to upload a photo instead.",
"id.verification.id.photo.unclear.question": "Is your ID card image not clear or too blurry?",
"id.verification.id.tips.title": "Helpful Identification Card Tips",
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid identification card that includes your full name and photo, such as a drivers license or passport. Please have your ID ready.",
"id.verification.id.tips.list.well.lit": "Your identification card is well-lit.",
"id.verification.id.tips.list.clear": "Asegúrate de que puedes ver tu foto y leer claramente tu nombre.",
"id.verification.id.photo.title.camera": "Take a Photo of Your Identification Card",
"id.verification.id.photo.title.upload": "Upload a Photo of Your Identification Card",
"id.verification.id.photo.preview.alt": "Previsualización de Foto ID",
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo. Please use a passport, drivers license, or another identification card that includes your full name and a picture of your face.",
"id.verification.id.photo.instructions.upload": "Please upload a photo of your identification card. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ",
"id.verification.id.photo.instructions.upload.error.invalidFileType": "The file you have selected is not a supported image type. Please choose from the following formats: ",
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "El archivo que has seleccionado es demasiado grande. Vuelve a intentarlo con un archivo de menos de 10 MB.",
"id.verification.account.name.title": "Verificación de nombre de cuenta",
"id.verification.name.check.title": "Double-Check Your Name",
"id.verification.account.name.instructions": "El nombre de tu cuenta y el nombre de tu identificación deben coincidir exactamente. De lo contrario, haz clic en \"No\" para actualizar el nombre de tu cuenta.",
"id.verification.name.check.instructions": "Does the name below match the name on your government-issued ID? If not, update the name below to match your goverment-issued ID.",
"id.verification.name.check.mismatch.information": "If the name below does not match your government-issued ID, your identity verification will be denied.",
"id.verification.account.name.radio.label": "¿El nombre de tu identificación coincide con el nombre de la cuenta a continuación?",
"id.verification.name.check.radio.label": "Select an option",
"id.verification.account.name.radio.yes": "Si",
"id.verification.name.check.radio.yes": "Yes, the name below matches my ID",
"id.verification.account.name.radio.no": "No",
"id.verification.name.check.radio.no": "No, the name below does not match my ID",
"id.verification.account.name.error": "Actualiza el nombre de la cuenta para que coincida con el nombre de tu identificación.",
"id.verification.account.name.warning.prefix": "Ten en cuenta:",
"id.verification.account.name.settings": "Configuración de cuenta",
"id.verification.account.name.label": "Nombre de la cuenta",
"id.verification.name.label": "Name",
"id.verification.account.name.photo.alt": "Foto de tu identificación a enviar.",
"id.verification.account.name.save": "Guardar y siguiente",
"id.verification.review.title": "Revisar tus fotos",
"id.verification.review.description": "Asegúrate de que podamos verificar tu identidad con las imágenes y la información suministradas.",
"id.verification.review.portrait.label": "Tu Retrato",
"id.verification.review.portrait.alt": "Foto de tu rostro a enviar.",
"id.verification.review.portrait.retake": "Retomar Foto de Retrato",
"id.verification.review.id.label": "Your Identification Card",
"id.verification.review.id.alt": "Photo of your identification card to be submitted.",
"id.verification.review.id.retake": "Retomar Foto de identificación",
"id.verification.review.confirm": "Enviar",
"id.verification.submission.alert.error.face": "Se requiere una foto de tu rostro. Vuelve a tomar tu foto de retrato.",
"id.verification.submission.alert.error.id": "Se requiere una foto de tu documento de ID. Vuelve a tomar tu foto de ID.",
"id.verification.submission.alert.error.name": "Se requiere un nombre de cuenta válido. Actualiza el nombre de tu cuenta para que coincida con el nombre que figura en tu ID.",
"id.verification.submission.alert.error.unsupported": "One or more of the files you have uploaded is in an unsupported format. Please choose from the following: ",
"id.verification.review.error": "{siteName} Support Page",
"id.verification.submitted.title": "Verificación de identidad en progreso.",
"id.verification.submitted.text": "Hemos recibido tu información y estamos verificando tu identidad. Verás un mensaje en tu tablero cuando se complete el proceso de verificación (generalmente en un periodo de 5 días). Mientras tanto, aún puedes acceder a todo el contenido del curso disponible.",
"id.verification.return.dashboard": "Volver al panel principal",
"id.verification.return.course": "Regresar al curso",
"id.verification.photo.upload.help.title": "Upload a Photo Instead",
"id.verification.photo.camera.help.title": "Use Your Camera Instead",
"id.verification.photo.upload.help.text": "If you are having trouble using the photo capture above, you may want to upload a photo instead. To upload a photo, click the button below.",
"id.verification.photo.camera.help.text": "If you are having trouble uploading a photo above, you may want to use your camera instead. To use your camera, click the button below.",
"id.verification.upload.help.button": "Switch to Upload Mode",
"id.verification.camera.help.button": "Switch to Camera Mode",
"id.verification.choose.mode.title": "Photo Requirements Options",
"id.verification.choose.mode.hep.text": "To complete verification, please select one of the following options to submit photos. You will be able to switch between these options throughout the process if needed.",
"id.verification.choose.mode.radio.upload": "Upload photos from my device",
"id.verification.choose.mode.radio.camera": "Take pictures using my camera",
"id.verification.account.name.managed.alert": "Your profile settings are managed by {managerTitle}, so you are not allowed to update your name. Please contact your {profileDataManager} administrator or {support} for help.",
"id.verification.request.camera.access.instructions": "Para tomar una foto con tu cámara web, es posible que recibas un aviso del navegador para acceder a tu cámara. {clickAllow}",
"id.verification.requirements.account.managed.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help before completing the Photo Verification process.",
"id.verification.requirements.card.device.text": "Necesitas un dispositivo que tenga una cámara. Si has recibido un aviso del navegador para habilitar acceso a tu cámara, por favor asegúrate de seleccionar [allow].",
"id.verification.account.name.summary.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help.",
"idv.submission.alert.error": "\n Se produjo un error técnico al intentar enviar la verificación de ID.\n Es posible que sea una cuestión temporal, así que inténtalo de nuevo en unos minutos.\n Si el problema continúa, dirígete a {support_link} para obtener ayuda.\n ",
"id.verification.account.name.edit": "Editar {sr}"
} }

View File

@@ -1,363 +1,296 @@
{ {
"account.settings.message.duplicate.tpa.provider": "Le compte {provider} que vous avez sélectionné est déjà lié à un autre compte {siteName}.", "account.settings.message.duplicate.tpa.provider": "The {provider} account you selected is already linked to another edX account.",
"account.settings.message.managed.settings": "Vos paramètres de profile sont gérés par {managerTitle}. Contactez votre administrateur ou {support} en cas de besoin.", "account.settings.message.managed.settings": "Your profile settings are managed by {managerTitle}. Contact your administrator or {support} for help.",
"account.settings.message.managed.settings.support": "support", "account.settings.message.managed.settings.support": "support",
"account.settings.page.heading": "Paramètres du compte", "account.settings.page.heading": "Account Settings",
"account.settings.loading.message": "Chargement...", "account.settings.loading.message": "Loading...",
"account.settings.loading.error": "Erreur : {error}", "account.settings.loading.error": "Error: {error}",
"account.settings.banner.beta.language": "Vous avez réglé votre langue sur {beta_language}, qui n'est actuellement pas entièrement traduite. Vous pouvez nous aider à traduire complètement cette langue en rejoignant la communauté Transifex et en ajoutant des traductions de l'anglais pour les apprenants qui parlent {beta_language}.", "account.settings.banner.beta.language": "You have set your language to {beta_language}, which is currently not fully translated. You can help us translate this language fully by joining the Transifex community and adding translations from English for learners that speak {beta_language}.",
"account.settings.banner.beta.language.action.switch.back": "Revenir au {previous_language}", "account.settings.banner.beta.language.action.switch.back": "Switch Back to {previous_language}",
"account.settings.banner.beta.language.action.help.translate": "Aidez-nous à traduire en {beta_language}", "account.settings.banner.beta.language.action.help.translate": "Help Translate into {beta_language}",
"account.settings.section.account.information": "Information du compte", "account.settings.section.account.information": "Account Information",
"account.settings.section.account.information.description": "Ces paramètres comprennent des informations de base à propos de votre compte.", "account.settings.section.account.information.description": "These settings include basic information about your account.",
"account.settings.section.profile.information": "Informations de profil", "account.settings.section.profile.information": "Profile Information",
"account.settings.section.demographics.information": "Informations optionnelles", "account.settings.section.demographics.information": "Optional Information",
"account.settings.section.site.preferences": "Préférences de site", "account.settings.section.site.preferences": "Site Preferences",
"account.settings.section.linked.accounts": "Comptes liés", "account.settings.section.linked.accounts": "Linked Accounts",
"account.settings.section.linked.accounts.description": "Vous pouvez associer vos comptes d'identité pour simplifier la connexion à {siteName}.", "account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to edX.",
"account.settings.field.username": "Nom dutilisateur", "account.settings.field.username": "Username",
"account.settings.field.username.help.text": "Le nom qui vous identifie sur {siteName}. Vous ne pouvez pas changer votre nom d'utilisateur.", "account.settings.field.username.help.text": "The name that identifies you on edX. You cannot change your username.",
"account.settings.field.full.name": "Nom complet", "account.settings.field.full.name": "Full name",
"account.settings.field.full.name.empty": "Ajouter un nom", "account.settings.field.full.name.empty": "Add name",
"account.settings.field.full.name.help.text": "Le nom qui apparaîtra sur vos certificats et dans le cadre de toute vérification d'identité.", "account.settings.field.full.name.help.text": "The name that is used for ID verification and that appears on your certificates.",
"account.settings.field.full.name.help.text.non.certificate": "Le nom qui apparaît sur votre profile public.", "account.settings.field.email": "Email address (Sign in)",
"account.settings.field.full.name.help.text.certificate": "Ce nom va apparaître sur vos attestations et dossiers publics.", "account.settings.field.email.empty": "Add email address",
"account.settings.field.name.verified": "Nom vérifié", "account.settings.field.email.confirmation": "Weve sent a confirmation message to {value}. Click the link in the message to update your email address.",
"account.settings.field.name.verified.help.text.verified": "Ce nom a été vérifié par une pièce d'identité gouvernementale.", "account.settings.field.email.help.text": "You receive messages from edX and course teams at this address.",
"account.settings.field.name.verified.help.text.certificate": "This name has been verified by government ID and selected to appear on your certificates and public-facing records.", "account.settings.field.secondary.email": "Recovery email address",
"account.settings.field.name.verified.help.text.submitted": "La vérification a été soumise. Cela prend généralement 48 heures ou moins. Le nom vérifié ne peut pas être modifié pour le moment.", "account.settings.field.secondary.email.empty": "Add a recovery email address",
"account.settings.field.name.verified.help.text.submitted.certificate": "Une fois la vérification d'identité réussie, ce nom apparaîtra sur vos attestations et vos dossiers publics. Le nom complet ne peut pas être modifié pour le moment.", "account.settings.field.secondary.email.confirmation": "Weve sent a confirmation message to {value}. Click the link in the message to update your recovery email address.",
"account.settings.field.name.verified.verification.help": "Enter your name as it appears on your government-issued ID.", "account.settings.email.field.confirmation.header": "Pending confirmation",
"account.settings.field.full.name.help.text.submitted": "La vérification a été soumise. Cela prend généralement 48 heures ou moins. Le nom vérifié ne peut pas être modifié pour le moment.", "account.settings.field.dob": "Year of birth",
"account.settings.field.full.name.help.text.submitted.certificate": "Une fois la vérification d'identité réussie, ce nom apparaîtra sur vos attestations et vos dossiers publics. Le nom complet ne peut pas être modifié pour le moment.", "account.settings.field.dob.empty": "Add year of birth",
"account.settings.field.name.verified.success.message": "Votre demande de vérification d'identité a été complétée avec succès. Vous avez maintenant la possibilité de sélectionner le nom que vous préférez voir apparaître sur vos attestations et documents publics.", "account.settings.field.year_of_birth.options.empty": "Select a year of birth",
"account.settings.field.name.verified.success.message.header": "Votre demande de changement de nom est terminée!", "account.settings.field.country": "Country",
"account.settings.field.name.verified.failure.message": "Votre dernière tentative de vérification d'identité n'a pas abouti. Les paramètres du compte correspondant ont été restaurés.", "account.settings.field.country.empty": "Add country",
"account.settings.field.name.verified.failure.message.header": "Nous n'avons pas pu vérifier votre identité.", "account.settings.field.country.options.empty": "Select a Country",
"account.settings.field.name.verified.failure.message.help.link": "En savoir plus sur la vérification d'identité", "account.settings.field.state": "State",
"account.settings.field.name.verified.submitted.message": "Votre demande de vérification d'identité a bien été envoyée et prend généralement entre 24 et 48 heures afin d'être validée.", "account.settings.field.state.empty": "Add state",
"account.settings.field.name.verified.submitted.message.certificate": "Lorsque votre demande est approuvée, votre nouveau nom apparaîtra sur tous les certificats associés et les enregistrements publics.", "account.settings.field.state.options.empty": "Select a State",
"account.settings.field.name.verified.submitted.message.header": "Votre demande de changement de nom est presque terminée!", "account.settings.field.site.language": "Site language",
"account.settings.field.email": "Adresse e-mail (Connexion)", "account.settings.field.site.language.help.text": "The language used throughout this site. This site is currently available in a limited number of languages.",
"account.settings.field.email.empty": "Ajouter l'adresse e-mail",
"account.settings.field.email.confirmation": "Un message de confirmation a été envoyé à {value}. Cliquer le lien dans le message afin de mettre à jour votre adresse email.",
"account.settings.field.email.help.text": "Vous recevrez des messages de {siteName} et des équipes pédagogiques à cette adresse.",
"account.settings.field.secondary.email": "Adresse électronique de récupération",
"account.settings.field.secondary.email.empty": "Ajouter une adresse électronique de récupération",
"account.settings.field.secondary.email.confirmation": "Un message de confirmation a été envoyé à {value}. Cliquer le lien dans le message afin de mettre à jour votre adresse email.",
"account.settings.email.field.confirmation.header": "En attente de confirmation",
"account.settings.field.dob": "Année de naissance",
"account.settings.field.dob.empty": "Ajouter année de naissance",
"account.settings.field.year_of_birth.options.empty": "Sélectionnez une année de naissance",
"account.settings.field.country": "Pays",
"account.settings.field.country.empty": "Ajouter un pays",
"account.settings.field.country.options.empty": "Sélectionnez un pays",
"account.settings.field.state": "État",
"account.settings.field.state.empty": "Ajouter un état",
"account.settings.field.state.options.empty": "Choisir un état",
"account.settings.field.site.language": "Langue du site",
"account.settings.field.site.language.help.text": "La langue utilisée au travers du site. Le site est actuellement disponible dans un nombre limité de langues.",
"account.settings.field.education": "Education", "account.settings.field.education": "Education",
"account.settings.field.education.empty": "Ajouter le niveau d'éducation", "account.settings.field.education.empty": "Add level of education",
"account.settings.field.education.levels.empty": "Sélectionnez un niveau d'éducation", "account.settings.field.education.levels.empty": "Select a level of education",
"account.settings.field.education.levels.p": "Doctorat", "account.settings.field.education.levels.p": "Doctorate",
"account.settings.field.education.levels.m": "Master ou diplôme professionnel", "account.settings.field.education.levels.m": "Master's or professional degree",
"account.settings.field.education.levels.b": "Diplôme de licence", "account.settings.field.education.levels.b": "Bachelor's Degree",
"account.settings.field.education.levels.a": "Grade de l'associé", "account.settings.field.education.levels.a": "Associate's degree",
"account.settings.field.education.levels.hs": "Lycée / enseignement secondaire", "account.settings.field.education.levels.hs": "Secondary/high school",
"account.settings.field.education.levels.jhs": "Collège / enseignement secondaire inférieur", "account.settings.field.education.levels.jhs": "Junior secondary/junior high/middle school",
"account.settings.field.education.levels.el": "Enseignement primaire", "account.settings.field.education.levels.el": "Elementary/primary school",
"account.settings.field.education.levels.none": "Sans diplôme", "account.settings.field.education.levels.none": "No formal education",
"account.settings.field.education.levels.o": "Autre niveau d'étude", "account.settings.field.education.levels.o": "Other education",
"account.settings.field.gender": "Genre", "account.settings.field.gender": "Gender",
"account.settings.field.gender.empty": "Ajouter le genre", "account.settings.field.gender.empty": "Add gender",
"account.settings.field.gender.options.empty": "Sélectionner un genre", "account.settings.field.gender.options.empty": "Select a gender",
"account.settings.field.gender.options.f": "Femme", "account.settings.field.gender.options.f": "Female",
"account.settings.field.gender.options.m": "Homme", "account.settings.field.gender.options.m": "Male",
"account.settings.field.gender.options.o": "Autre", "account.settings.field.gender.options.o": "Other",
"account.settings.field.language.proficiencies": "Langue parlée", "account.settings.field.language.proficiencies": "Spoken languages",
"account.settings.field.language.proficiencies.empty": "Ajouter une langue parlée", "account.settings.field.language.proficiencies.empty": "Add a spoken language",
"account.settings.field.language_proficiencies.options.empty": "Sélectionnez une langue", "account.settings.field.language_proficiencies.options.empty": "Select a Language",
"account.settings.field.time.zone": "Fuseau horaire", "account.settings.field.time.zone": "Time zone",
"account.settings.field.time.zone.empty": "Définir le fuseau horaire", "account.settings.field.time.zone.empty": "Set time zone",
"account.settings.field.time.zone.description": "Sélectionnez le fuseau horaire pour l'affichage des dates de cours. Si vous n'indiquez aucun fuseau horaire, les dates de cours, y compris les échéances de devoirs, seront affichés en fonction du fuseau horaire local de votre navigateur. ", "account.settings.field.time.zone.description": "Select the time zone for displaying course dates. If you do not specify a time zone, course dates, including assignment deadlines, will be displayed in your browsers local time zone.",
"account.settings.field.time.zone.default": "Défaut (fuseau horaire local)", "account.settings.field.time.zone.default": "Default (Local Time Zone)",
"account.settings.field.time.zone.all": "Tous les fuseaux horaires", "account.settings.field.time.zone.all": "All time zones",
"account.settings.field.time.zone.country": "Fuseaux horaires des pays", "account.settings.field.time.zone.country": "Country time zones",
"account.settings.section.social.media": "Liens vers les réseaux sociaux", "account.settings.section.social.media": "Social Media Links",
"account.settings.section.social.media.description": "Optionnellement, liez vos comptes personnels aux icônes des médias sociaux sur votre profil {siteName}.", "account.settings.section.social.media.description": "Optionally, link your personal accounts to the social media icons on your edX profile.",
"account.settings.field.social.platform.name.linkedin": "LinkedIn", "account.settings.field.social.platform.name.linkedin": "LinkedIn",
"account.settings.field.social.platform.name.linkedin.empty": "Ajouter un profil LinkedIn", "account.settings.field.social.platform.name.linkedin.empty": "Add LinkedIn profile",
"account.settings.jump.nav.delete.account": "Supprimer mon compte", "account.settings.jump.nav.delete.account": "Delete My Account",
"account.settings.field.social.platform.name.twitter": "Twitter", "account.settings.field.social.platform.name.twitter": "Twitter",
"account.settings.field.social.platform.name.twitter.empty": "Ajouter un profil Twitter", "account.settings.field.social.platform.name.twitter.empty": "Add Twitter profile",
"account.settings.field.social.platform.name.facebook": "Facebook", "account.settings.field.social.platform.name.facebook": "Facebook",
"account.settings.field.social.platform.name.facebook.empty": "Ajouter un profil Facebook", "account.settings.field.social.platform.name.facebook.empty": "Add Facebook profile",
"account.settings.editable.field.action.save": "Enregistrer", "account.settings.editable.field.action.save": "Save",
"account.settings.editable.field.action.cancel": "Annuler", "account.settings.editable.field.action.cancel": "Cancel",
"account.settings.editable.field.action.edit": "Modifier", "account.settings.editable.field.action.edit": "Edit",
"account.settings.static.field.empty": "Aucune valeur n'a été fixée. Contactez l'administrateur de votre {entreprise} pour effectuer des modifications.", "account.settings.static.field.empty": "No value set. Contact your {enterprise} administrator to make changes.",
"account.settings.static.field.empty.no.admin": "Pas de valeur définie.", "account.settings.static.field.empty.no.admin": "No value set.",
"account.settings.field.name.certificate.select": "Si sélectionné, ce nom va apparaître sur vos attestations et dossiers publics.", "account.settings.coaching.consent.welcome.header": "Lets get started.",
"account.settings.field.name.modal.certificate.title": "Choisissez le nom qui va apparaîtres sur vos attestations et dossiers publics.", "account.settings.coaching.consent.welcome.subheader": "We're here for you from start to finish",
"account.settings.field.name.modal.certificate.select": "Sélectionnez un nom", "account.settings.coaching.consent.description": "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.*",
"account.settings.field.name.modal.certificate.option.full": "Nom complet", "account.settings.coaching.consent.text-messaging.disclaimer": "* 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.",
"account.settings.field.name.modal.certificate.option.verified": "Nom vérifié", "account.settings.coaching.consent.accept-coaching": "Sign up for coaching",
"account.settings.field.name.modal.certificate.button.choose": "Choisissez un nom", "account.settings.coaching.consent.decline-coaching": "I prefer not to be contacted with free coaching services",
"account.settings.coaching.consent.welcome.header": "Commençons.", "account.settings.coaching.consent.label.name": "Please confirm your name",
"account.settings.coaching.consent.welcome.subheader": "Nous sommes là pour vous du début à la fin", "account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
"account.settings.coaching.consent.description": "Les programmes MicroBachelors comprennent un coaching axé sur votre carrière, vos études et la façon dont vous obtiendrez des résultats grâce à une communication individuelle avec un professionnel expérimenté. Si vous êtes intéressé, fournissez les informations ci-dessous et cliquez sur \"Soumettre\". Notre partenaire de coaching vous contactera par courrier électronique et/ou par SMS pour vous aider à progresser. Les conditions générales s'appliquent.*", "account.settings.coaching.consent.success.header": "Success!",
"account.settings.coaching.consent.text-messaging.disclaimer": "* Les services de coaching sont inclus sans frais supplémentaires pour les apprenants ayant un numéro de téléphone aux États-Unis. Le coaching comprend des messages textes récurrents. Les tarifs des messages et des données peuvent s'appliquer. Text STOP pour se désengager.", "account.settings.coaching.consent.success.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
"account.settings.coaching.consent.accept-coaching": "S'inscrire pour le coaching.", "account.settings.coaching.consent.success.continue": "Start my course",
"account.settings.coaching.consent.decline-coaching": "Je préfère ne pas être contacté par les services de coaching gratuit.",
"account.settings.coaching.consent.label.name": "Veuillez confirmer votre nom",
"account.settings.coaching.consent.label.phone-number": "Entrer un numéro de cellulaire",
"account.settings.coaching.consent.success.header": "Opération réussie!",
"account.settings.coaching.consent.success.message": "Vous êtes inscrit au coaching. Vous pouvez vous attendre à un message par courriel ou SMS dans les prochains jours.",
"account.settings.coaching.consent.success.continue": "Démarrer mon cours",
"account.settings.coaching.managed.support": "support", "account.settings.coaching.managed.support": "support",
"account.settings.coaching.managed.alert": "Votre nom est géré par {managerTitle}. Contactez votre administrateur pour obtenir de l'aide.", "account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
"account.settings.field.phone_number": "Numéro de téléphone", "account.settings.field.phone_number": "Phone Number",
"account.settings.field.phone_number.empty": "Ajouter un numéro de téléphone", "account.settings.field.phone_number.empty": "Add a phone number",
"account.settings.field.coaching_consent": "Consentement pour le coaching", "account.settings.field.coaching_consent": "Coaching consent",
"account.settings.field.coaching_consent.tooltip": "Un parcours MicroBachelors inclus du coaching par texto avec un professionnel expériementé qui vous aidera à combiner votre expérience éducationnelle et vos objectifs de carrière. Les services de coaching sont inclus sans coût additionnel aux apprenant ayant un numéro de téléphone US. Le coût standard des textos s'applique. Texter 'STOP' en tout temp pour arrêter les messages.", "account.settings.field.coaching_consent.tooltip": "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.",
"account.settings.field.coaching_consent.error": "Un numéro de téléphone US valide est requis pour s'inscrire pour du coaching", "account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
"account.settings.delete.account.before.proceeding": "Avant de poursuivre, veuillez {actionLink}.", "account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
"account.settings.delete.account.header": "Supprimer mon compte", "account.settings.delete.account.header": "Delete My Account",
"account.settings.delete.account.subheader": "Nous sommes désolés de vous voir quitter!", "account.settings.delete.account.subheader": "We're sorry to see you go!",
"account.settings.delete.account.text.1": "Veuillez noter que la suppression de votre compte et de vos données personnelles est permanente et ne peut être annulée. {siteName} ne pourra pas récupérer votre compte ou les données supprimées.", "account.settings.delete.account.text.1": "Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.",
"account.settings.delete.account.text.2": "Une fois votre compte supprimé, vous ne pourrez plus l'utiliser pour suivre des cours sur {siteName}.", "account.settings.delete.account.text.2": "Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.",
"account.settings.delete.account.text.2.edX": "Une fois votre compte supprimé, vous ne pourrez plus l'utiliser pour prendre des cours sur l'application edX, edx.org, ou tout autre site hébergé par edX. Cela comprend l'accès à edx.org à partir du système de votre employeur ou de votre université et l'accès aux sites privés offerts par MIT Open Learning, Wharton Executive Education, et Harvard Medical School.", "account.settings.delete.account.text.3.link": "follow the instructions for printing or downloading a certificate",
"account.settings.delete.account.text.3.link": "Suivez ces instructions pour imprimer ou télécharger une attestation", "account.settings.delete.account.text.warning": "Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.",
"account.settings.delete.account.text.warning": "Avertissement : la suppression d'un compte est permanente. Veuillez lire attentivement ce qui précède avant de continuer. Cette action est irréversible et vous ne pourrez plus utiliser la même adresse courriel sur {siteName}.", "account.settings.delete.account.text.change.instead": "Want to change your email, name, or password instead?",
"account.settings.delete.account.text.change.instead": "Vous souhaitez modifier votre adresse électronique, votre nom ou votre mot de passe?", "account.settings.delete.account.button": "Delete My Account",
"account.settings.delete.account.button": "Supprimer mon compte", "account.settings.delete.account.please.activate": "activate your account",
"account.settings.delete.account.please.activate": "activez votre compte", "account.settings.delete.account.please.unlink": "unlink all social media accounts",
"account.settings.delete.account.please.unlink": "dissocier tous les comptes de médias sociaux", "account.settings.delete.account.modal.header": "Are you sure?",
"account.settings.delete.account.modal.header": "Êtes-vous certain ?", "account.settings.delete.account.modal.text.1": "You have selected \"Delete My Account\". Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.",
"account.settings.delete.account.modal.text.1": "Vous avez sélectionné \"Supprimer mon compte\". La suppression de votre compte et de vos données personnelles est permanente et ne peut être annulée. {siteName} ne pourra ni récupérer votre compte ni les données supprimées.", "account.settings.delete.account.modal.text.2": "If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.",
"account.settings.delete.account.modal.text.2": "Si vous continuez, vous ne pourrez plus utiliser ce compte pour suivre des cours sur {siteName}.", "account.settings.delete.account.modal.enter.password": "If you still wish to continue and delete your account, please enter your account password:",
"account.settings.delete.account.modal.text.2.edX": "Si vous procédez ainsi, vous ne pourrez pas utiliser ce compte pour suivre des cours sur l'application edX, edx.org ou tout autre site hébergé par edX. Cela inclut l'accès à edx.org depuis le système de votre employeur ou de votre université et l'accès aux sites privés proposés par MIT Open Learning, Wharton Executive Education et Harvard Medical School.", "account.settings.delete.account.modal.confirm.delete": "Yes, Delete",
"account.settings.delete.account.modal.enter.password": "Si vous souhaitez toujours supprimer votre compte, veuillez saisir le mot de passe de votre compte :", "account.settings.delete.account.modal.confirm.cancel": "Cancel",
"account.settings.delete.account.modal.confirm.delete": "Oui, supprimer", "account.settings.delete.account.error.unable.to.delete": "Unable to delete account",
"account.settings.delete.account.modal.confirm.cancel": "Annuler", "account.settings.delete.account.error.no.password": "A password is required",
"account.settings.delete.account.error.unable.to.delete": "Impossible de supprimer le compte", "account.settings.delete.account.error.invalid.password": "Password is incorrect",
"account.settings.delete.account.error.no.password": "Un mot de passe est requis", "account.settings.delete.account.error.unable.to.delete.details": "Sorry, there was an error trying to process your request. Please try again later.",
"account.settings.delete.account.error.invalid.password": "Mot de passe incorrect.", "account.settings.delete.account.modal.after.header": "We're sorry to see you go! Your account will be deleted shortly.",
"account.settings.delete.account.error.unable.to.delete.details": "Nous sommes désolés, il y a eu une erreur dans le suivi de votre demande. Veuillez réessayer plus tard.", "account.settings.delete.account.modal.after.text": "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.",
"account.settings.delete.account.modal.after.header": "Nous sommes désolés de vous voir quitter! Votre compte sera bientôt supprimé.", "account.settings.delete.account.modal.after.button": "Close",
"account.settings.delete.account.modal.after.text": "La suppression d'un compte, comprenant le retrait des listes d'e-mail, peu prendre quelques semaines afin de traiter entièrement notre système. Si vous souhaitez renoncer aux e-mails ultérieurement, veuillez vous désinscrire en consultant le pied de page de n'importe quel e-mail.", "account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
"account.settings.delete.account.modal.after.button": "Fermer", "account.settings.field.demographics.gender": "Gender identity",
"account.settings.delete.account.text.3.edX": "Vous pouvez également perdre l'accès aux attestations vérifiées et à d'autres informations d'identification de programme, comme les attestations MicroMasters. Vous pouvez en faire une copie pour vos dossiers avant de procéder à la suppression. {actionLink}.", "account.settings.field.demographics.gender.empty": "Add gender identity",
"account.settings.delete.account.text.3": "Vous pouvez également perdre accès aux attestations vérifiées et à d'autres informations d'identification de programme. Vous pouvez en faire une copie pour vos dossiers avant de procéder à la suppression.", "account.settings.field.demographics.gender.options.empty": "Select a gender identity",
"account.settings.message.demographics.service.issue": "Une erreur s'est produite lors de la tentative de récupération ou d'enregistrement des informations de votre compte. Veuillez réessayer plus tard.", "account.settings.field.demographics.gender.options.woman": "Woman",
"account.settings.field.demographics.gender": "Identité de genre", "account.settings.field.demographics.gender.options.man": "Man",
"account.settings.field.demographics.gender.empty": "Ajouter une identité de genre", "account.settings.field.demographics.gender.options.non-binary": "Non-binary",
"account.settings.field.demographics.gender.options.empty": "Sélectionnez une identité de genre", "account.settings.field.demographics.gender.options.self-describe": "Prefer to self-describe",
"account.settings.field.demographics.gender_description": "Description de l'identité de genre", "account.settings.field.demographics.gender_description": "Gender identity description",
"account.settings.field.demographics.gender_description.empty": "Entrez la description", "account.settings.field.demographics.gender_description.empty": "Enter description",
"account.settings.field.demographics.ethnicity": "Identité raciale / ethnique", "account.settings.field.demographics.ethnicity": "Race/Ethnicity identity",
"account.settings.field.demographics.ethnicity.empty": "Ajouter une identité raciale / ethnique", "account.settings.field.demographics.ethnicity.empty": "Add race/ethnicity identity",
"account.settings.field.demographics.ethnicity.options.empty": "Sélectionnez tout ce qui s'y rapporte", "account.settings.field.demographics.ethnicity.options.empty": "Select all that apply",
"account.settings.field.demographics.income": "Revenu familial", "account.settings.field.demographics.ethnicity.options.american-indian-or-alaska-native": "American Indian or Alaska Native",
"account.settings.field.demographics.income.empty": "Ajouter le revenu familial", "account.settings.field.demographics.ethnicity.options.asian": "Asian",
"account.settings.field.demographics.income.options.empty": "Sélectionnez une fourchette de revenu familial", "account.settings.field.demographics.ethnicity.options.black-or-african-american": "Black or African American",
"account.settings.field.demographics.military_history": "Statut militaire U.S.", "account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish": "Hispanic, Latin, or Spanish origin",
"account.settings.field.demographics.military_history.empty": "Ajouter un statut militaire", "account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african": "Middle Eastern or North African",
"account.settings.field.demographics.military_history.options.empty": "Sélectionnez un statut militaire", "account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander": "Native Hawaiian or Other Pacific Islander",
"account.settings.field.demographics.learner_education_level": "Votre niveau d'éducation", "account.settings.field.demographics.ethnicity.options.white": "White",
"account.settings.field.demographics.learner_education_level.empty": "Ajouter un niveau d'éducation", "account.settings.field.demographics.ethnicity.options.other": "Some other race, ethnicity, or origin",
"account.settings.field.demographics.parent_education_level": "Niveau d'éducation des parents / tuteurs", "account.settings.field.demographics.income": "Family income",
"account.settings.field.demographics.parent_education_level.empty": "Ajouter un niveau d'éducation", "account.settings.field.demographics.income.empty": "Add family income",
"account.settings.field.demographics.education_level.options.empty": "Sélectionnez le niveau d'éducation", "account.settings.field.demographics.income.options.empty": "Select a family income range",
"account.settings.field.demographics.work_status": "Statut d'emploi", "account.settings.field.demographics.income.options.less-than-10k": "Less than US $10,000",
"account.settings.field.demographics.work_status.empty": "Ajouter un statut d'emploi", "account.settings.field.demographics.income.options.10k-25k": "US $10,000 - $25,000",
"account.settings.field.demographics.work_status.options.empty": "Sélectionnez le statut d'emploi", "account.settings.field.demographics.income.options.25k-50k": "US $25,000 - $50,000",
"account.settings.field.demographics.work_status_description": "Description du statut d'emploi", "account.settings.field.demographics.income.options.50k-75k": "US $50,000 - $75,000",
"account.settings.field.demographics.work_status_description.empty": "Entrez la description", "account.settings.field.demographics.income.options.over-100k": "Over US $100,000",
"account.settings.field.demographics.current_work_sector": "Secteur d'emploi actuel", "account.settings.field.demographics.income.options.unsure": "I don't know",
"account.settings.field.demographics.current_work_sector.empty": "Ajouter le secteur d'emploi", "account.settings.field.demographics.military_history": "U.S. Military status",
"account.settings.field.demographics.future_work_sector": "Secteur d'emploi futur", "account.settings.field.demographics.military_history.empty": "Add military status",
"account.settings.field.demographics.future_work_sector.empty": "Ajouter le secteur d'emploi", "account.settings.field.demographics.military_history.options.empty": "Select military status",
"account.settings.field.demographics.work_sector.options.empty": "Sélectionnez un secteur d'emploi", "account.settings.field.demographics.income.options.never-served": "Never served in the military",
"account.settings.section.demographics.why": "Pourquoi est-ce que {siteName} collecte ces informations ?", "account.settings.field.demographics.income.options.training": "Only on active duty for training",
"account.settings.name.change.title.id": "Ce changement de nom requiert une vérification d'identité.", "account.settings.field.demographics.income.options.active": "Now on active duty",
"account.settings.name.change.title.begin": "Avant que l'on commençons", "account.settings.field.demographics.income.options.previously-active": "On active duty in the past, but not now",
"account.settings.name.change.warning.one": "Warning: This action updates the name that appears on all certificates that have been earned on this account in the past and any certificates you are currently earning or will earn in the future.", "account.settings.field.demographics.learner_education_level": "Your education level",
"account.settings.name.change.warning.two": "Cette action ne peut pas être renversée sans vérification d'identité.", "account.settings.field.demographics.learner_education_level.empty": "Add education level",
"account.settings.name.change.id.name.label": "Enter your name as it appears on your government-issued ID.", "account.settings.field.demographics.parent_education_level": "Parents/Guardians education level",
"account.settings.name.change.id.name.placeholder": "Enter the name on your government ID", "account.settings.field.demographics.parent_education_level.empty": "Add education level",
"account.settings.name.change.error.valid.name": "Entrez un nom valide", "account.settings.field.demographics.education_level.options.empty": "Select education level",
"account.settings.name.change.error.general": "Une erreur est survenue. Veuillez réessayer.", "account.settings.field.demographics.education_level.options.no-high-school": "No High School",
"account.settings.name.change.continue": "Continuer", "account.settings.field.demographics.education_level.options.some-high-school": "Some High School",
"account.settings.name.change.cancel": "Annuler", "account.settings.field.demographics.education_level.options.high-school-ged-equivalent": "High School diploma, GED, or equivalent",
"error.notfound.message": "La page que vous recherchez n'est pas disponible ou il y a une erreur dans l'URL. Veuillez vérifier l'URL et réessayer.", "account.settings.field.demographics.education_level.options.some-college": "Some college, but no degree",
"account.settings.field.demographics.education_level.options.associates": "Associates degree",
"account.settings.field.demographics.education_level.options.bachelors": "Bachelors degree",
"account.settings.field.demographics.education_level.options.masters": "Masters degree",
"account.settings.field.demographics.education_level.options.professional": "Professional degree",
"account.settings.field.demographics.education_level.options.doctorate": "Doctorate degree",
"account.settings.field.demographics.work_status": "Employment status",
"account.settings.field.demographics.work_status.empty": "Add employment status",
"account.settings.field.demographics.work_status.options.empty": "Select employment status",
"account.settings.field.demographics.work_status.options.full-time": "Employed, working full-time",
"account.settings.field.demographics.work_status.options.part-time": "Employed, working part-time",
"account.settings.field.demographics.work_status.options.not-employed-looking": "Not employed, looking for work",
"account.settings.field.demographics.work_status.options.not-employed-not-looking": "Not employed, not looking for work",
"account.settings.field.demographics.work_status.options.unable": "Unable to work",
"account.settings.field.demographics.work_status.options.retired": "Retired",
"account.settings.field.demographics.work_status.options.other": "Other",
"account.settings.field.demographics.work_status_description": "Employment status description",
"account.settings.field.demographics.work_status_description.empty": "Enter description",
"account.settings.field.demographics.current_work_sector": "Current work industry",
"account.settings.field.demographics.current_work_sector.empty": "Add work industry",
"account.settings.field.demographics.future_work_sector": "Future work industry",
"account.settings.field.demographics.future_work_sector.empty": "Add work industry",
"account.settings.field.demographics.work_sector.options.empty": "Select work industry",
"account.settings.field.demographics.work_sector.options.accommodation-food": "Accommodation and Food Services",
"account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation": "Administrative and Support and Waste Management and Remediation Services",
"account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting": "Agriculture, Forestry, Fishing and Hunting",
"account.settings.field.demographics.work_sector.options.arts-entertainment-recreation": "Arts, Entertainment, and Recreation",
"account.settings.field.demographics.work_sector.options.construction": "Construction",
"account.settings.field.demographics.work_sector.options.educational": "Education Services",
"account.settings.field.demographics.work_sector.options.finance-insurance": "Finance and Insurance",
"account.settings.field.demographics.work_sector.options.healthcare-social": "Health Care and Social Assistance",
"account.settings.field.demographics.work_sector.options.information": "Information",
"account.settings.field.demographics.work_sector.options.management": "Management of Companies and Enterprises",
"account.settings.field.demographics.work_sector.options.manufacturing": "Manufacturing",
"account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas": "Mining, Quarrying, and Oil and Gas Extraction",
"account.settings.field.demographics.work_sector.options.professional-scientific-technical": "Professional, Scientific, and Technical Services",
"account.settings.field.demographics.work_sector.options.public-admin": "Public Administration",
"account.settings.field.demographics.work_sector.options.real-estate": "Real Estate and Rental and Leasing",
"account.settings.field.demographics.work_sector.options.retail": "Retail Trade",
"account.settings.field.demographics.work_sector.options.transport-warehousing": "Transportation and Warehousing",
"account.settings.field.demographics.work_sector.options.utilities": "Utilities",
"account.settings.field.demographics.work_sector.options.trade": "Wholesale Trade",
"account.settings.field.demographics.work_sector.options.other": "Other",
"account.settings.field.demographics.options.declined": "Prefer not to respond",
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support", "account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
"account.settings.editable.field.password.reset.button.confirmation": "Nous avons envoyé un message à {email}. Cliquez sur le lien dans le message pour réinitialiser votre mot de passe. Vous n'avez pas reçu le message? Contactez {technicalSupportLink}.", "account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
"account.settings.editable.field.password.reset.button": "Réinitialiser le mot de passe", "account.settings.editable.field.password.reset.button": "Reset Password",
"account.settings.editable.field.password.reset.button.forbidden": "Votre demande précédente est en cours, veuillez réessayer dans quelques instants.", "account.settings.editable.field.password.reset.button.forbidden": "Your previous request is in progress, please try again in few moments.",
"account.settings.editable.field.password.reset.label": "Mot de passe", "account.settings.editable.field.password.reset.label": "Password",
"account.settings.sso.link.account": "Se connecter avec {name}", "account.settings.sso.link.account": "Sign in with {name}",
"account.settings.sso.account.connected": "Lié", "account.settings.sso.account.connected": "Linked",
"account.settings.sso.account.disconnect.error": "Un problème est survenu lors de la déconnexion de ce compte. Contactez le support si le problème persiste.", "account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
"account.settings.sso.unlink.account": "Dissocier le compte {name}", "account.settings.sso.unlink.account": "Unlink {name} account",
"account.settings.sso.no.providers": "Aucun compte ne peut être lié pour le moment.", "account.settings.sso.no.providers": "No accounts can be linked at this time.",
"id.verification.access.blocked.denied": "Vous ne pouvez pas vérifier votre identité pour le moment. Si vous n'avez pas encore activé votre compte, veuillez vérifier votre dossier de pourriels pour le courriel d'activation de {email}.", "id.verification.next": "Next",
"id.verification.next": "Suivant", "id.verification.requirements.title": "Photo Verification Requirements",
"id.verification.support": "support", "id.verification.requirements.description": "In order to complete Photo Verification online, you will need the following:",
"id.verification.continue.upload": "Continuer avec le téléchargement", "id.verification.requirements.card.device.title": "Device with Camera",
"id.verification.example.card.alt": "Exemple de carte d'identité valide avec un nom complet et une photo.", "id.verification.requirements.card.device.allow": "Allow",
"id.verification.requirements.title": "Exigences de vérification des photos", "id.verification.requirements.card.id.title": "Photo Identification",
"id.verification.requirements.description": "Afin de procéder à la vérification des photos, vous aurez besoin des éléments suivants :", "id.verification.requirements.card.id.text": "You need a valid ID that contains your full name and photo.",
"id.verification.requirements.card.device.title": "Appareil avec caméra", "id.verification.privacy.title": "Privacy Information",
"id.verification.requirements.card.device.allow": "Autoriser", "id.verification.privacy.need.photo.question": "Why does edX need my photo?",
"id.verification.requirements.card.id.title": "Carte d'identité avec photo", "id.verification.privacy.need.photo.answer": "We use your verification photos to confirm your identity and ensure the validity of your certificate.",
"id.verification.requirements.card.id.text": "Vous avez besoin d'une carte d'identité valide contenant votre nom complet et votre photo, comme un permis de conduire ou un passeport.", "id.verification.privacy.do.with.photo.question": "What does edX do with this photo?",
"id.verification.privacy.title": "Information sur la confidentialité", "id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
"id.verification.privacy.need.photo.question": "Pourquoi est-ce que {siteName} a besoin de ma photo?", "id.verification.existing.request.title": "Identity Verification",
"id.verification.privacy.need.photo.answer": "Nous utilisons vos photos de vérification pour confirmer votre identité et assurer la validité de votre certificat.", "id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).",
"id.verification.privacy.do.with.photo.question": "Qu'est-ce que {siteName} fait avec cette photo?", "id.verification.existing.request.denied.text": "You cannot verify your identity at this time.",
"id.verification.privacy.do.with.photo.answer": "Nous chiffrons votre photo de manière sécurisée et l'envoyons à notre service d'autorisation pour vérification. Votre photo et vos informations ne sont pas enregistrées ni visibles nulle part sur {siteName} une fois le processus de vérification terminé.", "id.verification.photo.take": "Take Photo",
"id.verification.access.blocked.title": "Vérification d'identité", "id.verification.photo.retake": "Retake Photo",
"id.verification.access.blocked.enrollment": "Vous n'êtes actuellement pas inscrit à un cours nécessitant une vérification d'identité.", "id.verification.camera.access.title": "Camera Permissions",
"id.verification.access.blocked.pending": "Vous avez déjà soumis vos informations de vérification. Vous verrez un message sur votre tableau de bord lorsque le processus de vérification sera terminé (généralement dans les 5 jours).", "id.verification.camera.access.click.allow": "Please make sure to click \"Allow\"",
"id.verification.photo.take": "Prendre une photo", "id.verification.camera.access.enable": "Enable Camera",
"id.verification.photo.retake": "Reprendre la photo ?", "id.verification.camera.access.problems": "Having problems?",
"id.verification.photo.enable.detection": "Activer la détection des visages", "id.verification.camera.access.skip": "Skip and upload image files instead",
"id.verification.photo.enable.detection.portrait.help.text": "Si coché, une case apparaîtra autour de votre visage. Votre visage peut être vu clairement si la boîte qui l'entoure est bleue. Si votre visage n'est pas dans une bonne position ou indétectable, la case sera rouge.", "id.verification.camera.access.success": "Looks like your camera is working and ready.",
"id.verification.photo.enable.detection.id.help.text": "Si coché, une case apparaîtra autour du visage sur votre pièce d'identité. Le visage peut être vu clairement si la boîte qui l'entoure est bleue. Si le visage n'est pas dans une bonne position ou indétectable, la case sera rouge.", "id.verification.camera.access.failure": "It looks like we're unable to access your camera. You will need to upload image files of you and your photo id.",
"id.verification.photo.feedback.correct": "Le visage est en bonne position.", "id.verification.photo.tips.title": "Helpful Photo Tips",
"id.verification.photo.feedback.two.faces": "Plus d'un visage détecté.", "id.verification.photo.tips.description": "Next, we'll need you to take a photo of your face. Please review the helpful tips below.",
"id.verification.photo.feedback.no.faces": "Aucun visage détecté.", "id.verification.photo.tips.list.title": "Photo Tips",
"id.verification.photo.feedback.top.left": "Position incorrecte. En haut à gauche.", "id.verification.photo.tips.list.description": "To take a successful photo, make sure that:",
"id.verification.photo.feedback.top.center": "Position incorrecte. En haut au centre.", "id.verification.photo.tips.list.well.lit": "Your face is well-lit.",
"id.verification.photo.feedback.top.right": "Position incorrecte. En haut à droite.", "id.verification.photo.tips.list.inside.frame": "Your entire face fits inside the frame.",
"id.verification.photo.feedback.center.left": "Position incorrecte. Centre gauche.", "id.verification.portrait.photo.title.camera": "Take Your Photo",
"id.verification.photo.feedback.center.center": "Position incorrecte. Trop près de la caméra.", "id.verification.portrait.photo.title.upload": "Upload Your Portrait Photo",
"id.verification.photo.feedback.center.right": "Position incorrecte. Centre droit.", "id.verification.portrait.photo.preview.alt": "Preview of photo of user's face.",
"id.verification.photo.feedback.bottom.left": "Position incorrecte. En bas à gauche.", "id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.",
"id.verification.photo.feedback.bottom.center": "Position incorrecte. En bas au centre.", "id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.photo.feedback.bottom.right": "Position incorrecte. En bas à droite.", "id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?",
"id.verification.camera.access.title": "Autorisations de la caméra", "id.verification.camera.help.sight.answer": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.access.title.success": "Accès à la caméra activé", "id.verification.camera.help.head.question": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.access.title.failed": "L'accès à la caméra a échoué", "id.verification.camera.help.head.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.camera.access.click.allow": "Veuillez vous assurer de cliquer sur \"Autoriser\"", "id.verification.id.tips.title": "Helpful ID Tips",
"id.verification.camera.access.enable": "Activer la caméra", "id.verification.id.tips.description": "Next you'll need an eligible ID photo, make sure that:",
"id.verification.camera.access.problems": "Vous avez des problèmes?", "id.verification.id.tips.list.well.lit": "Your ID is well-lit.",
"id.verification.camera.access.skip": "Ignorer et télécharger les fichiers image au lieu", "id.verification.id.tips.list.clear": "Ensure that you can see your photo and clearly read your name.",
"id.verification.camera.access.success": "Il semble que votre appareil photo fonctionne et est prêt.", "id.verification.id.photo.title.camera": "Take ID Photo",
"id.verification.camera.access.failure": "Il semble que nous ne puissions pas accéder à votre caméra. Vous devrez télécharger des fichiers image de vous et de votre pièce d'identité.", "id.verification.id.photo.title.upload": "Upload Your ID Photo",
"id.verification.camera.access.failure.temporary": "Il semblerait que nous ne pouvons pas accéder à votre caméra. Vérifiez qu'elle est bien connectée et que votre navigateur est autorisé à y accéder.", "id.verification.id.photo.preview.alt": "Preview of photo ID.",
"id.verification.camera.access.failure.temporary.chrome": "Pour activer l'accès à la caméra dans Chrome:", "id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo.",
"id.verification.camera.access.failure.temporary.chrome.step1": "Ouvrir Chrome.", "id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.camera.access.failure.temporary.chrome.step2": "Accédez à Plus> Paramètres.", "id.verification.account.name.title": "Account Name Check",
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "Pour Windows: Alt+F, Alt+E ou F10 suivi de la barre d'espace", "id.verification.account.name.instructions": "Please check the Account Name below to ensure it matches the name on your ID. If not, click \"Edit\".",
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "Pour Mac: Command+,", "id.verification.account.name.warning.prefix": "Please Note:",
"id.verification.camera.access.failure.temporary.chrome.step3": "Sous l'onglet «Confidentialité et sécurité», sélectionnez «Paramètres du site», puis «Appareil photo».", "id.verification.account.name.settings": "Account Settings",
"id.verification.camera.access.failure.temporary.chrome.step4": "Sous \"Bloqué\", trouvez \"edx.org\" et sélectionnez-le.", "id.verification.account.name.label": "Name on ID",
"id.verification.camera.access.failure.temporary.chrome.step5": "Dans la section \"Autorisations\", mettez à jour les autorisations de la caméra pour \"Autoriser\".", "id.verification.account.name.edit": "Edit",
"id.verification.camera.access.failure.temporary.ie11": "Pour activer l'accès à la caméra dans Internet Explorer:", "id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
"id.verification.camera.access.failure.temporary.ie11.step1": "Ouvrez le Gestionnaire des paramètres de Flash Player en accédant à Paramètres Windows> Panneau de configuration> Flash Player.", "id.verification.account.name.save": "Save",
"id.verification.camera.access.failure.temporary.ie11.step2": "Sélectionnez l'onglet \"Caméra et micro\", puis sélectionnez le bouton \"Paramètres de la caméra et du microphone par site\".", "id.verification.review.title": "Review Your Photos",
"id.verification.camera.access.failure.temporary.ie11.step3": "Choisissez \"edx.org\" dans la liste des sites Web et modifiez les autorisations en sélectionnant \"Autoriser\" dans le menu déroulant.", "id.verification.review.description": "Make sure we can verify your identity with the photos and information you have provided.",
"id.verification.camera.access.failure.temporary.firefox": "Pour activer l'accès à la caméra dans Firefox:", "id.verification.review.portrait.label": "Your Portrait",
"id.verification.camera.access.failure.temporary.firefox.step1": "Ouvrez Firefox.", "id.verification.review.portrait.alt": "Photo of your face to be submitted.",
"id.verification.camera.access.failure.temporary.firefox.step2": "Saisissez \"about:preferences\" dans la barre d'URL.", "id.verification.review.portrait.retake": "Retake Portrait Photo",
"id.verification.camera.access.failure.temporary.firefox.step3": "Sélectionnez l'onglet \"Confidentialité et sécurité\" et accédez à la section \"Autorisations\".", "id.verification.review.id.label": "Your Photo ID",
"id.verification.camera.access.failure.temporary.firefox.step4": "À côté de \"Appareil photo\", sélectionnez le bouton \"Paramètres…\".", "id.verification.review.id.alt": "Photo of your ID to be submitted.",
"id.verification.camera.access.failure.temporary.firefox.step5": "Dans la barre de recherche, entrez \"edx.org\".", "id.verification.review.id.retake": "Retake ID Photo",
"id.verification.camera.access.failure.temporary.firefox.step6": "Dans la colonne d'état de \"edx.org\", sélectionnez \"Autoriser\" dans la liste déroulante.", "id.verification.review.confirm": "Confirm",
"id.verification.camera.access.failure.temporary.firefox.step7": "Sélectionnez \"Enregistrer les modifications\".", "id.verification.submitted.title": "Identity Verification in Progress",
"id.verification.camera.access.failure.temporary.safari": "Pour activer l'accès à la caméra dans Safari:", "id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days). In the meantime, you can still access all available course content.",
"id.verification.camera.access.failure.temporary.safari.step1": "Ouvrir Safari.", "id.verification.submitted.return": "Return to Your Dashboard",
"id.verification.camera.access.failure.temporary.safari.step2": "Cliquez sur le menu de l'application Safari, puis sélectionnez \"Préférences\". Vous pouvez également utiliser Command+ comme raccourci clavier.", "id.verification.account.name.warning": "{prefix} Any edit to your name will be saved to your account and can be reviewed on {accountSettings}.",
"id.verification.camera.access.failure.temporary.safari.step3": "Sélectionnez l'onglet \"Sites Web\", puis sélectionnez \"Appareil photo\".", "id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
"id.verification.camera.access.failure.temporary.safari.step4": "Sélectionnez \"edx.org\" et changez les autorisations de la caméra en \"Allow\".", "id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}."
"id.verification.camera.access.failure.unsupported": "Il semble que votre fureteur ne prend pas en charge l'accès à la caméra.",
"id.verification.camera.access.failure.unsupported.chrome.explanation": "Le fureteur Chrome ne prend actuellement pas en charge l'accès à la caméra sur les appareils iOS, tels que les iPhones et les iPad.",
"id.verification.camera.access.failure.unsupported.instructions": "Veuillez utiliser un autre navigateur pour terminer la vérification d'identité.",
"id.verification.photo.tips.title": "Conseils photo utiles",
"id.verification.photo.tips.description": "Ensuite, nous aurons besoin de vous preniez une photo de votre visage. Veuillez consulter les conseils utiles ci-dessous.",
"id.verification.photo.tips.list.title": "Conseils photo",
"id.verification.photo.tips.list.description": "Pour prendre une bonne photo, assurez-vous que: ",
"id.verification.photo.tips.list.well.lit": "Votre visage est bien éclairé",
"id.verification.photo.tips.list.inside.frame": " Votre visage est entièrement dans le cadre.",
"id.verification.portrait.photo.title.camera": "Prenez une photo de vous",
"id.verification.portrait.photo.title.upload": "Téléchargez une photo de vous",
"id.verification.portrait.photo.preview.alt": "Aperçu de la photo du visage de l'utilisateur.",
"id.verification.portrait.photo.instructions.camera": "Lorsque votre visage est en position, utilisez le bouton Prendre une photo ci-dessous pour prendre votre photo.",
"id.verification.portrait.photo.instructions.upload": "Veuillez télécharger une photo de portrait. Assurez-vous que tout votre visage s'insère dans le cadre et est bien éclairé. Formats pris en charge :",
"id.verification.camera.help.sight.question": "Que faire si je ne peux pas voir l'image de la caméra ou si je ne peux pas voir ma photo pour déterminer quel côté est visible?",
"id.verification.camera.help.sight.answer.portrait": "Vous pourrez peut-être terminer la procédure de capture d'image sans aide, mais cela peut prendre quelques tentatives de soumission pour obtenir le bon positionnement de la caméra. Le positionnement optimal de la caméra varie avec chaque ordinateur, mais généralement la meilleure position pour une prise de vue de la tête est d'environ 12-18 pouces (30-45 centimètres) de la caméra, la tête étant centrée par rapport à l'écran de l'ordinateur. Si les photos que vous soumettez sont rejetées, essayez de déplacer lorientation de lordinateur ou de lappareil photo pour modifier langle déclairage.",
"id.verification.camera.help.sight.answer.id": "Vous pourrez peut-être terminer la procédure de capture d'image sans aide, mais cela peut prendre quelques tentatives de soumission pour obtenir le bon positionnement de la caméra. Le positionnement optimal de la caméra varie avec chaque ordinateur, mais généralement, la meilleure position pour une photo d'une carte d'identité est de 8-12 pouces (20-30 centimètres) de la caméra, la carte d'identité étant centrée par rapport à la caméra. Si les photos que vous soumettez sont rejetées, essayez de déplacer lorientation de lordinateur ou de lappareil photo pour modifier langle déclairage. La raison la plus courante de rejet est l'incapacité de lire le texte sur la carte d'identité.",
"id.verification.camera.help.difficulty.question.portrait": "Que faire si j'ai des difficultés à maintenir ma tête en position par rapport à la caméra ?",
"id.verification.camera.help.difficulty.question.id": "Que faire si j'ai des difficultés à tenir ma carte d'identité en position par rapport à la caméra ?",
"id.verification.camera.help.difficulty.answer": "Si vous avez besoin d'aide pour prendre une photo à soumettre, contactez le support {siteName} pour des suggestions supplémentaires.",
"id.verification.camera.help.upload.question": "Et si je souhaite télécharger une photo à la place ?",
"id.verification.camera.help.upload.answer": "Sur la page suivante, vous aurez la possibilité de passer en mode téléchargement. En sélectionnant cette option, vous pourrez télécharger une photo à la place.",
"id.verification.id.photo.unclear.question": "L'image de votre carte d'identité n'est pas claire ou trop floue ?",
"id.verification.id.tips.title": "Conseils utiles pour les cartes d'identité",
"id.verification.id.tips.description": "Ensuite, nous vous demanderons de prendre une photo d'une carte d'identité valide comportant votre nom complet et votre photo, comme un permis de conduire ou un passeport. Veuillez préparer votre carte d'identité.",
"id.verification.id.tips.list.well.lit": "Votre carte d'identité est bien éclairée.",
"id.verification.id.tips.list.clear": "Assurez-vous que vous pouvez voir votre photo et lire clairement votre nom.",
"id.verification.id.photo.title.camera": "Prenez une photo de votre carte d'identité",
"id.verification.id.photo.title.upload": "Téléversez une photo de votre carte d'identité",
"id.verification.id.photo.preview.alt": "Aperçu de la pièce d'identité avec photo.",
"id.verification.id.photo.instructions.camera": "Lorsque votre pièce d'identité est en place, utilisez le bouton Prendre une photo ci-dessous pour prendre votre photo. Veuillez utiliser un passeport, un permis de conduire ou une autre carte d'identité comportant votre nom complet et une photo de votre visage.",
"id.verification.id.photo.instructions.upload": "Veuillez téléverser une photo de votre carte d'identité. Assurez-vous que la totalité de la carte d'identité rentre dans le cadre et qu'elle est bien éclairée. La taille du fichier doit être inférieure à 10 Mo. Formats pris en charge : ",
"id.verification.id.photo.instructions.upload.error.invalidFileType": "Le fichier que vous avez sélectionné n'est pas un type d'image pris en charge. Veuillez choisir parmi les formats suivants :",
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "Le fichier que vous avez sélectionné est trop volumineux. Veuillez réessayer avec un fichier de moins de 10 Mo.",
"id.verification.account.name.title": "Vérification du nom du compte",
"id.verification.name.check.title": "Vérifiez votre nom",
"id.verification.account.name.instructions": "Le nom de votre compte et le nom de votre pièce d'identité doivent correspondre exactement. Sinon, cliquez sur \"Non\" pour mettre à jour le nom de votre compte.",
"id.verification.name.check.instructions": "Le nom ci-dessous correspond-il au nom figurant sur votre pièce d'identité officielle ? Si ce n'est pas le cas, mettez à jour le nom ci-dessous afin qu'il corresponde à votre pièce d'identité.",
"id.verification.name.check.mismatch.information": "Si le nom ci-dessous ne correspond pas à votre pièce d'identité, la vérification de votre identité sera refusée.",
"id.verification.account.name.radio.label": "Le nom sur votre pièce d'identité correspond-il au nom du compte ci-dessous?",
"id.verification.name.check.radio.label": "Sélectionnez une option",
"id.verification.account.name.radio.yes": "Oui",
"id.verification.name.check.radio.yes": "Oui, le nom ci-dessous correspond à mon ID",
"id.verification.account.name.radio.no": "Non",
"id.verification.name.check.radio.no": "Non, le nom ci-dessous ne correspond pas à mon ID",
"id.verification.account.name.error": "Veuillez mettre à jour le nom de votre compte pour qu'il corresponde au nom sur votre pièce d'identité.",
"id.verification.account.name.warning.prefix": "Veuillez noter:",
"id.verification.account.name.settings": "Paramètres du compte",
"id.verification.account.name.label": "Nom du compte",
"id.verification.name.label": "Nom",
"id.verification.account.name.photo.alt": "Photo de votre pièce d'identité à soumettre.",
"id.verification.account.name.save": "Enregistrer et suivant",
"id.verification.review.title": "Vérifiez vos photos",
"id.verification.review.description": "Assurez-vous que nous pourrons vérifier votre identité avec les photos et les informations fournies.",
"id.verification.review.portrait.label": "Votre portrait",
"id.verification.review.portrait.alt": "Photo de votre visage à soumettre.",
"id.verification.review.portrait.retake": "Reprendre la photo de portrait",
"id.verification.review.id.label": "Votre carte d'identité",
"id.verification.review.id.alt": "Photo de votre carte d'identité à présenter.",
"id.verification.review.id.retake": "Reprendre la photo de la pièce d'identité",
"id.verification.review.confirm": "Envoyez",
"id.verification.submission.alert.error.face": "Une photo de votre visage est requise. Veuillez reprendre votre photo de portrait.",
"id.verification.submission.alert.error.id": "Une photo de votre pièce d'identité est requise. Veuillez reprendre votre photo d'identité.",
"id.verification.submission.alert.error.name": "Un nom de compte valide est requis. Veuillez mettre à jour le nom de votre compte pour qu'il corresponde au nom sur votre pièce d'identité.",
"id.verification.submission.alert.error.unsupported": "Un ou plusieurs des fichiers que vous avez téléchargés sont dans un format non pris en charge. Veuillez choisir parmi les formats suivants :",
"id.verification.review.error": "Page de Support {siteName}",
"id.verification.submitted.title": "Vérification d'identité en cours",
"id.verification.submitted.text": "Nous avons reçu vos informations et vérifions votre identité. Vous verrez un message sur votre tableau de bord lorsque le processus de vérification sera terminé (généralement dans les 5 jours). En attendant, vous pouvez toujours accéder à tous les contenus de cours disponibles.",
"id.verification.return.dashboard": "Retour au Tableau de bord",
"id.verification.return.course": "Revenir au cours",
"id.verification.photo.upload.help.title": "Téléchargez une photo à la place",
"id.verification.photo.camera.help.title": "Utilisez votre appareil photo à la place",
"id.verification.photo.upload.help.text": "Si vous rencontrez des difficultés lors de l'utilisation de la capture de photo ci-dessus, vous souhaiterez peut-être télécharger une photo à la place. Pour télécharger une photo, cliquez sur le bouton ci-dessous.",
"id.verification.photo.camera.help.text": "Si vous rencontrez des difficultés pour télécharger une photo ci-dessus, vous pouvez utiliser votre appareil photo à la place. Pour utiliser votre appareil photo, cliquez sur le bouton ci-dessous.",
"id.verification.upload.help.button": "Passer en mode de téléchargement",
"id.verification.camera.help.button": "Passer en mode caméra",
"id.verification.choose.mode.title": "Options d'exigences relatives aux photos",
"id.verification.choose.mode.hep.text": "Pour terminer la vérification, veuillez sélectionner l'une des options suivantes pour soumettre des photos. Vous pourrez basculer entre ces options tout au long du processus si nécessaire.",
"id.verification.choose.mode.radio.upload": "Télécharger des photos depuis mon appareil",
"id.verification.choose.mode.radio.camera": "Prendre des photos avec mon appareil photo",
"id.verification.account.name.managed.alert": "Les paramètres de votre profil sont gérés par {managerTitle}, vous n'êtes donc pas autorisé à mettre à jour votre nom. Veuillez contacter votre administrateur {profileDataManager} ou {support} pour obtenir de l'aide.",
"id.verification.request.camera.access.instructions": "Afin de prendre une photo à l'aide de votre webcam, vous pouvez recevoir une invite du navigateur pour accéder à votre caméra. {clickAllow}",
"id.verification.requirements.account.managed.alert": "Les paramètres de votre compte sont gérés par {managerTitle}. Si le nom sur votre pièce d'identité avec photo ne correspond pas au nom de votre compte, veuillez contacter votre administrateur {profileDataManager} ou {support} pour obtenir de l'aide avant de terminer le processus de vérification des photos.",
"id.verification.requirements.card.device.text": "Vous avez besoin d'un appareil équipé d'une caméra. Si vous recevez une invite du navigateur pour accéder à votre caméra, assurez-vous de cliquer sur {allow}.",
"id.verification.account.name.summary.alert": "Les paramètres de votre compte sont gérés par {managerTitle}. Si le nom sur votre pièce d'identité avec photo ne correspond pas au nom de votre compte, veuillez contacter votre administrateur {profileDataManager} ou {support} pour obtenir de l'aide.",
"idv.submission.alert.error": "\n Nous avons rencontré une erreur technique en essayant de soumettre la vérification d'identité.\n Il peut s'agir d'un problème temporaire. Veuillez réessayer dans quelques minutes.\n Si le problème persiste, veuillez consulter {support_link} pour obtenir de l'aide.\n ",
"id.verification.account.name.edit": "Modifier {sr}"
} }

View File

@@ -1,5 +1,5 @@
{ {
"account.settings.message.duplicate.tpa.provider": "The {provider} account you selected is already linked to another {siteName} account.", "account.settings.message.duplicate.tpa.provider": "The {provider} account you selected is already linked to another edX account.",
"account.settings.message.managed.settings": "Your profile settings are managed by {managerTitle}. Contact your administrator or {support} for help.", "account.settings.message.managed.settings": "Your profile settings are managed by {managerTitle}. Contact your administrator or {support} for help.",
"account.settings.message.managed.settings.support": "support", "account.settings.message.managed.settings.support": "support",
"account.settings.page.heading": "Account Settings", "account.settings.page.heading": "Account Settings",
@@ -14,34 +14,16 @@
"account.settings.section.demographics.information": "Optional Information", "account.settings.section.demographics.information": "Optional Information",
"account.settings.section.site.preferences": "Site Preferences", "account.settings.section.site.preferences": "Site Preferences",
"account.settings.section.linked.accounts": "Linked Accounts", "account.settings.section.linked.accounts": "Linked Accounts",
"account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to {siteName}.", "account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to edX.",
"account.settings.field.username": "Username", "account.settings.field.username": "Username",
"account.settings.field.username.help.text": "The name that identifies you on {siteName}. You cannot change your username.", "account.settings.field.username.help.text": "The name that identifies you on edX. You cannot change your username.",
"account.settings.field.full.name": "Full name", "account.settings.field.full.name": "Full name",
"account.settings.field.full.name.empty": "Add name", "account.settings.field.full.name.empty": "Add name",
"account.settings.field.full.name.help.text": "The name that is used for ID verification and that appears on your certificates.", "account.settings.field.full.name.help.text": "The name that is used for ID verification and that appears on your certificates.",
"account.settings.field.full.name.help.text.non.certificate": "The name that appears on your public profile.",
"account.settings.field.full.name.help.text.certificate": "This name is selected to appear on your certificates and public-facing records.",
"account.settings.field.name.verified": "Verified name",
"account.settings.field.name.verified.help.text.verified": "This name has been verified by government ID.",
"account.settings.field.name.verified.help.text.certificate": "This name has been verified by government ID and selected to appear on your certificates and public-facing records.",
"account.settings.field.name.verified.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Verified name cannot be changed at this time.",
"account.settings.field.name.verified.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Verified name cannot be changed at this time.",
"account.settings.field.name.verified.verification.help": "Enter your name as it appears on your government-issued ID.",
"account.settings.field.full.name.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Full name cannot be changed at this time.",
"account.settings.field.full.name.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Full name cannot be changed at this time.",
"account.settings.field.name.verified.success.message": "Your identity verification request has successfully completed. You now have the option of selecting which name you prefer to appear on your certificates and public-records.",
"account.settings.field.name.verified.success.message.header": "Your name change request is complete!",
"account.settings.field.name.verified.failure.message": "Your most recent identity verification attempt did not pass. Related account settings have been restored.",
"account.settings.field.name.verified.failure.message.header": "We were not able to verify your identity.",
"account.settings.field.name.verified.failure.message.help.link": "Learn more about ID verification",
"account.settings.field.name.verified.submitted.message": "Your identity verification request has been submitted and usually takes between 24 and 48 hours to complete.",
"account.settings.field.name.verified.submitted.message.certificate": "When your request is approved, your updated name will appear on all associated certificates and public-facing records.",
"account.settings.field.name.verified.submitted.message.header": "Your name change request is almost complete!",
"account.settings.field.email": "Email address (Sign in)", "account.settings.field.email": "Email address (Sign in)",
"account.settings.field.email.empty": "Add email address", "account.settings.field.email.empty": "Add email address",
"account.settings.field.email.confirmation": "Weve sent a confirmation message to {value}. Click the link in the message to update your email address.", "account.settings.field.email.confirmation": "Weve sent a confirmation message to {value}. Click the link in the message to update your email address.",
"account.settings.field.email.help.text": "You receive messages from {siteName} and course teams at this address.", "account.settings.field.email.help.text": "You receive messages from edX and course teams at this address.",
"account.settings.field.secondary.email": "Recovery email address", "account.settings.field.secondary.email": "Recovery email address",
"account.settings.field.secondary.email.empty": "Add a recovery email address", "account.settings.field.secondary.email.empty": "Add a recovery email address",
"account.settings.field.secondary.email.confirmation": "Weve sent a confirmation message to {value}. Click the link in the message to update your recovery email address.", "account.settings.field.secondary.email.confirmation": "Weve sent a confirmation message to {value}. Click the link in the message to update your recovery email address.",
@@ -75,7 +57,7 @@
"account.settings.field.gender.options.f": "Female", "account.settings.field.gender.options.f": "Female",
"account.settings.field.gender.options.m": "Male", "account.settings.field.gender.options.m": "Male",
"account.settings.field.gender.options.o": "Other", "account.settings.field.gender.options.o": "Other",
"account.settings.field.language.proficiencies": "Spoken language", "account.settings.field.language.proficiencies": "Spoken languages",
"account.settings.field.language.proficiencies.empty": "Add a spoken language", "account.settings.field.language.proficiencies.empty": "Add a spoken language",
"account.settings.field.language_proficiencies.options.empty": "Select a Language", "account.settings.field.language_proficiencies.options.empty": "Select a Language",
"account.settings.field.time.zone": "Time zone", "account.settings.field.time.zone": "Time zone",
@@ -85,7 +67,7 @@
"account.settings.field.time.zone.all": "All time zones", "account.settings.field.time.zone.all": "All time zones",
"account.settings.field.time.zone.country": "Country time zones", "account.settings.field.time.zone.country": "Country time zones",
"account.settings.section.social.media": "Social Media Links", "account.settings.section.social.media": "Social Media Links",
"account.settings.section.social.media.description": "Optionally, link your personal accounts to the social media icons on your {siteName} profile.", "account.settings.section.social.media.description": "Optionally, link your personal accounts to the social media icons on your edX profile.",
"account.settings.field.social.platform.name.linkedin": "LinkedIn", "account.settings.field.social.platform.name.linkedin": "LinkedIn",
"account.settings.field.social.platform.name.linkedin.empty": "Add LinkedIn profile", "account.settings.field.social.platform.name.linkedin.empty": "Add LinkedIn profile",
"account.settings.jump.nav.delete.account": "Delete My Account", "account.settings.jump.nav.delete.account": "Delete My Account",
@@ -98,12 +80,6 @@
"account.settings.editable.field.action.edit": "Edit", "account.settings.editable.field.action.edit": "Edit",
"account.settings.static.field.empty": "No value set. Contact your {enterprise} administrator to make changes.", "account.settings.static.field.empty": "No value set. Contact your {enterprise} administrator to make changes.",
"account.settings.static.field.empty.no.admin": "No value set.", "account.settings.static.field.empty.no.admin": "No value set.",
"account.settings.field.name.certificate.select": "If checked, this name will appear on your certificates and public-facing records.",
"account.settings.field.name.modal.certificate.title": "Choose a preferred name for certificates and public-facing records",
"account.settings.field.name.modal.certificate.select": "Select a name",
"account.settings.field.name.modal.certificate.option.full": "Full Name",
"account.settings.field.name.modal.certificate.option.verified": "Verified Name",
"account.settings.field.name.modal.certificate.button.choose": "Choose name",
"account.settings.coaching.consent.welcome.header": "Lets get started.", "account.settings.coaching.consent.welcome.header": "Lets get started.",
"account.settings.coaching.consent.welcome.subheader": "We're here for you from start to finish", "account.settings.coaching.consent.welcome.subheader": "We're here for you from start to finish",
"account.settings.coaching.consent.description": "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.*", "account.settings.coaching.consent.description": "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.*",
@@ -125,19 +101,17 @@
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.", "account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
"account.settings.delete.account.header": "Delete My Account", "account.settings.delete.account.header": "Delete My Account",
"account.settings.delete.account.subheader": "We're sorry to see you go!", "account.settings.delete.account.subheader": "We're sorry to see you go!",
"account.settings.delete.account.text.1": "Please note: Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.", "account.settings.delete.account.text.1": "Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.",
"account.settings.delete.account.text.2": "Once your account is deleted, you cannot use it to take courses on {siteName}.", "account.settings.delete.account.text.2": "Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.",
"account.settings.delete.account.text.2.edX": "Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.", "account.settings.delete.account.text.3.link": "follow the instructions for printing or downloading a certificate",
"account.settings.delete.account.text.3.link": "Follow these instructions for printing or downloading a certificate", "account.settings.delete.account.text.warning": "Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.",
"account.settings.delete.account.text.warning": "Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on {siteName}.",
"account.settings.delete.account.text.change.instead": "Want to change your email, name, or password instead?", "account.settings.delete.account.text.change.instead": "Want to change your email, name, or password instead?",
"account.settings.delete.account.button": "Delete My Account", "account.settings.delete.account.button": "Delete My Account",
"account.settings.delete.account.please.activate": "activate your account", "account.settings.delete.account.please.activate": "activate your account",
"account.settings.delete.account.please.unlink": "unlink all social media accounts", "account.settings.delete.account.please.unlink": "unlink all social media accounts",
"account.settings.delete.account.modal.header": "Are you sure?", "account.settings.delete.account.modal.header": "Are you sure?",
"account.settings.delete.account.modal.text.1": "You have selected \"Delete My Account\". Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.", "account.settings.delete.account.modal.text.1": "You have selected \"Delete My Account\". Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.",
"account.settings.delete.account.modal.text.2": "If you proceed, you will be unable to use this account to take courses on {siteName}.", "account.settings.delete.account.modal.text.2": "If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.",
"account.settings.delete.account.modal.text.2.edX": "If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.",
"account.settings.delete.account.modal.enter.password": "If you still wish to continue and delete your account, please enter your account password:", "account.settings.delete.account.modal.enter.password": "If you still wish to continue and delete your account, please enter your account password:",
"account.settings.delete.account.modal.confirm.delete": "Yes, Delete", "account.settings.delete.account.modal.confirm.delete": "Yes, Delete",
"account.settings.delete.account.modal.confirm.cancel": "Cancel", "account.settings.delete.account.modal.confirm.cancel": "Cancel",
@@ -148,31 +122,67 @@
"account.settings.delete.account.modal.after.header": "We're sorry to see you go! Your account will be deleted shortly.", "account.settings.delete.account.modal.after.header": "We're sorry to see you go! Your account will be deleted shortly.",
"account.settings.delete.account.modal.after.text": "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.", "account.settings.delete.account.modal.after.text": "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.",
"account.settings.delete.account.modal.after.button": "Close", "account.settings.delete.account.modal.after.button": "Close",
"account.settings.delete.account.text.3.edX": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. You can make a copy of these for your records before proceeding with deletion. {actionLink}.", "account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
"account.settings.delete.account.text.3": "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.",
"account.settings.message.demographics.service.issue": "An error occurred attempting to retrieve or save your account information. Please try again later.",
"account.settings.field.demographics.gender": "Gender identity", "account.settings.field.demographics.gender": "Gender identity",
"account.settings.field.demographics.gender.empty": "Add gender identity", "account.settings.field.demographics.gender.empty": "Add gender identity",
"account.settings.field.demographics.gender.options.empty": "Select a gender identity", "account.settings.field.demographics.gender.options.empty": "Select a gender identity",
"account.settings.field.demographics.gender.options.woman": "Woman",
"account.settings.field.demographics.gender.options.man": "Man",
"account.settings.field.demographics.gender.options.non-binary": "Non-binary",
"account.settings.field.demographics.gender.options.self-describe": "Prefer to self-describe",
"account.settings.field.demographics.gender_description": "Gender identity description", "account.settings.field.demographics.gender_description": "Gender identity description",
"account.settings.field.demographics.gender_description.empty": "Enter description", "account.settings.field.demographics.gender_description.empty": "Enter description",
"account.settings.field.demographics.ethnicity": "Race/Ethnicity identity", "account.settings.field.demographics.ethnicity": "Race/Ethnicity identity",
"account.settings.field.demographics.ethnicity.empty": "Add race/ethnicity identity", "account.settings.field.demographics.ethnicity.empty": "Add race/ethnicity identity",
"account.settings.field.demographics.ethnicity.options.empty": "Select all that apply", "account.settings.field.demographics.ethnicity.options.empty": "Select all that apply",
"account.settings.field.demographics.ethnicity.options.american-indian-or-alaska-native": "American Indian or Alaska Native",
"account.settings.field.demographics.ethnicity.options.asian": "Asian",
"account.settings.field.demographics.ethnicity.options.black-or-african-american": "Black or African American",
"account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish": "Hispanic, Latin, or Spanish origin",
"account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african": "Middle Eastern or North African",
"account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander": "Native Hawaiian or Other Pacific Islander",
"account.settings.field.demographics.ethnicity.options.white": "White",
"account.settings.field.demographics.ethnicity.options.other": "Some other race, ethnicity, or origin",
"account.settings.field.demographics.income": "Family income", "account.settings.field.demographics.income": "Family income",
"account.settings.field.demographics.income.empty": "Add family income", "account.settings.field.demographics.income.empty": "Add family income",
"account.settings.field.demographics.income.options.empty": "Select a family income range", "account.settings.field.demographics.income.options.empty": "Select a family income range",
"account.settings.field.demographics.income.options.less-than-10k": "Less than US $10,000",
"account.settings.field.demographics.income.options.10k-25k": "US $10,000 - $25,000",
"account.settings.field.demographics.income.options.25k-50k": "US $25,000 - $50,000",
"account.settings.field.demographics.income.options.50k-75k": "US $50,000 - $75,000",
"account.settings.field.demographics.income.options.over-100k": "Over US $100,000",
"account.settings.field.demographics.income.options.unsure": "I don't know",
"account.settings.field.demographics.military_history": "U.S. Military status", "account.settings.field.demographics.military_history": "U.S. Military status",
"account.settings.field.demographics.military_history.empty": "Add military status", "account.settings.field.demographics.military_history.empty": "Add military status",
"account.settings.field.demographics.military_history.options.empty": "Select military status", "account.settings.field.demographics.military_history.options.empty": "Select military status",
"account.settings.field.demographics.income.options.never-served": "Never served in the military",
"account.settings.field.demographics.income.options.training": "Only on active duty for training",
"account.settings.field.demographics.income.options.active": "Now on active duty",
"account.settings.field.demographics.income.options.previously-active": "On active duty in the past, but not now",
"account.settings.field.demographics.learner_education_level": "Your education level", "account.settings.field.demographics.learner_education_level": "Your education level",
"account.settings.field.demographics.learner_education_level.empty": "Add education level", "account.settings.field.demographics.learner_education_level.empty": "Add education level",
"account.settings.field.demographics.parent_education_level": "Parents/Guardians education level", "account.settings.field.demographics.parent_education_level": "Parents/Guardians education level",
"account.settings.field.demographics.parent_education_level.empty": "Add education level", "account.settings.field.demographics.parent_education_level.empty": "Add education level",
"account.settings.field.demographics.education_level.options.empty": "Select education level", "account.settings.field.demographics.education_level.options.empty": "Select education level",
"account.settings.field.demographics.education_level.options.no-high-school": "No High School",
"account.settings.field.demographics.education_level.options.some-high-school": "Some High School",
"account.settings.field.demographics.education_level.options.high-school-ged-equivalent": "High School diploma, GED, or equivalent",
"account.settings.field.demographics.education_level.options.some-college": "Some college, but no degree",
"account.settings.field.demographics.education_level.options.associates": "Associates degree",
"account.settings.field.demographics.education_level.options.bachelors": "Bachelors degree",
"account.settings.field.demographics.education_level.options.masters": "Masters degree",
"account.settings.field.demographics.education_level.options.professional": "Professional degree",
"account.settings.field.demographics.education_level.options.doctorate": "Doctorate degree",
"account.settings.field.demographics.work_status": "Employment status", "account.settings.field.demographics.work_status": "Employment status",
"account.settings.field.demographics.work_status.empty": "Add employment status", "account.settings.field.demographics.work_status.empty": "Add employment status",
"account.settings.field.demographics.work_status.options.empty": "Select employment status", "account.settings.field.demographics.work_status.options.empty": "Select employment status",
"account.settings.field.demographics.work_status.options.full-time": "Employed, working full-time",
"account.settings.field.demographics.work_status.options.part-time": "Employed, working part-time",
"account.settings.field.demographics.work_status.options.not-employed-looking": "Not employed, looking for work",
"account.settings.field.demographics.work_status.options.not-employed-not-looking": "Not employed, not looking for work",
"account.settings.field.demographics.work_status.options.unable": "Unable to work",
"account.settings.field.demographics.work_status.options.retired": "Retired",
"account.settings.field.demographics.work_status.options.other": "Other",
"account.settings.field.demographics.work_status_description": "Employment status description", "account.settings.field.demographics.work_status_description": "Employment status description",
"account.settings.field.demographics.work_status_description.empty": "Enter description", "account.settings.field.demographics.work_status_description.empty": "Enter description",
"account.settings.field.demographics.current_work_sector": "Current work industry", "account.settings.field.demographics.current_work_sector": "Current work industry",
@@ -180,17 +190,27 @@
"account.settings.field.demographics.future_work_sector": "Future work industry", "account.settings.field.demographics.future_work_sector": "Future work industry",
"account.settings.field.demographics.future_work_sector.empty": "Add work industry", "account.settings.field.demographics.future_work_sector.empty": "Add work industry",
"account.settings.field.demographics.work_sector.options.empty": "Select work industry", "account.settings.field.demographics.work_sector.options.empty": "Select work industry",
"account.settings.section.demographics.why": "Why does {siteName} collect this information?", "account.settings.field.demographics.work_sector.options.accommodation-food": "Accommodation and Food Services",
"account.settings.name.change.title.id": "This name change requires identity verification", "account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation": "Administrative and Support and Waste Management and Remediation Services",
"account.settings.name.change.title.begin": "Before we begin", "account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting": "Agriculture, Forestry, Fishing and Hunting",
"account.settings.name.change.warning.one": "Warning: This action updates the name that appears on all certificates that have been earned on this account in the past and any certificates you are currently earning or will earn in the future.", "account.settings.field.demographics.work_sector.options.arts-entertainment-recreation": "Arts, Entertainment, and Recreation",
"account.settings.name.change.warning.two": "This action cannot be undone without verifying your identity.", "account.settings.field.demographics.work_sector.options.construction": "Construction",
"account.settings.name.change.id.name.label": "Enter your name as it appears on your government-issued ID.", "account.settings.field.demographics.work_sector.options.educational": "Education Services",
"account.settings.name.change.id.name.placeholder": "Enter the name on your government ID", "account.settings.field.demographics.work_sector.options.finance-insurance": "Finance and Insurance",
"account.settings.name.change.error.valid.name": "Please enter a valid name.", "account.settings.field.demographics.work_sector.options.healthcare-social": "Health Care and Social Assistance",
"account.settings.name.change.error.general": "A technical error occurred. Please try again.", "account.settings.field.demographics.work_sector.options.information": "Information",
"account.settings.name.change.continue": "Continue", "account.settings.field.demographics.work_sector.options.management": "Management of Companies and Enterprises",
"account.settings.name.change.cancel": "Cancel", "account.settings.field.demographics.work_sector.options.manufacturing": "Manufacturing",
"account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas": "Mining, Quarrying, and Oil and Gas Extraction",
"account.settings.field.demographics.work_sector.options.professional-scientific-technical": "Professional, Scientific, and Technical Services",
"account.settings.field.demographics.work_sector.options.public-admin": "Public Administration",
"account.settings.field.demographics.work_sector.options.real-estate": "Real Estate and Rental and Leasing",
"account.settings.field.demographics.work_sector.options.retail": "Retail Trade",
"account.settings.field.demographics.work_sector.options.transport-warehousing": "Transportation and Warehousing",
"account.settings.field.demographics.work_sector.options.utilities": "Utilities",
"account.settings.field.demographics.work_sector.options.trade": "Wholesale Trade",
"account.settings.field.demographics.work_sector.options.other": "Other",
"account.settings.field.demographics.options.declined": "Prefer not to respond",
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.", "error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support", "account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
"account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.", "account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
@@ -202,162 +222,75 @@
"account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.", "account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
"account.settings.sso.unlink.account": "Unlink {name} account", "account.settings.sso.unlink.account": "Unlink {name} account",
"account.settings.sso.no.providers": "No accounts can be linked at this time.", "account.settings.sso.no.providers": "No accounts can be linked at this time.",
"id.verification.access.blocked.denied": "You cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}.",
"id.verification.next": "Next", "id.verification.next": "Next",
"id.verification.support": "support",
"id.verification.continue.upload": "Continue with Upload",
"id.verification.example.card.alt": "Example of a valid identification card with a full name and photo.",
"id.verification.requirements.title": "Photo Verification Requirements", "id.verification.requirements.title": "Photo Verification Requirements",
"id.verification.requirements.description": "In order to complete Photo Verification, you will need the following:", "id.verification.requirements.description": "In order to complete Photo Verification online, you will need the following:",
"id.verification.requirements.card.device.title": "Device with Camera", "id.verification.requirements.card.device.title": "Device with Camera",
"id.verification.requirements.card.device.allow": "Allow", "id.verification.requirements.card.device.allow": "Allow",
"id.verification.requirements.card.id.title": "Photo Identification Card", "id.verification.requirements.card.id.title": "Photo Identification",
"id.verification.requirements.card.id.text": "You need a valid identification card that contains your full name and photo, such as a drivers license or passport.", "id.verification.requirements.card.id.text": "You need a valid ID that contains your full name and photo.",
"id.verification.privacy.title": "Privacy Information", "id.verification.privacy.title": "Privacy Information",
"id.verification.privacy.need.photo.question": "Why does {siteName} need my photo?", "id.verification.privacy.need.photo.question": "Why does edX need my photo?",
"id.verification.privacy.need.photo.answer": "We use your verification photos to confirm your identity and ensure the validity of your certificate.", "id.verification.privacy.need.photo.answer": "We use your verification photos to confirm your identity and ensure the validity of your certificate.",
"id.verification.privacy.do.with.photo.question": "What does {siteName} do with this photo?", "id.verification.privacy.do.with.photo.question": "What does edX do with this photo?",
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on {siteName} after the verification process is complete.", "id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
"id.verification.access.blocked.title": "Identity Verification", "id.verification.existing.request.title": "Identity Verification",
"id.verification.access.blocked.enrollment": "You are not currently enrolled in a course that requires identity verification.", "id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).",
"id.verification.access.blocked.pending": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).", "id.verification.existing.request.denied.text": "You cannot verify your identity at this time.",
"id.verification.photo.take": "Take Photo", "id.verification.photo.take": "Take Photo",
"id.verification.photo.retake": "Retake Photo?", "id.verification.photo.retake": "Retake Photo",
"id.verification.photo.enable.detection": "Enable Face Detection",
"id.verification.photo.enable.detection.portrait.help.text": "If checked, a box will appear around your face. Your face can be seen clearly if the box around it is blue. If your face is not in a good position or undetectable, the box will be red.",
"id.verification.photo.enable.detection.id.help.text": "If checked, a box will appear around the face on your ID card. The face can be seen clearly if the box around it is blue. If the face is not in a good position or undetectable, the box will be red.",
"id.verification.photo.feedback.correct": "Face is in a good position.",
"id.verification.photo.feedback.two.faces": "More than one face detected.",
"id.verification.photo.feedback.no.faces": "No face detected.",
"id.verification.photo.feedback.top.left": "Incorrect position. Top left.",
"id.verification.photo.feedback.top.center": "Incorrect position. Top center.",
"id.verification.photo.feedback.top.right": "Incorrect position. Top right.",
"id.verification.photo.feedback.center.left": "Incorrect position. Center left.",
"id.verification.photo.feedback.center.center": "Incorrect position. Too close to camera.",
"id.verification.photo.feedback.center.right": "Incorrect position. Center right.",
"id.verification.photo.feedback.bottom.left": "Incorrect position. Bottom left.",
"id.verification.photo.feedback.bottom.center": "Incorrect position. Bottom center.",
"id.verification.photo.feedback.bottom.right": "Incorrect position. Bottom right.",
"id.verification.camera.access.title": "Camera Permissions", "id.verification.camera.access.title": "Camera Permissions",
"id.verification.camera.access.title.success": "Camera Access Enabled",
"id.verification.camera.access.title.failed": "Camera Access Failed",
"id.verification.camera.access.click.allow": "Please make sure to click \"Allow\"", "id.verification.camera.access.click.allow": "Please make sure to click \"Allow\"",
"id.verification.camera.access.enable": "Enable Camera", "id.verification.camera.access.enable": "Enable Camera",
"id.verification.camera.access.problems": "Having problems?", "id.verification.camera.access.problems": "Having problems?",
"id.verification.camera.access.skip": "Skip and upload image files instead", "id.verification.camera.access.skip": "Skip and upload image files instead",
"id.verification.camera.access.success": "Looks like your camera is working and ready.", "id.verification.camera.access.success": "Looks like your camera is working and ready.",
"id.verification.camera.access.failure": "It looks like we're unable to access your camera. You will need to upload image files of you and your photo id.", "id.verification.camera.access.failure": "It looks like we're unable to access your camera. You will need to upload image files of you and your photo id.",
"id.verification.camera.access.failure.temporary": "It looks like we're unable to access your camera. Please verify that your webcam is connected and that you have allowed your browser to access it.",
"id.verification.camera.access.failure.temporary.chrome": "To enable camera access in Chrome:",
"id.verification.camera.access.failure.temporary.chrome.step1": "Open Chrome.",
"id.verification.camera.access.failure.temporary.chrome.step2": "Navigate to More > Settings.",
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "For Windows: Alt+F, Alt+E, or F10 followed by the spacebar",
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "For Mac: Command+,",
"id.verification.camera.access.failure.temporary.chrome.step3": "Under the \"Privacy and security\" tab, select \"Site Settings\" and then \"Camera.\"",
"id.verification.camera.access.failure.temporary.chrome.step4": "Under \"Blocked,\" find \"edx.org\" and select it.",
"id.verification.camera.access.failure.temporary.chrome.step5": "In the \"Permissions\" section, update the camera permissions to \"Allow.\"",
"id.verification.camera.access.failure.temporary.ie11": "To enable camera access in Internet Explorer:",
"id.verification.camera.access.failure.temporary.ie11.step1": "Open the Flash Player Settings Manager by navigating to Windows Settings > Control Panel > Flash Player.",
"id.verification.camera.access.failure.temporary.ie11.step2": "Select the \"Camera and Mic\" tab, and then select the \"Camera and Microphone Settings by Site\" button.",
"id.verification.camera.access.failure.temporary.ie11.step3": "Choose \"edx.org\" from the list of websites and change the permissions by selecting \"Allow\" in the dropdown menu.",
"id.verification.camera.access.failure.temporary.firefox": "To enable camera access in Firefox:",
"id.verification.camera.access.failure.temporary.firefox.step1": "Open Firefox.",
"id.verification.camera.access.failure.temporary.firefox.step2": "Enter \"about:preferences\" in the URL bar.",
"id.verification.camera.access.failure.temporary.firefox.step3": "Select the \"Privacy & Security\" tab, and navigate to the \"Permissions\" section.",
"id.verification.camera.access.failure.temporary.firefox.step4": "Next to \"Camera,\" select the \"Settings…\" button.",
"id.verification.camera.access.failure.temporary.firefox.step5": "In the search bar, enter \"edx.org.\"",
"id.verification.camera.access.failure.temporary.firefox.step6": "In the status column for \"edx.org,\" select \"Allow\" from the drop down.",
"id.verification.camera.access.failure.temporary.firefox.step7": "Select \"Save Changes.\"",
"id.verification.camera.access.failure.temporary.safari": "To enable camera access in Safari:",
"id.verification.camera.access.failure.temporary.safari.step1": "Open Safari.",
"id.verification.camera.access.failure.temporary.safari.step2": "Click on the Safari app menu, then select \"Preferences.\" You can also use Command+, as a keyboard shortcut.",
"id.verification.camera.access.failure.temporary.safari.step3": "Select the \"Websites\" tab and then select \"Camera.\"",
"id.verification.camera.access.failure.temporary.safari.step4": "Select \"edx.org\" and change the camera permissions to \"Allow.\"",
"id.verification.camera.access.failure.unsupported": "It looks like your browser does not support camera access.",
"id.verification.camera.access.failure.unsupported.chrome.explanation": "The Chrome browser currently does not support camera access on iOS devices, such as iPhones and iPads.",
"id.verification.camera.access.failure.unsupported.instructions": "Please use another browser to complete Identity Verification.",
"id.verification.photo.tips.title": "Helpful Photo Tips", "id.verification.photo.tips.title": "Helpful Photo Tips",
"id.verification.photo.tips.description": "Next, we'll need you to take a photo of your face. Please review the helpful tips below.", "id.verification.photo.tips.description": "Next, we'll need you to take a photo of your face. Please review the helpful tips below.",
"id.verification.photo.tips.list.title": "Photo Tips", "id.verification.photo.tips.list.title": "Photo Tips",
"id.verification.photo.tips.list.description": "To take a successful photo, make sure that:", "id.verification.photo.tips.list.description": "To take a successful photo, make sure that:",
"id.verification.photo.tips.list.well.lit": "Your face is well-lit.", "id.verification.photo.tips.list.well.lit": "Your face is well-lit.",
"id.verification.photo.tips.list.inside.frame": "Your entire face fits inside the frame.", "id.verification.photo.tips.list.inside.frame": "Your entire face fits inside the frame.",
"id.verification.portrait.photo.title.camera": "Take a Photo of Yourself", "id.verification.portrait.photo.title.camera": "Take Your Photo",
"id.verification.portrait.photo.title.upload": "Upload a Photo of Yourself", "id.verification.portrait.photo.title.upload": "Upload Your Portrait Photo",
"id.verification.portrait.photo.preview.alt": "Preview of photo of user's face.", "id.verification.portrait.photo.preview.alt": "Preview of photo of user's face.",
"id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.", "id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.",
"id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. Supported formats: ", "id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?", "id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?",
"id.verification.camera.help.sight.answer.portrait": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle.", "id.verification.camera.help.sight.answer": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.help.sight.answer.id": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally, the best position for a photo of an ID card is 8-12 inches (20-30 centimeters) from the camera, with the ID card centered relative to the camera. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.", "id.verification.camera.help.head.question": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.help.difficulty.question.portrait": "What if I have difficulty holding my head in position relative to the camera?", "id.verification.camera.help.head.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.camera.help.difficulty.question.id": "What if I have difficulty holding my ID in position relative to the camera?", "id.verification.id.tips.title": "Helpful ID Tips",
"id.verification.camera.help.difficulty.answer": "If you require assistance with taking a photo for submission, contact {siteName} support for additional suggestions.", "id.verification.id.tips.description": "Next you'll need an eligible ID photo, make sure that:",
"id.verification.camera.help.upload.question": "What if I want to upload a photo instead?", "id.verification.id.tips.list.well.lit": "Your ID is well-lit.",
"id.verification.camera.help.upload.answer": "On the next page you will have the option to switch to upload mode. By selecting that option, you will be able to upload a photo instead.",
"id.verification.id.photo.unclear.question": "Is your ID card image not clear or too blurry?",
"id.verification.id.tips.title": "Helpful Identification Card Tips",
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid identification card that includes your full name and photo, such as a drivers license or passport. Please have your ID ready.",
"id.verification.id.tips.list.well.lit": "Your identification card is well-lit.",
"id.verification.id.tips.list.clear": "Ensure that you can see your photo and clearly read your name.", "id.verification.id.tips.list.clear": "Ensure that you can see your photo and clearly read your name.",
"id.verification.id.photo.title.camera": "Take a Photo of Your Identification Card", "id.verification.id.photo.title.camera": "Take ID Photo",
"id.verification.id.photo.title.upload": "Upload a Photo of Your Identification Card", "id.verification.id.photo.title.upload": "Upload Your ID Photo",
"id.verification.id.photo.preview.alt": "Preview of photo ID.", "id.verification.id.photo.preview.alt": "Preview of photo ID.",
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo. Please use a passport, drivers license, or another identification card that includes your full name and a picture of your face.", "id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo.",
"id.verification.id.photo.instructions.upload": "Please upload a photo of your identification card. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ", "id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.id.photo.instructions.upload.error.invalidFileType": "The file you have selected is not a supported image type. Please choose from the following formats: ",
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "The file you have selected is too large. Please try again with a file less than 10MB.",
"id.verification.account.name.title": "Account Name Check", "id.verification.account.name.title": "Account Name Check",
"id.verification.name.check.title": "Double-Check Your Name", "id.verification.account.name.instructions": "Please check the Account Name below to ensure it matches the name on your ID. If not, click \"Edit\".",
"id.verification.account.name.instructions": "The name on your account and the name on your ID must be an exact match. If not, please click \"No\" to update your account name.",
"id.verification.name.check.instructions": "Does the name below match the name on your government-issued ID? If not, update the name below to match your goverment-issued ID.",
"id.verification.name.check.mismatch.information": "If the name below does not match your government-issued ID, your identity verification will be denied.",
"id.verification.account.name.radio.label": "Does the name on your ID match the Account Name below?",
"id.verification.name.check.radio.label": "Select an option",
"id.verification.account.name.radio.yes": "Yes",
"id.verification.name.check.radio.yes": "Yes, the name below matches my ID",
"id.verification.account.name.radio.no": "No",
"id.verification.name.check.radio.no": "No, the name below does not match my ID",
"id.verification.account.name.error": "Please update account name to match the name on your ID.",
"id.verification.account.name.warning.prefix": "Please Note:", "id.verification.account.name.warning.prefix": "Please Note:",
"id.verification.account.name.settings": "Account Settings", "id.verification.account.name.settings": "Account Settings",
"id.verification.account.name.label": "Account Name", "id.verification.account.name.label": "Name on ID",
"id.verification.name.label": "Name", "id.verification.account.name.edit": "Edit",
"id.verification.account.name.photo.alt": "Photo of your ID to be submitted.", "id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
"id.verification.account.name.save": "Save and Next", "id.verification.account.name.save": "Save",
"id.verification.review.title": "Review Your Photos", "id.verification.review.title": "Review Your Photos",
"id.verification.review.description": "Make sure we can verify your identity with the photos and information you have provided.", "id.verification.review.description": "Make sure we can verify your identity with the photos and information you have provided.",
"id.verification.review.portrait.label": "Your Portrait", "id.verification.review.portrait.label": "Your Portrait",
"id.verification.review.portrait.alt": "Photo of your face to be submitted.", "id.verification.review.portrait.alt": "Photo of your face to be submitted.",
"id.verification.review.portrait.retake": "Retake Portrait Photo", "id.verification.review.portrait.retake": "Retake Portrait Photo",
"id.verification.review.id.label": "Your Identification Card", "id.verification.review.id.label": "Your Photo ID",
"id.verification.review.id.alt": "Photo of your identification card to be submitted.", "id.verification.review.id.alt": "Photo of your ID to be submitted.",
"id.verification.review.id.retake": "Retake ID Photo", "id.verification.review.id.retake": "Retake ID Photo",
"id.verification.review.confirm": "Submit", "id.verification.review.confirm": "Confirm",
"id.verification.submission.alert.error.face": "A photo of your face is required. Please retake your portrait photo.",
"id.verification.submission.alert.error.id": "A photo of your ID card is required. Please retake your ID photo.",
"id.verification.submission.alert.error.name": "A valid account name is required. Please update your account name to match the name on your ID.",
"id.verification.submission.alert.error.unsupported": "One or more of the files you have uploaded is in an unsupported format. Please choose from the following: ",
"id.verification.review.error": "{siteName} Support Page",
"id.verification.submitted.title": "Identity Verification in Progress", "id.verification.submitted.title": "Identity Verification in Progress",
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 5 days). In the meantime, you can still access all available course content.", "id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days). In the meantime, you can still access all available course content.",
"id.verification.return.dashboard": "Return to Your Dashboard", "id.verification.submitted.return": "Return to Your Dashboard",
"id.verification.return.course": "Return to Course", "id.verification.account.name.warning": "{prefix} Any edit to your name will be saved to your account and can be reviewed on {accountSettings}.",
"id.verification.photo.upload.help.title": "Upload a Photo Instead",
"id.verification.photo.camera.help.title": "Use Your Camera Instead",
"id.verification.photo.upload.help.text": "If you are having trouble using the photo capture above, you may want to upload a photo instead. To upload a photo, click the button below.",
"id.verification.photo.camera.help.text": "If you are having trouble uploading a photo above, you may want to use your camera instead. To use your camera, click the button below.",
"id.verification.upload.help.button": "Switch to Upload Mode",
"id.verification.camera.help.button": "Switch to Camera Mode",
"id.verification.choose.mode.title": "Photo Requirements Options",
"id.verification.choose.mode.hep.text": "To complete verification, please select one of the following options to submit photos. You will be able to switch between these options throughout the process if needed.",
"id.verification.choose.mode.radio.upload": "Upload photos from my device",
"id.verification.choose.mode.radio.camera": "Take pictures using my camera",
"id.verification.account.name.managed.alert": "Your profile settings are managed by {managerTitle}, so you are not allowed to update your name. Please contact your {profileDataManager} administrator or {support} for help.",
"id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}", "id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
"id.verification.requirements.account.managed.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help before completing the Photo Verification process.", "id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}."
"id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}.",
"id.verification.account.name.summary.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help.",
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists, please go to {support_link} for help.\n ",
"id.verification.account.name.edit": "Edit {sr}"
} }

View File

@@ -1,49 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './IdVerification.messages';
import { ERROR_REASONS } from './IdVerificationContext';
function AccessBlocked({ error, intl }) {
const handleMessage = () => {
if (error === ERROR_REASONS.COURSE_ENROLLMENT) {
return <p>{intl.formatMessage(messages['id.verification.access.blocked.enrollment'])}</p>;
}
if (error === ERROR_REASONS.EXISTING_REQUEST) {
return <p>{intl.formatMessage(messages['id.verification.access.blocked.pending'])}</p>;
}
return (
<FormattedMessage
id="id.verification.access.blocked.denied"
defaultMessage="You cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}."
description="Text that displays when user is denied from making a request, and to check their email for an activation email."
values={{
email: <strong>no-reply@registration.edx.org</strong>,
}}
/>
);
};
return (
<div>
<h3 aria-level="1" tabIndex="-1">
{intl.formatMessage(messages['id.verification.access.blocked.title'])}
</h3>
{handleMessage()}
<div className="action-row">
<a className="btn btn-primary mt-3" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
{intl.formatMessage(messages['id.verification.return.dashboard'])}
</a>
</div>
</div>
);
}
AccessBlocked.propTypes = {
intl: intlShape.isRequired,
error: PropTypes.string.isRequired,
};
export default injectIntl(AccessBlocked);

View File

@@ -1,13 +1,7 @@
/* eslint-disable jsx-a11y/media-has-caption */
/* eslint-disable jsx-a11y/no-access-key */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
// eslint-disable-next-line import/no-unresolved
import * as blazeface from '@tensorflow-models/blazeface';
import CameraPhoto, { FACING_MODES } from 'jslib-html5-camera-photo'; import CameraPhoto, { FACING_MODES } from 'jslib-html5-camera-photo';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Form, Spinner } from '@edx/paragon';
import shutter from './data/camera-shutter.base64.json'; import shutter from './data/camera-shutter.base64.json';
import messages from './IdVerification.messages'; import messages from './IdVerification.messages';
@@ -17,264 +11,38 @@ class Camera extends React.Component {
super(props, context); super(props, context);
this.cameraPhoto = null; this.cameraPhoto = null;
this.videoRef = React.createRef(); this.videoRef = React.createRef();
this.canvasRef = React.createRef();
this.setDetection = this.setDetection.bind(this);
this.state = { this.state = {
trackedObject: null,
dataUri: '', dataUri: '',
videoHasLoaded: false,
shouldDetect: false,
isFinishedLoadingDetection: true,
shouldGiveFeedback: true,
feedback: '',
}; };
} }
componentDidMount() { componentDidMount() {
this.cameraPhoto = new CameraPhoto(this.videoRef.current); this.cameraPhoto = new CameraPhoto(this.videoRef.current);
this.cameraPhoto.startCamera( this.cameraPhoto.startCameraMaxResolution(FACING_MODES.USER);
this.props.isPortrait ? FACING_MODES.USER : FACING_MODES.ENVIRONMENT,
{ width: 640, height: 480 },
);
}
async componentWillUnmount() {
this.cameraPhoto.stopCamera();
}
setDetection() {
this.setState(
(state) => ({ shouldDetect: !state.shouldDetect }),
() => {
if (this.state.shouldDetect) {
this.setState({ isFinishedLoadingDetection: false });
this.startDetection();
}
this.sendEvent();
},
);
}
setVideoHasLoaded() {
this.setState({ videoHasLoaded: 'true' });
}
getGridPosition(coordinates) {
// Used to determine where a face is (i.e. top-left, center-right, bottom-center, etc.)
const x = coordinates[0];
const y = coordinates[1];
let messageBase = 'id.verification.photo.feedback';
const heightUpperLimit = 320;
const heightMiddleLimit = 160;
if (y < heightMiddleLimit) {
messageBase += '.top';
} else if (y < heightUpperLimit && y >= heightMiddleLimit) {
messageBase += '.center';
} else {
messageBase += '.bottom';
}
const widthRightLimit = 213;
const widthMiddleLimit = 427;
if (x < widthRightLimit) {
messageBase += '.right';
} else if (x >= widthRightLimit && x < widthMiddleLimit) {
messageBase += '.center';
} else {
messageBase += '.left';
}
return messageBase;
}
getSizeFactor() {
let sizeFactor = 1;
const settings = this.cameraPhoto.getCameraSettings();
if (settings) {
const videoWidth = settings.width;
const videoHeight = settings.height;
// need to multiply by 3 because each pixel contains 3 bytes
const currentSize = videoWidth * videoHeight * 3;
// chose a limit of 9,999,999 (bytes) so that result will
// always be less than 10MB
const ratio = 9999999 / currentSize;
if (ratio < 1) {
// if the current resolution creates an image larger than 10 MB, adjust sizeFactor (resolution)
// to ensure that image will have a file size of less than 10 MB.
sizeFactor = ratio;
} else if (videoWidth === 640 && videoHeight === 480) {
// otherwise increase the resolution to try and prevent blurry images.
sizeFactor = 2;
}
}
return sizeFactor;
}
detectFromVideoFrame = (model, video) => {
model.estimateFaces(video).then((predictions) => {
if (this.state.shouldDetect && !this.state.dataUri) {
this.showDetections(predictions);
requestAnimationFrame(() => {
this.detectFromVideoFrame(model, video);
});
}
});
};
showDetections = (predictions) => {
let canvasContext;
if (predictions.length > 0) {
canvasContext = this.canvasRef.current.getContext('2d');
canvasContext.clearRect(0, 0, canvasContext.canvas.width, canvasContext.canvas.height);
}
// predictions is an array of objects describing each detected face
predictions.forEach((prediction) => {
const start = [prediction.topLeft[0], prediction.topLeft[1]];
const end = [prediction.bottomRight[0], prediction.bottomRight[1]];
const size = [end[0] - start[0], end[1] - start[1]];
// landmarks is an array of points representing each facial landmark (i.e. right eye, left eye, nose, etc.)
const features = prediction.landmarks;
let isInPosition = true;
// for each of the landmarks, determine if it is in position
for (let j = 0; j < features.length; j++) {
const x = features[j][0];
const y = features[j][1];
let isInRange;
if (this.props.isPortrait) {
isInRange = this.isInRangeForPortrait(x, y);
} else {
isInRange = this.isInRangeForID(x, y);
}
// if it is not in range, give feedback depending on which feature is out of range
isInPosition = isInPosition && isInRange;
}
// draw a box depending on if all landmarks are in position
if (isInPosition) {
canvasContext.strokeStyle = '#00ffff';
canvasContext.lineWidth = 6;
canvasContext.strokeRect(start[0], start[1], size[0], size[1]);
// give positive feedback here if user is in correct position
this.giveFeedback(predictions.length, [], true);
} else {
canvasContext.fillStyle = 'rgba(255, 51, 0, 0.75)';
canvasContext.fillRect(start[0], start[1], size[0], size[1]);
this.giveFeedback(predictions.length, features[0], false);
}
});
if (predictions.length === 0) {
this.giveFeedback(predictions.length, [], false);
}
}
startDetection() {
setTimeout(() => {
if (this.state.videoHasLoaded) {
const loadModelPromise = blazeface.load();
Promise.all([loadModelPromise])
.then((values) => {
this.setState({ isFinishedLoadingDetection: true });
this.detectFromVideoFrame(values[0], this.videoRef.current);
});
} else {
this.setState({ isFinishedLoadingDetection: true });
this.setState({ shouldDetect: false });
// TODO: add error message
}
}, 1000);
}
sendEvent() {
let eventName = 'edx.id_verification';
if (this.props.isPortrait) {
eventName += '.user_photo';
} else {
eventName += '.id_photo';
}
if (this.state.shouldDetect) {
eventName += '.face_detection_enabled';
} else {
eventName += '.face_detection_disabled';
}
sendTrackEvent(eventName);
}
giveFeedback(numFaces, rightEye, isCorrect) {
if (this.state.shouldGiveFeedback) {
const currentFeedback = this.state.feedback;
let newFeedback = '';
if (numFaces === 1) {
// only give feedback if one face is detected otherwise
// it would be difficult to tell a user which face to move
if (isCorrect) {
newFeedback = this.props.intl.formatMessage(messages['id.verification.photo.feedback.correct']);
} else {
// give feedback based on where user is
newFeedback = this.props.intl.formatMessage(messages[this.getGridPosition(rightEye)]);
}
} else if (numFaces > 1) {
newFeedback = this.props.intl.formatMessage(messages['id.verification.photo.feedback.two.faces']);
} else {
newFeedback = this.props.intl.formatMessage(messages['id.verification.photo.feedback.no.faces']);
}
if (currentFeedback !== newFeedback) {
// only update status if it is different, so we don't overload the user with status updates
this.setState({ feedback: newFeedback });
}
// turn off feedback for one to ensure that instructions aren't disruptive/interrupting
this.setState({ shouldGiveFeedback: false });
setTimeout(() => {
this.setState({ shouldGiveFeedback: true });
}, 1000);
}
}
isInRangeForPortrait(x, y) {
return x > 47 && x < 570 && y > 100 && y < 410;
}
isInRangeForID(x, y) {
return x > 120 && x < 470 && y > 120 && y < 350;
} }
takePhoto() { takePhoto() {
if (this.state.dataUri) { if (this.state.dataUri) {
this.reset(); return this.reset();
return;
} }
const config = { const config = {
sizeFactor: this.getSizeFactor(), sizeFactor: 1,
}; };
this.playShutterClick(); this.playShutterClick();
const dataUri = this.cameraPhoto.getDataUri(config); const dataUri = this.cameraPhoto.getDataUri(config);
this.setState({ dataUri }); this.setState({ dataUri });
this.props.onImageCapture(dataUri); this.props.onImageCapture(dataUri);
this.props.setPhotoMode('camera');
} }
playShutterClick() { playShutterClick() {
const audio = new Audio(`data:audio/mp3;base64,${shutter.base64}`); const audio = new Audio('data:audio/mp3;base64,' + shutter.base64);
audio.play(); audio.play();
} }
reset() { reset() {
this.setState({ dataUri: '' }); this.setState({ dataUri: '' });
if (this.state.shouldDetect) {
this.startDetection();
}
} }
render() { render() {
@@ -282,68 +50,25 @@ class Camera extends React.Component {
? 'do-transition camera-flash' ? 'do-transition camera-flash'
: 'camera-flash'; : 'camera-flash';
return ( return (
<div className="camera-outer-wrapper shadow"> <div className='camera-outer-wrapper shadow'>
<Form.Group style={{ textAlign: 'left', padding: '0.5rem', marginBottom: '0.5rem' }}> <div className='camera-wrapper'>
<Form.Check
id="videoDetection"
name="videoDetection"
label={this.props.intl.formatMessage(messages['id.verification.photo.enable.detection'])}
aria-describedby="videoDetectionHelpText"
checked={this.state.shouldDetect}
onChange={this.setDetection}
style={{ padding: '0rem', marginLeft: '1.25rem', float: this.state.isFinishedLoadingDetection ? 'none' : 'left' }}
/>
{!this.state.isFinishedLoadingDetection && <Spinner animation="border" variant="primary" style={{ marginLeft: '0.5rem' }} data-testid="spinner" />}
<Form.Text id="videoDetectionHelpText" data-testid="videoDetectionHelpText">
{this.props.isPortrait
? this.props.intl.formatMessage(messages['id.verification.photo.enable.detection.portrait.help.text'])
: this.props.intl.formatMessage(messages['id.verification.photo.enable.detection.id.help.text'])}
</Form.Text>
</Form.Group>
<div className="camera-wrapper">
<div className={cameraFlashClass} /> <div className={cameraFlashClass} />
<video <video
ref={this.videoRef} ref={this.videoRef}
data-testid="video" autoPlay={true}
autoPlay className='camera-video'
className="camera-video" style={{ display: this.state.dataUri ? 'none' : 'block' }}
onLoadedData={() => { this.setVideoHasLoaded(); }}
style={{
display: this.state.dataUri ? 'none' : 'block',
WebkitTransform: 'scaleX(-1)',
transform: 'scaleX(-1)',
}}
playsInline
/>
<canvas
ref={this.canvasRef}
data-testid="detection-canvas"
className="canvas-video"
style={{
display: !this.state.shouldDetect || this.state.dataUri ? 'none' : 'block',
WebkitTransform: 'scaleX(-1)',
transform: 'scaleX(-1)',
}}
width="640"
height="480"
/> />
<img <img
data-hj-suppress alt='imgCamera'
alt="imgCamera"
src={this.state.dataUri} src={this.state.dataUri}
className="camera-video" className='camera-video'
style={{ display: this.state.dataUri ? 'block' : 'none' }} style={{ display: this.state.dataUri ? 'block' : 'none' }}
/> />
<div role="status" className="sr-only">{this.state.feedback}</div>
</div> </div>
<button <button
type="button" className='btn btn-primary camera-btn'
className={`btn camera-btn ${ accessKey='c'
this.state.dataUri
? 'btn-outline-primary'
: 'btn-primary'
}`}
accessKey="c"
onClick={() => { onClick={() => {
this.takePhoto(); this.takePhoto();
}} }}
@@ -360,8 +85,6 @@ class Camera extends React.Component {
Camera.propTypes = { Camera.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,
onImageCapture: PropTypes.func.isRequired, onImageCapture: PropTypes.func.isRequired,
setPhotoMode: PropTypes.func.isRequired,
isPortrait: PropTypes.bool.isRequired,
}; };
export default injectIntl(Camera); export default injectIntl(Camera);

View File

@@ -1,51 +1,29 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types'; import React from 'react';
import { Collapsible } from '@edx/paragon'; import { Collapsible } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import messages from './IdVerification.messages'; import messages from './IdVerification.messages';
import IdVerificationContext from './IdVerificationContext';
function CameraHelp(props) { function CameraHelp(props) {
const { optimizelyExperimentName } = useContext(IdVerificationContext);
return ( return (
<div> <div>
{ optimizelyExperimentName
&& (
<Collapsible
styling="card"
title={props.intl.formatMessage(messages['id.verification.camera.help.upload.question'])}
className="mb-4 shadow"
defaultOpen={props.isOpen}
>
<p>
{props.intl.formatMessage(messages['id.verification.camera.help.upload.answer'])}
</p>
</Collapsible>
)}
<Collapsible <Collapsible
styling="card" styling="card"
title={props.intl.formatMessage(messages['id.verification.camera.help.sight.question'])} title={props.intl.formatMessage(messages['id.verification.camera.help.sight.question'])}
className="mb-4 shadow" className="mb-4 shadow"
defaultOpen={props.isOpen}
> >
<p> <p>
{props.intl.formatMessage(messages[`id.verification.camera.help.sight.answer.${props.isPortrait ? 'portrait' : 'id'}`])} {props.intl.formatMessage(messages['id.verification.camera.help.sight.answer'])}
</p> </p>
</Collapsible> </Collapsible>
<Collapsible <Collapsible
styling="card" styling="card"
title={props.intl.formatMessage(messages[`id.verification.camera.help.difficulty.question.${props.isPortrait ? 'portrait' : 'id'}`])} title={props.intl.formatMessage(messages['id.verification.camera.help.head.question'])}
className="mb-4 shadow" className="mb-4 shadow"
defaultOpen={props.isOpen}
> >
<p> <p>
{props.intl.formatMessage( {props.intl.formatMessage(messages['id.verification.camera.help.head.answer'])}
messages['id.verification.camera.help.difficulty.answer'],
{ siteName: getConfig().SITE_NAME },
)}
</p> </p>
</Collapsible> </Collapsible>
</div> </div>
@@ -54,13 +32,6 @@ function CameraHelp(props) {
CameraHelp.propTypes = { CameraHelp.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,
isOpen: PropTypes.bool,
isPortrait: PropTypes.bool,
};
CameraHelp.defaultProps = {
isOpen: false,
isPortrait: false,
}; };
export default injectIntl(CameraHelp); export default injectIntl(CameraHelp);

View File

@@ -1,55 +0,0 @@
import React, { useState, useContext } from 'react';
import PropTypes from 'prop-types';
import { Collapsible } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import messages from './IdVerification.messages';
import ImageFileUpload from './ImageFileUpload';
import IdVerificationContext from './IdVerificationContext';
import ImagePreview from './ImagePreview';
import SupportedMediaTypes from './SupportedMediaTypes';
function CameraHelpWithUpload(props) {
const { setIdPhotoFile, idPhotoFile, userId } = useContext(IdVerificationContext);
const [hasUploadedImage, setHasUploadedImage] = useState(false);
function setAndTrackIdPhotoFile(image) {
sendTrackEvent('edx.id_verification.upload_id', {
category: 'id_verification',
user_id: userId,
});
setHasUploadedImage(true);
setIdPhotoFile(image);
}
return (
<div>
<Collapsible
styling="card"
title={props.intl.formatMessage(messages['id.verification.id.photo.unclear.question'])}
data-testid="collapsible"
className="mb-4 shadow"
defaultOpen={props.isOpen}
>
{idPhotoFile && hasUploadedImage && <ImagePreview src={idPhotoFile} alt={props.intl.formatMessage(messages['id.verification.id.photo.preview.alt'])} />}
<p>
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
<SupportedMediaTypes />
</p>
<ImageFileUpload onFileChange={setAndTrackIdPhotoFile} intl={props.intl} />
</Collapsible>
</div>
);
}
CameraHelpWithUpload.propTypes = {
intl: intlShape.isRequired,
isOpen: PropTypes.bool,
};
CameraHelpWithUpload.defaultProps = {
isOpen: false,
};
export default injectIntl(CameraHelpWithUpload);

View File

@@ -1,73 +0,0 @@
import React, { useContext } from 'react';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Collapsible } from '@edx/paragon';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import IdVerificationContext, { MEDIA_ACCESS } from './IdVerificationContext';
import messages from './IdVerification.messages';
function CollapsibleImageHelp(props) {
const {
userId, shouldUseCamera, setShouldUseCamera, optimizelyExperimentName, mediaAccess,
} = useContext(IdVerificationContext);
function handleClick() {
const toggleTo = shouldUseCamera ? 'upload' : 'camera';
const eventName = `edx.id_verification.toggle_to.${toggleTo}`;
sendTrackEvent(eventName, {
category: 'id_verification',
user_id: userId,
});
setShouldUseCamera(!shouldUseCamera);
}
if (optimizelyExperimentName && mediaAccess !== MEDIA_ACCESS.DENIED && mediaAccess !== MEDIA_ACCESS.UNSUPPORTED) {
return (
<Collapsible
styling="card"
title={shouldUseCamera ? props.intl.formatMessage(messages['id.verification.photo.upload.help.title']) : props.intl.formatMessage(messages['id.verification.photo.camera.help.title'])}
className="mb-4 shadow"
defaultOpen
>
<p data-testid="help-text">
{shouldUseCamera
? props.intl.formatMessage(messages['id.verification.photo.upload.help.text'])
: props.intl.formatMessage(messages['id.verification.photo.camera.help.text'])}
</p>
{ (mediaAccess === MEDIA_ACCESS.PENDING && !shouldUseCamera)
? (
// if a user has not enabled camera access yet, and they are trying to switch
// to camera mode, direct them to panel that requests camera access
<Link
to={{ pathname: 'request-camera-access', state: { fromPortraitCapture: props.isPortrait, fromIdCapture: !props.isPortrait } }}
className="btn btn-primary"
data-testid="access-link"
>
{props.intl.formatMessage(messages['id.verification.photo.camera.help.button'])}
</Link>
)
: (
<Button
title={shouldUseCamera ? 'Upload Portrait Photo' : 'Take Portrait Photo'}
data-testid="toggle-button"
onClick={handleClick}
style={{ marginTop: '0.5rem' }}
>
{shouldUseCamera ? props.intl.formatMessage(messages['id.verification.photo.upload.help.button']) : props.intl.formatMessage(messages['id.verification.photo.camera.help.button'])}
</Button>
)}
</Collapsible>
);
}
return null;
}
CollapsibleImageHelp.propTypes = {
intl: intlShape.isRequired,
isPortrait: PropTypes.bool.isRequired,
};
export default injectIntl(CollapsibleImageHelp);

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './IdVerification.messages';
function ExistingRequest(props) {
return (
<div>
<h3 aria-level="1" tabIndex="-1">
{props.intl.formatMessage(messages['id.verification.existing.request.title'])}
</h3>
{props.status === 'pending' || props.status == 'approved'
? <p>{props.intl.formatMessage(messages['id.verification.existing.request.pending.text'])}</p>
: <p>{props.intl.formatMessage(messages['id.verification.existing.request.denied.text'])}</p>
}
<a className="btn btn-primary" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
{props.intl.formatMessage(messages['id.verification.return'])}
</a>
</div>
);
}
ExistingRequest.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(ExistingRequest);

View File

@@ -6,21 +6,6 @@ const messages = defineMessages({
defaultMessage: 'Next', defaultMessage: 'Next',
description: 'Next button.', description: 'Next button.',
}, },
'id.verification.support': {
id: 'id.verification.support',
defaultMessage: 'support',
description: 'Website support.',
},
'id.verification.continue.upload': {
id: 'id.verification.continue.upload',
defaultMessage: 'Continue with Upload',
description: 'Button to continue with upload.',
},
'id.verification.example.card.alt': {
id: 'id.verification.example.card.alt',
defaultMessage: 'Example of a valid identification card with a full name and photo.',
description: 'Alt text for an example identification card.',
},
'id.verification.requirements.title': { 'id.verification.requirements.title': {
id: 'id.verification.requirements.title', id: 'id.verification.requirements.title',
defaultMessage: 'Photo Verification Requirements', defaultMessage: 'Photo Verification Requirements',
@@ -28,7 +13,7 @@ const messages = defineMessages({
}, },
'id.verification.requirements.description': { 'id.verification.requirements.description': {
id: 'id.verification.requirements.description', id: 'id.verification.requirements.description',
defaultMessage: 'In order to complete Photo Verification, you will need the following:', defaultMessage: 'In order to complete Photo Verification online, you will need the following:',
description: 'Description for the Photo Verification Requirements page.', description: 'Description for the Photo Verification Requirements page.',
}, },
'id.verification.requirements.card.device.title': { 'id.verification.requirements.card.device.title': {
@@ -43,12 +28,12 @@ const messages = defineMessages({
}, },
'id.verification.requirements.card.id.title': { 'id.verification.requirements.card.id.title': {
id: 'id.verification.requirements.card.id.title', id: 'id.verification.requirements.card.id.title',
defaultMessage: 'Photo Identification Card', defaultMessage: 'Photo Identification',
description: 'Title for the Photo Identification requirement card.', description: 'Title for the Photo Identification requirement card.',
}, },
'id.verification.requirements.card.id.text': { 'id.verification.requirements.card.id.text': {
id: 'id.verification.requirements.card.id.text', id: 'id.verification.requirements.card.id.text',
defaultMessage: 'You need a valid identification card that contains your full name and photo, such as a drivers license or passport.', defaultMessage: 'You need a valid ID that contains your full name and photo.',
description: 'Text that explains that the user needs a photo ID.', description: 'Text that explains that the user needs a photo ID.',
}, },
'id.verification.privacy.title': { 'id.verification.privacy.title': {
@@ -58,39 +43,39 @@ const messages = defineMessages({
}, },
'id.verification.privacy.need.photo.question': { 'id.verification.privacy.need.photo.question': {
id: 'id.verification.privacy.need.photo.question', id: 'id.verification.privacy.need.photo.question',
defaultMessage: 'Why does {siteName} need my photo?', defaultMessage: 'Why does edX need my photo?',
description: 'Question about why the platform needs a verification photo.', description: 'Question about why edX needs a verification photo.',
}, },
'id.verification.privacy.need.photo.answer': { 'id.verification.privacy.need.photo.answer': {
id: 'id.verification.privacy.need.photo.answer', id: 'id.verification.privacy.need.photo.answer',
defaultMessage: 'We use your verification photos to confirm your identity and ensure the validity of your certificate.', defaultMessage: 'We use your verification photos to confirm your identity and ensure the validity of your certificate.',
description: 'Answering why the platform needs a verification photo.', description: 'Answering why edX needs a verification photo.',
}, },
'id.verification.privacy.do.with.photo.question': { 'id.verification.privacy.do.with.photo.question': {
id: 'id.verification.privacy.do.with.photo.question', id: 'id.verification.privacy.do.with.photo.question',
defaultMessage: 'What does {siteName} do with this photo?', defaultMessage: 'What does edX do with this photo?',
description: 'Question about what the platform does with the verification photo.', description: 'Question about what edX does with the verification photo.',
}, },
'id.verification.privacy.do.with.photo.answer': { 'id.verification.privacy.do.with.photo.answer': {
id: 'id.verification.privacy.do.with.photo.answer', id: 'id.verification.privacy.do.with.photo.answer',
defaultMessage: 'We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on {siteName} after the verification process is complete.', defaultMessage: 'We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.',
description: 'Answering what the platform does with the verification photo.', description: 'Answering what edX does with the verification photo.',
}, },
'id.verification.access.blocked.title': { 'id.verification.existing.request.title': {
id: 'id.verification.access.blocked.title', id: 'id.verification.existing.request.title',
defaultMessage: 'Identity Verification', defaultMessage: 'Identity Verification',
description: 'Title for text that displays when a user is blocked from ID verification.', description: 'Title for text that displays when user has already made a request.',
}, },
'id.verification.access.blocked.enrollment': { 'id.verification.existing.request.pending.text': {
id: 'id.verification.access.blocked.enrollment', id: 'id.verification.existing.request.pending.text',
defaultMessage: 'You are not currently enrolled in a course that requires identity verification.', defaultMessage: 'You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).',
description: 'Text that displays when user is trying to verify while not enrolled in a course that requires ID verification.',
},
'id.verification.access.blocked.pending': {
id: 'id.verification.access.blocked.pending',
defaultMessage: 'You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).',
description: 'Text that displays when user has a pending or approved request.', description: 'Text that displays when user has a pending or approved request.',
}, },
'id.verification.existing.request.denied.text': {
id: 'id.verification.existing.request.denied.text',
defaultMessage: 'You cannot verify your identity at this time.',
description: 'Text that displays when user is denied from making a request.',
},
'id.verification.photo.take': { 'id.verification.photo.take': {
id: 'id.verification.photo.take', id: 'id.verification.photo.take',
defaultMessage: 'Take Photo', defaultMessage: 'Take Photo',
@@ -98,99 +83,14 @@ const messages = defineMessages({
}, },
'id.verification.photo.retake': { 'id.verification.photo.retake': {
id: 'id.verification.photo.retake', id: 'id.verification.photo.retake',
defaultMessage: 'Retake Photo?', defaultMessage: 'Retake Photo',
description: 'Button to retake photo.', description: 'Button to retake photo.',
}, },
'id.verification.photo.enable.detection': {
id: 'id.verification.photo.enable.detection',
defaultMessage: 'Enable Face Detection',
description: 'Text label for the checkbox to enable face detection.',
},
'id.verification.photo.enable.detection.portrait.help.text': {
id: 'id.verification.photo.enable.detection.portrait.help.text',
defaultMessage: 'If checked, a box will appear around your face. Your face can be seen clearly if the box around it is blue. If your face is not in a good position or undetectable, the box will be red.',
description: 'Help text that appears for enabling face detection on the portrait photo panel.',
},
'id.verification.photo.enable.detection.id.help.text': {
id: 'id.verification.photo.enable.detection.id.help.text',
defaultMessage: 'If checked, a box will appear around the face on your ID card. The face can be seen clearly if the box around it is blue. If the face is not in a good position or undetectable, the box will be red.',
description: 'Help text that appears for enabling face detection on the portrait photo panel.',
},
'id.verification.photo.feedback.correct': {
id: 'id.verification.photo.feedback.correct',
defaultMessage: 'Face is in a good position.',
description: 'Text for screen reader when user\'s face is in a good position.',
},
'id.verification.photo.feedback.two.faces': {
id: 'id.verification.photo.feedback.two.faces',
defaultMessage: 'More than one face detected.',
description: 'Text for screen reader when more than one face detected.',
},
'id.verification.photo.feedback.no.faces': {
id: 'id.verification.photo.feedback.no.faces',
defaultMessage: 'No face detected.',
description: 'Text for screen reader when no face detected.',
},
'id.verification.photo.feedback.top.left': {
id: 'id.verification.photo.feedback.top.left',
defaultMessage: 'Incorrect position. Top left.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.top.center': {
id: 'id.verification.photo.feedback.top.center',
defaultMessage: 'Incorrect position. Top center.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.top.right': {
id: 'id.verification.photo.feedback.top.right',
defaultMessage: 'Incorrect position. Top right.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.center.left': {
id: 'id.verification.photo.feedback.center.left',
defaultMessage: 'Incorrect position. Center left.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.center.center': {
id: 'id.verification.photo.feedback.center.center',
defaultMessage: 'Incorrect position. Too close to camera.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.center.right': {
id: 'id.verification.photo.feedback.center.right',
defaultMessage: 'Incorrect position. Center right.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.bottom.left': {
id: 'id.verification.photo.feedback.bottom.left',
defaultMessage: 'Incorrect position. Bottom left.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.bottom.center': {
id: 'id.verification.photo.feedback.bottom.center',
defaultMessage: 'Incorrect position. Bottom center.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.bottom.right': {
id: 'id.verification.photo.feedback.bottom.right',
defaultMessage: 'Incorrect position. Bottom right.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.camera.access.title': { 'id.verification.camera.access.title': {
id: 'id.verification.camera.access.title', id: 'id.verification.camera.access.title',
defaultMessage: 'Camera Permissions', defaultMessage: 'Camera Permissions',
description: 'Title for the Camera Access page.', description: 'Title for the Camera Access page.',
}, },
'id.verification.camera.access.title.success': {
id: 'id.verification.camera.access.title.success',
defaultMessage: 'Camera Access Enabled',
description: 'Title for the Camera Access page when camera is enabled.',
},
'id.verification.camera.access.title.failed': {
id: 'id.verification.camera.access.title.failed',
defaultMessage: 'Camera Access Failed',
description: 'Title for the Camera Access page when camera access is denied or unavailable.',
},
'id.verification.camera.access.click.allow': { 'id.verification.camera.access.click.allow': {
id: 'id.verification.camera.access.click.allow', id: 'id.verification.camera.access.click.allow',
defaultMessage: 'Please make sure to click "Allow"', defaultMessage: 'Please make sure to click "Allow"',
@@ -221,151 +121,6 @@ const messages = defineMessages({
defaultMessage: 'It looks like we\'re unable to access your camera. You will need to upload image files of you and your photo id.', defaultMessage: 'It looks like we\'re unable to access your camera. You will need to upload image files of you and your photo id.',
description: 'Text indicating that the camera could not be accessed and image upload will be enabled.', description: 'Text indicating that the camera could not be accessed and image upload will be enabled.',
}, },
'id.verification.camera.access.failure.temporary': {
id: 'id.verification.camera.access.failure.temporary',
defaultMessage: 'It looks like we\'re unable to access your camera. Please verify that your webcam is connected and that you have allowed your browser to access it.',
description: 'Text indicating that the camera could not be accessed.',
},
'id.verification.camera.access.failure.temporary.chrome': {
id: 'id.verification.camera.access.failure.temporary.chrome',
defaultMessage: 'To enable camera access in Chrome:',
description: 'Description for the directions on enabling camera access.',
},
'id.verification.camera.access.failure.temporary.chrome.step1': {
id: 'id.verification.camera.access.failure.temporary.chrome.step1',
defaultMessage: 'Open Chrome.',
description: 'Text for step one of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.chrome.step2': {
id: 'id.verification.camera.access.failure.temporary.chrome.step2',
defaultMessage: 'Navigate to More > Settings.',
description: 'Text for step two of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.chrome.step2.windows': {
id: 'id.verification.camera.access.failure.temporary.chrome.step2.windows',
defaultMessage: 'For Windows: Alt+F, Alt+E, or F10 followed by the spacebar',
description: 'Text for Windows keyboard shortcut in chrome.',
},
'id.verification.camera.access.failure.temporary.chrome.step2.mac': {
id: 'id.verification.camera.access.failure.temporary.chrome.step2.mac',
defaultMessage: 'For Mac: Command+,',
description: 'Text for Mac keyboard shortcut in chrome.',
},
'id.verification.camera.access.failure.temporary.chrome.step3': {
id: 'id.verification.camera.access.failure.temporary.chrome.step3',
defaultMessage: 'Under the "Privacy and security" tab, select "Site Settings" and then "Camera."',
description: 'Text for step three of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.chrome.step4': {
id: 'id.verification.camera.access.failure.temporary.chrome.step4',
defaultMessage: 'Under "Blocked," find "edx.org" and select it.',
description: 'Text for step four of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.chrome.step5': {
id: 'id.verification.camera.access.failure.temporary.chrome.step5',
defaultMessage: 'In the "Permissions" section, update the camera permissions to "Allow."',
description: 'Text for step five of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.ie11': {
id: 'id.verification.camera.access.failure.temporary.ie11',
defaultMessage: 'To enable camera access in Internet Explorer:',
description: 'Description for the directions on enabling camera access.',
},
'id.verification.camera.access.failure.temporary.ie11.step1': {
id: 'id.verification.camera.access.failure.temporary.ie11.step1',
defaultMessage: 'Open the Flash Player Settings Manager by navigating to Windows Settings > Control Panel > Flash Player.',
description: 'Text for step one of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.ie11.step2': {
id: 'id.verification.camera.access.failure.temporary.ie11.step2',
defaultMessage: 'Select the "Camera and Mic" tab, and then select the "Camera and Microphone Settings by Site" button.',
description: 'Text for step two of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.ie11.step3': {
id: 'id.verification.camera.access.failure.temporary.ie11.step3',
defaultMessage: 'Choose "edx.org" from the list of websites and change the permissions by selecting "Allow" in the dropdown menu.',
description: 'Text for step three of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox': {
id: 'id.verification.camera.access.failure.temporary.firefox',
defaultMessage: 'To enable camera access in Firefox:',
description: 'Description for the directions on enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox.step1': {
id: 'id.verification.camera.access.failure.temporary.firefox.step1',
defaultMessage: 'Open Firefox.',
description: 'Text for step one of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox.step2': {
id: 'id.verification.camera.access.failure.temporary.firefox.step2',
defaultMessage: 'Enter "about:preferences" in the URL bar.',
description: 'Text for step two of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox.step3': {
id: 'id.verification.camera.access.failure.temporary.firefox.step3',
defaultMessage: 'Select the "Privacy & Security" tab, and navigate to the "Permissions" section.',
description: 'Text for step three of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox.step4': {
id: 'id.verification.camera.access.failure.temporary.firefox.step4',
defaultMessage: 'Next to "Camera," select the "Settings…" button.',
description: 'Text for step four of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox.step5': {
id: 'id.verification.camera.access.failure.temporary.firefox.step5',
defaultMessage: 'In the search bar, enter "edx.org."',
description: 'Text for step five of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox.step6': {
id: 'id.verification.camera.access.failure.temporary.firefox.step6',
defaultMessage: 'In the status column for "edx.org," select "Allow" from the drop down.',
description: 'Text for step six of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox.step7': {
id: 'id.verification.camera.access.failure.temporary.firefox.step7',
defaultMessage: 'Select "Save Changes."',
description: 'Text for step seven of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.safari': {
id: 'id.verification.camera.access.failure.temporary.safari',
defaultMessage: 'To enable camera access in Safari:',
description: 'Description for the directions on enabling camera access.',
},
'id.verification.camera.access.failure.temporary.safari.step1': {
id: 'id.verification.camera.access.failure.temporary.safari.step1',
defaultMessage: 'Open Safari.',
description: 'Text for step one of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.safari.step2': {
id: 'id.verification.camera.access.failure.temporary.safari.step2',
defaultMessage: 'Click on the Safari app menu, then select "Preferences." You can also use Command+, as a keyboard shortcut.',
description: 'Text for step two of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.safari.step3': {
id: 'id.verification.camera.access.failure.temporary.safari.step3',
defaultMessage: 'Select the "Websites" tab and then select "Camera."',
description: 'Text for step three of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.safari.step4': {
id: 'id.verification.camera.access.failure.temporary.safari.step4',
defaultMessage: 'Select "edx.org" and change the camera permissions to "Allow."',
description: 'Text for step four of enabling camera access.',
},
'id.verification.camera.access.failure.unsupported': {
id: 'id.verification.camera.access.failure.unsupported',
defaultMessage: 'It looks like your browser does not support camera access.',
description: 'Text indicating that the user\'s browser does not support camera access.',
},
'id.verification.camera.access.failure.unsupported.chrome.explanation': {
id: 'id.verification.camera.access.failure.unsupported.chrome.explanation',
defaultMessage: 'The Chrome browser currently does not support camera access on iOS devices, such as iPhones and iPads.',
description: 'Explanation for why certain web browsers, like Chrome, do not support accessing the user\'s camera.',
},
'id.verification.camera.access.failure.unsupported.instructions': {
id: 'id.verification.camera.access.failure.unsupported.instructions',
defaultMessage: 'Please use another browser to complete Identity Verification.',
description: 'Instructions for the user to user another web browser to complete the process.',
},
'id.verification.photo.tips.title': { 'id.verification.photo.tips.title': {
id: 'id.verification.photo.tips.title', id: 'id.verification.photo.tips.title',
defaultMessage: 'Helpful Photo Tips', defaultMessage: 'Helpful Photo Tips',
@@ -398,12 +153,12 @@ const messages = defineMessages({
}, },
'id.verification.portrait.photo.title.camera': { 'id.verification.portrait.photo.title.camera': {
id: 'id.verification.portrait.photo.title.camera', id: 'id.verification.portrait.photo.title.camera',
defaultMessage: 'Take a Photo of Yourself', defaultMessage: 'Take Your Photo',
description: 'Title for the Portrait Photo page if camera access is enabled.', description: 'Title for the Portrait Photo page if camera access is enabled.',
}, },
'id.verification.portrait.photo.title.upload': { 'id.verification.portrait.photo.title.upload': {
id: 'id.verification.portrait.photo.title.upload', id: 'id.verification.portrait.photo.title.upload',
defaultMessage: 'Upload a Photo of Yourself', defaultMessage: 'Upload Your Portrait Photo',
description: 'Title for the Portrait Photo page if camera access is disabled.', description: 'Title for the Portrait Photo page if camera access is disabled.',
}, },
'id.verification.portrait.photo.preview.alt': { 'id.verification.portrait.photo.preview.alt': {
@@ -418,7 +173,7 @@ const messages = defineMessages({
}, },
'id.verification.portrait.photo.instructions.upload': { 'id.verification.portrait.photo.instructions.upload': {
id: 'id.verification.portrait.photo.instructions.upload', id: 'id.verification.portrait.photo.instructions.upload',
defaultMessage: 'Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. Supported formats: ', defaultMessage: 'Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)',
description: 'Instructions for portrait photo upload.', description: 'Instructions for portrait photo upload.',
}, },
'id.verification.camera.help.sight.question': { 'id.verification.camera.help.sight.question': {
@@ -426,59 +181,34 @@ const messages = defineMessages({
defaultMessage: 'What if I can\'t see the camera image or if I can\'t see my photo to determine which side is visible?', defaultMessage: 'What if I can\'t see the camera image or if I can\'t see my photo to determine which side is visible?',
description: 'Question on what to do if the user cannot see the camera image or photo during verification.', description: 'Question on what to do if the user cannot see the camera image or photo during verification.',
}, },
'id.verification.camera.help.sight.answer.portrait': { 'id.verification.camera.help.sight.answer': {
id: 'id.verification.camera.help.sight.answer.portrait', id: 'id.verification.camera.help.sight.answer',
defaultMessage: 'You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle.', defaultMessage: 'You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.',
description: 'Confirming what to do if the camera image of the portrait cannot be seen during verification.', description: 'Confirming what to do if the camera image or photo cannot be seen during verification.',
}, },
'id.verification.camera.help.sight.answer.id': { 'id.verification.camera.help.head.question': {
id: 'id.verification.camera.help.sight.answer.id', id: 'id.verification.camera.help.head.question',
defaultMessage: 'You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally, the best position for a photo of an ID card is 8-12 inches (20-30 centimeters) from the camera, with the ID card centered relative to the camera. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.',
description: 'Confirming what to do if the camera image of the ID cannot be seen during verification.',
},
'id.verification.camera.help.difficulty.question.portrait': {
id: 'id.verification.camera.help.difficulty.question.portrait',
defaultMessage: 'What if I have difficulty holding my head in position relative to the camera?', defaultMessage: 'What if I have difficulty holding my head in position relative to the camera?',
description: 'Question on what to do if the user has difficulty holding their head relative to the camera.', description: 'Question on what to do if the user has difficulty holding their head relative to the camera.',
}, },
'id.verification.camera.help.difficulty.question.id': { 'id.verification.camera.help.head.answer': {
id: 'id.verification.camera.help.difficulty.question.id', id: 'id.verification.camera.help.head.answer',
defaultMessage: 'What if I have difficulty holding my ID in position relative to the camera?', defaultMessage: 'If you require assistance with taking a photo for submission, contact edX support for additional suggestions.',
description: 'Question on what to do if the user has difficulty holding their ID relative to the camera.',
},
'id.verification.camera.help.difficulty.answer': {
id: 'id.verification.camera.help.difficulty.answer',
defaultMessage: 'If you require assistance with taking a photo for submission, contact {siteName} support for additional suggestions.',
description: 'Confirming what to do if the user has difficult holding their head relative to the camera.', description: 'Confirming what to do if the user has difficult holding their head relative to the camera.',
}, },
'id.verification.camera.help.upload.question': {
id: 'id.verification.camera.help.upload.question',
defaultMessage: 'What if I want to upload a photo instead?',
description: 'Question on what to do if the user would like to upload a photo instead.',
},
'id.verification.camera.help.upload.answer': {
id: 'id.verification.camera.help.upload.answer',
defaultMessage: 'On the next page you will have the option to switch to upload mode. By selecting that option, you will be able to upload a photo instead.',
description: 'Confirming what to do if the user would like to upload a photo.',
},
'id.verification.id.photo.unclear.question': {
id: 'id.verification.id.photo.unclear.question',
defaultMessage: 'Is your ID card image not clear or too blurry?',
description: 'Question on what to do if the user\'s ID image is unclear',
},
'id.verification.id.tips.title': { 'id.verification.id.tips.title': {
id: 'id.verification.id.tips.title', id: 'id.verification.id.tips.title',
defaultMessage: 'Helpful Identification Card Tips', defaultMessage: 'Helpful ID Tips',
description: 'Title for the ID Tips page.', description: 'Title for the ID Tips page.',
}, },
'id.verification.id.tips.description': { 'id.verification.id.tips.description': {
id: 'id.verification.id.tips.description', id: 'id.verification.id.tips.description',
defaultMessage: 'Next, we\'ll need you to take a photo of a valid identification card that includes your full name and photo, such as a drivers license or passport. Please have your ID ready.', defaultMessage: 'Next you\'ll need an eligible ID photo, make sure that:',
description: 'Description for the ID Tips page.', description: 'Description for the ID Tips page.',
}, },
'id.verification.id.tips.list.well.lit': { 'id.verification.id.tips.list.well.lit': {
id: 'id.verification.id.tips.list.well.lit', id: 'id.verification.id.tips.list.well.lit',
defaultMessage: 'Your identification card is well-lit.', defaultMessage: 'Your ID is well-lit.',
description: 'Tip to ensure ID is well lit.', description: 'Tip to ensure ID is well lit.',
}, },
'id.verification.id.tips.list.clear': { 'id.verification.id.tips.list.clear': {
@@ -488,12 +218,12 @@ const messages = defineMessages({
}, },
'id.verification.id.photo.title.camera': { 'id.verification.id.photo.title.camera': {
id: 'id.verification.id.photo.title.camera', id: 'id.verification.id.photo.title.camera',
defaultMessage: 'Take a Photo of Your Identification Card', defaultMessage: 'Take ID Photo',
description: 'Title for the ID Photo page if camera access is enabled.', description: 'Title for the ID Photo page if camera access is enabled.',
}, },
'id.verification.id.photo.title.upload': { 'id.verification.id.photo.title.upload': {
id: 'id.verification.id.photo.title.upload', id: 'id.verification.id.photo.title.upload',
defaultMessage: 'Upload a Photo of Your Identification Card', defaultMessage: 'Upload Your ID Photo',
description: 'Title for the ID Photo page if camera access is disabled.', description: 'Title for the ID Photo page if camera access is disabled.',
}, },
'id.verification.id.photo.preview.alt': { 'id.verification.id.photo.preview.alt': {
@@ -503,84 +233,24 @@ const messages = defineMessages({
}, },
'id.verification.id.photo.instructions.camera': { 'id.verification.id.photo.instructions.camera': {
id: 'id.verification.id.photo.instructions.camera', id: 'id.verification.id.photo.instructions.camera',
defaultMessage: 'When your ID is in position, use the Take Photo button below to take your photo. Please use a passport, drivers license, or another identification card that includes your full name and a picture of your face.', defaultMessage: 'When your ID is in position, use the Take Photo button below to take your photo.',
description: 'Instructions to use the camera to take an ID photo.', description: 'Instructions to use the camera to take an ID photo.',
}, },
'id.verification.id.photo.instructions.upload': { 'id.verification.id.photo.instructions.upload': {
id: 'id.verification.id.photo.instructions.upload', id: 'id.verification.id.photo.instructions.upload',
defaultMessage: 'Please upload a photo of your identification card. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ', defaultMessage: 'Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)',
description: 'Instructions for ID photo upload.', description: 'Instructions for ID photo upload.',
}, },
'id.verification.id.photo.instructions.upload.error.invalidFileType': {
id: 'id.verification.id.photo.instructions.upload.error.invalidFileType',
defaultMessage: 'The file you have selected is not a supported image type. Please choose from the following formats: ',
description: 'Error message for file upload that is not a supported image type.',
},
'id.verification.id.photo.instructions.upload.error.fileTooLarge': {
id: 'id.verification.id.photo.instructions.upload.error.fileTooLarge',
defaultMessage: 'The file you have selected is too large. Please try again with a file less than 10MB.',
description: 'Error message for file upload that is larger than 10MB.',
},
'id.verification.account.name.title': { 'id.verification.account.name.title': {
id: 'id.verification.account.name.title', id: 'id.verification.account.name.title',
defaultMessage: 'Account Name Check', defaultMessage: 'Account Name Check',
description: 'Title for the Account Name Check page.', description: 'Title for the Account Name Check page.',
}, },
'id.verification.name.check.title': {
id: 'id.verification.name.check.title',
defaultMessage: 'Double-Check Your Name',
description: 'Title for the page where a user double-checks that their name is correct.',
},
'id.verification.account.name.instructions': { 'id.verification.account.name.instructions': {
id: 'id.verification.account.name.instructions', id: 'id.verification.account.name.instructions',
defaultMessage: 'The name on your account and the name on your ID must be an exact match. If not, please click "No" to update your account name.', defaultMessage: 'Please check the Account Name below to ensure it matches the name on your ID. If not, click "Edit".',
description: 'Text to verify that the account name matches the name on the ID photo.', description: 'Text to verify that the account name matches the name on the ID photo.',
}, },
'id.verification.name.check.instructions': {
id: 'id.verification.name.check.instructions',
defaultMessage: 'Does the name below match the name on your government-issued ID? If not, update the name below to match your goverment-issued ID.',
description: 'Text to instruct the user to check that the name displayed on the page matches what is on their government-issued ID.',
},
'id.verification.name.check.mismatch.information': {
id: 'id.verification.name.check.mismatch.information',
defaultMessage: 'If the name below does not match your government-issued ID, your identity verification will be denied.',
description: 'Text to inform the user that if the name displayed on the page does not match what is on their government-issued ID, identity verification will be denied.',
},
'id.verification.account.name.radio.label': {
id: 'id.verification.account.name.radio.label',
defaultMessage: 'Does the name on your ID match the Account Name below?',
description: 'Question to ask the user whether their account name match the name on their ID card.',
},
'id.verification.name.check.radio.label': {
id: 'id.verification.name.check.radio.label',
defaultMessage: 'Select an option',
description: 'Label for a radio button group where the user needs to choose one of two options.',
},
'id.verification.account.name.radio.yes': {
id: 'id.verification.account.name.radio.yes',
defaultMessage: 'Yes',
description: 'The radio button that says the account name matches.',
},
'id.verification.name.check.radio.yes': {
id: 'id.verification.name.check.radio.yes',
defaultMessage: 'Yes, the name below matches my ID',
description: 'Label for a radio button that indicates that the name displayed on the page matches the name on the user\'s ID.',
},
'id.verification.account.name.radio.no': {
id: 'id.verification.account.name.radio.no',
defaultMessage: 'No',
description: 'The radio button that says the account name does not match.',
},
'id.verification.name.check.radio.no': {
id: 'id.verification.name.check.radio.no',
defaultMessage: 'No, the name below does not match my ID',
description: 'Label for a radio button that indicates that the name displayed on the page does not match the name on the user\'s ID.',
},
'id.verification.account.name.error': {
id: 'id.verification.account.name.error',
defaultMessage: 'Please update account name to match the name on your ID.',
description: 'Error that shows when the user needs to update their account name to match the name on their ID.',
},
'id.verification.account.name.warning.prefix': { 'id.verification.account.name.warning.prefix': {
id: 'id.verification.account.name.warning.prefix', id: 'id.verification.account.name.warning.prefix',
defaultMessage: 'Please Note:', defaultMessage: 'Please Note:',
@@ -593,13 +263,13 @@ const messages = defineMessages({
}, },
'id.verification.account.name.label': { 'id.verification.account.name.label': {
id: 'id.verification.account.name.label', id: 'id.verification.account.name.label',
defaultMessage: 'Account Name', defaultMessage: 'Name on ID',
description: 'Label for account name input.', description: 'Label for account name input.',
}, },
'id.verification.name.label': { 'id.verification.account.name.edit': {
id: 'id.verification.name.label', id: 'id.verification.account.name.edit',
defaultMessage: 'Name', defaultMessage: 'Edit',
description: 'Label for name input.', description: 'Button to edit account name.',
}, },
'id.verification.account.name.photo.alt': { 'id.verification.account.name.photo.alt': {
id: 'id.verification.account.name.photo.alt', id: 'id.verification.account.name.photo.alt',
@@ -608,7 +278,7 @@ const messages = defineMessages({
}, },
'id.verification.account.name.save': { 'id.verification.account.name.save': {
id: 'id.verification.account.name.save', id: 'id.verification.account.name.save',
defaultMessage: 'Save and Next', defaultMessage: 'Save',
description: 'Button to save the account name.', description: 'Button to save the account name.',
}, },
'id.verification.review.title': { 'id.verification.review.title': {
@@ -638,12 +308,12 @@ const messages = defineMessages({
}, },
'id.verification.review.id.label': { 'id.verification.review.id.label': {
id: 'id.verification.review.id.label', id: 'id.verification.review.id.label',
defaultMessage: 'Your Identification Card', defaultMessage: 'Your Photo ID',
description: 'Label for the Photo ID card.', description: 'Label for the Photo ID card.',
}, },
'id.verification.review.id.alt': { 'id.verification.review.id.alt': {
id: 'id.verification.review.id.alt', id: 'id.verification.review.id.alt',
defaultMessage: 'Photo of your identification card to be submitted.', defaultMessage: 'Photo of your ID to be submitted.',
description: 'Alt text for the ID photo.', description: 'Alt text for the ID photo.',
}, },
'id.verification.review.id.retake': { 'id.verification.review.id.retake': {
@@ -653,33 +323,8 @@ const messages = defineMessages({
}, },
'id.verification.review.confirm': { 'id.verification.review.confirm': {
id: 'id.verification.review.confirm', id: 'id.verification.review.confirm',
defaultMessage: 'Submit', defaultMessage: 'Confirm',
description: 'Button to confirm all information is correct and submit.', description: 'Button to confirm all information is correct.',
},
'id.verification.submission.alert.error.face': {
id: 'id.verification.submission.alert.error.face',
defaultMessage: 'A photo of your face is required. Please retake your portrait photo.',
description: 'Error message displayed when the user\'s portrait photo is missing.',
},
'id.verification.submission.alert.error.id': {
id: 'id.verification.submission.alert.error.id',
defaultMessage: 'A photo of your ID card is required. Please retake your ID photo.',
description: 'Error message displayed when the user\'s ID photo is missing.',
},
'id.verification.submission.alert.error.name': {
id: 'id.verification.submission.alert.error.name',
defaultMessage: 'A valid account name is required. Please update your account name to match the name on your ID.',
description: 'Error message displayed when the user\'s account name is missing.',
},
'id.verification.submission.alert.error.unsupported': {
id: 'id.verification.submission.alert.error.unsupported',
defaultMessage: 'One or more of the files you have uploaded is in an unsupported format. Please choose from the following: ',
description: 'Error message displayed when the user uploads an unsupported file type.',
},
'id.verification.review.error': {
id: 'id.verification.review.error',
defaultMessage: '{siteName} Support Page',
description: 'Text linking to the platform support page.',
}, },
'id.verification.submitted.title': { 'id.verification.submitted.title': {
id: 'id.verification.submitted.title', id: 'id.verification.submitted.title',
@@ -688,69 +333,14 @@ const messages = defineMessages({
}, },
'id.verification.submitted.text': { 'id.verification.submitted.text': {
id: 'id.verification.submitted.text', id: 'id.verification.submitted.text',
defaultMessage: 'We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 5 days). In the meantime, you can still access all available course content.', defaultMessage: 'We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days). In the meantime, you can still access all available course content.',
description: 'Text confirming that ID verification request has been received.', description: 'Text confirming that ID verification request has been received.',
}, },
'id.verification.return.dashboard': { 'id.verification.return': {
id: 'id.verification.return.dashboard', id: 'id.verification.submitted.return',
defaultMessage: 'Return to Your Dashboard', defaultMessage: 'Return to Your Dashboard',
description: 'Button to return to the dashboard.', description: 'Button to return to the dashboard.',
}, },
'id.verification.return.course': {
id: 'id.verification.return.course',
defaultMessage: 'Return to Course',
description: 'Return to the course which ID verification was accessed from.',
},
'id.verification.photo.upload.help.title': {
id: 'id.verification.photo.upload.help.title',
defaultMessage: 'Upload a Photo Instead',
description: 'Title for section that allows switching to photo upload mode.',
},
'id.verification.photo.camera.help.title': {
id: 'id.verification.photo.camera.help.title',
defaultMessage: 'Use Your Camera Instead',
description: 'Title for section that allows switching to camera mode.',
},
'id.verification.photo.upload.help.text': {
id: 'id.verification.photo.upload.help.text',
defaultMessage: 'If you are having trouble using the photo capture above, you may want to upload a photo instead. To upload a photo, click the button below.',
description: 'Help text for switching to upload mode.',
},
'id.verification.photo.camera.help.text': {
id: 'id.verification.photo.camera.help.text',
defaultMessage: 'If you are having trouble uploading a photo above, you may want to use your camera instead. To use your camera, click the button below.',
description: 'Help text for switching to camera mode.',
},
'id.verification.photo.upload.help.button': {
id: 'id.verification.upload.help.button',
defaultMessage: 'Switch to Upload Mode',
description: 'Button used to switch to upload mode.',
},
'id.verification.photo.camera.help.button': {
id: 'id.verification.camera.help.button',
defaultMessage: 'Switch to Camera Mode',
description: 'Button used to switch to camera mode.',
},
'id.verification.choose.mode.title': {
id: 'id.verification.choose.mode.title',
defaultMessage: 'Photo Requirements Options',
description: 'Title for section that allows user to choose photo mode.',
},
'id.verification.choose.mode.help.text': {
id: 'id.verification.choose.mode.hep.text',
defaultMessage: 'To complete verification, please select one of the following options to submit photos. You will be able to switch between these options throughout the process if needed.',
description: 'Help text for section that allows user to choose photo mode.',
},
'id.verification.choose.mode.radio.upload': {
id: 'id.verification.choose.mode.radio.upload',
defaultMessage: 'Upload photos from my device',
description: 'Radio button to choose to upload photos.',
},
'id.verification.choose.mode.radio.camera': {
id: 'id.verification.choose.mode.radio.camera',
defaultMessage: 'Take pictures using my camera',
description: 'Radio button to choose to use camera for photos.',
},
}); });
export default messages; export default messages;

View File

@@ -1,4 +1,11 @@
import React from 'react'; import React, { useState, useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
import { AppContext } from '@edx/frontend-platform/react';
import { hasGetUserMediaSupport } from './getUserMediaShim';
import { getExistingIdVerification } from './data/service';
import PageLoading from '../account-settings/PageLoading';
import ExistingRequest from './ExistingRequest';
const IdVerificationContext = React.createContext({}); const IdVerificationContext = React.createContext({});
@@ -9,17 +16,75 @@ const MEDIA_ACCESS = {
GRANTED: 'granted', GRANTED: 'granted',
}; };
const ERROR_REASONS = { function IdVerificationContextProvider({ children }) {
COURSE_ENROLLMENT: 'course_enrollment', const [existingIdVerification, setExistingIdVerification] = useState(null);
EXISTING_REQUEST: 'existing_request', const [facePhotoFile, setFacePhotoFile] = useState(null);
CANNOT_VERIFY: 'cannot_verify', const [idPhotoFile, setIdPhotoFile] = useState(null);
const [idPhotoName, setIdPhotoName] = useState(null);
const [mediaStream, setMediaStream] = useState(null);
const [mediaAccess, setMediaAccess] = useState(hasGetUserMediaSupport ?
MEDIA_ACCESS.PENDING :
MEDIA_ACCESS.UNSUPPORTED);
const { authenticatedUser } = useContext(AppContext);
const contextValue = {
existingIdVerification,
facePhotoFile,
idPhotoFile,
idPhotoName,
mediaStream,
mediaAccess,
nameOnAccount: authenticatedUser.name,
setExistingIdVerification,
setFacePhotoFile,
setIdPhotoFile,
setIdPhotoName,
tryGetUserMedia: async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
setMediaAccess(MEDIA_ACCESS.GRANTED);
setMediaStream(stream);
// If we would like to stop the stream immediately. I guess we can leave it open
// const tracks = stream.getTracks();
// tracks.forEach(track => track.stop());
} catch (err) {
setMediaAccess(MEDIA_ACCESS.DENIED);
}
},
};
// Call verification status endpoint to check whether we can verify.
useEffect(() => {
(async () => {
const existingIdV = await getExistingIdVerification();
setExistingIdVerification(existingIdV);
})();
}, []);
// If we are waiting for verification status endpoint, show spinner.
if (!existingIdVerification) {
return <PageLoading srMessage="Loading verification status" />;
}
if (!existingIdVerification.canVerify) {
const { status } = existingIdVerification;
return (
<ExistingRequest status={status} />
);
}
return (
<IdVerificationContext.Provider value={contextValue}>
{children}
</IdVerificationContext.Provider>
);
}
IdVerificationContextProvider.propTypes = {
children: PropTypes.node.isRequired,
}; };
const VERIFIED_MODES = ['verified', 'professional', 'masters', 'executive_education'];
export default IdVerificationContext;
export { export {
IdVerificationContext,
IdVerificationContextProvider,
MEDIA_ACCESS, MEDIA_ACCESS,
ERROR_REASONS,
VERIFIED_MODES,
}; };

View File

@@ -1,162 +0,0 @@
import React, { useState, useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
import { AppContext } from '@edx/frontend-platform/react';
import { getProfileDataManager } from '../account-settings/data/service';
import PageLoading from '../account-settings/PageLoading';
import { useAsyncCall } from '../hooks';
import { getExistingIdVerification, getEnrollments } from './data/service';
import AccessBlocked from './AccessBlocked';
import { hasGetUserMediaSupport } from './getUserMediaShim';
import IdVerificationContext, { MEDIA_ACCESS, ERROR_REASONS, VERIFIED_MODES } from './IdVerificationContext';
import { VerifiedNameContext } from './VerifiedNameContext';
export default function IdVerificationContextProvider({ children }) {
const { authenticatedUser } = useContext(AppContext);
const { isVerifiedNameHistoryLoading, verifiedName, verifiedNameEnabled } = useContext(VerifiedNameContext);
// Call verification status endpoint to check whether we can verify.
const [existingIdVerification, setExistingIdVerification] = useState(null);
const [isIDVerificationLoading, idVerificationData] = useAsyncCall(getExistingIdVerification);
const [isEnrollmentsLoading, enrollmentsData] = useAsyncCall(getEnrollments);
useEffect(() => {
if (idVerificationData) {
setExistingIdVerification(idVerificationData);
}
}, [idVerificationData]);
const [facePhotoFile, setFacePhotoFile] = useState(null);
const [idPhotoFile, setIdPhotoFile] = useState(null);
const [idPhotoName, setIdPhotoName] = useState(null);
const [mediaStream, setMediaStream] = useState(null);
const [mediaAccess, setMediaAccess] = useState(
hasGetUserMediaSupport ? MEDIA_ACCESS.PENDING : MEDIA_ACCESS.UNSUPPORTED,
);
const [canVerify, setCanVerify] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
// With verified name we can redo verification multiple times
// if not a successful request prevents re-verification
if (!verifiedNameEnabled && existingIdVerification && !existingIdVerification.canVerify) {
const { status } = existingIdVerification;
setCanVerify(false);
if (status === 'pending' || status === 'approved') {
setError(ERROR_REASONS.EXISTING_REQUEST);
} else {
setError(ERROR_REASONS.CANNOT_VERIFY);
}
} else if (verifiedNameEnabled) {
setCanVerify(true);
}
}, [existingIdVerification, verifiedNameEnabled]);
useEffect(() => {
if (!isEnrollmentsLoading && enrollmentsData) {
const verifiedEnrollments = enrollmentsData.filter((enrollment) => (
VERIFIED_MODES.includes(enrollment.mode)
));
if (verifiedEnrollments.length === 0) {
setCanVerify(false);
setError(ERROR_REASONS.COURSE_ENROLLMENT);
}
}
}, [enrollmentsData]);
const [profileDataManager, setProfileDataManager] = useState(null);
useEffect(() => {
// Determine if the user's profile data is managed by a third-party identity provider.
// If so, they cannot update their account name manually.
if (authenticatedUser.roles.length > 0) {
(async () => {
const thirdPartyManager = await getProfileDataManager(
authenticatedUser.username,
authenticatedUser.roles,
);
if (thirdPartyManager) {
setProfileDataManager(thirdPartyManager);
}
})();
}
}, [authenticatedUser]);
const [optimizelyExperimentName, setOptimizelyExperimentName] = useState('');
const [shouldUseCamera, setShouldUseCamera] = useState(false);
// The following are used to keep track of how a user has submitted photos
const [portraitPhotoMode, setPortraitPhotoMode] = useState('');
const [idPhotoMode, setIdPhotoMode] = useState('');
// If the user reaches the end of the flow and goes back to retake their photos,
// this flag ensures that they are directed straight back to the summary panel
const [reachedSummary, setReachedSummary] = useState(false);
const contextValue = {
existingIdVerification,
facePhotoFile,
idPhotoFile,
idPhotoName,
mediaStream,
mediaAccess,
userId: authenticatedUser.userId,
// If the learner has an applicable verified name, then this should override authenticatedUser.name
// when determining the context value nameOnAccount.
nameOnAccount: verifiedName || authenticatedUser.name,
profileDataManager,
optimizelyExperimentName,
shouldUseCamera,
portraitPhotoMode,
idPhotoMode,
reachedSummary,
setExistingIdVerification,
setFacePhotoFile,
setIdPhotoFile,
setIdPhotoName,
setOptimizelyExperimentName,
setShouldUseCamera,
setPortraitPhotoMode,
setIdPhotoMode,
setReachedSummary,
tryGetUserMedia: async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
setMediaAccess(MEDIA_ACCESS.GRANTED);
setMediaStream(stream);
setShouldUseCamera(true);
// stop the stream, as we are not using it yet
const tracks = stream.getTracks();
tracks.forEach(track => track.stop());
} catch (err) {
setMediaAccess(MEDIA_ACCESS.DENIED);
setShouldUseCamera(false);
}
},
stopUserMedia: () => {
if (mediaStream) {
const tracks = mediaStream.getTracks();
tracks.forEach(track => track.stop());
setMediaStream(null);
}
},
};
// If we are waiting for verification status endpoint, show spinner.
if (isIDVerificationLoading || isVerifiedNameHistoryLoading) {
return <PageLoading srMessage="Loading verification status" />;
}
if (!canVerify) {
return <AccessBlocked error={error} />;
}
return (
<IdVerificationContext.Provider value={contextValue}>
{children}
</IdVerificationContext.Provider>
);
}
IdVerificationContextProvider.propTypes = {
children: PropTypes.node.isRequired,
};

View File

@@ -1,19 +1,13 @@
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import { Route, Switch, Redirect, useRouteMatch } from 'react-router-dom';
Route, Switch, Redirect, useRouteMatch, useLocation,
} from 'react-router-dom';
import qs from 'qs';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Modal, Button } from '@edx/paragon'; import { Modal, Button } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { idVerificationSelector } from './data/selectors'; import { idVerificationSelector } from './data/selectors';
import './getUserMediaShim'; import './getUserMediaShim';
import IdVerificationContextProvider from './IdVerificationContextProvider'; import { IdVerificationContextProvider } from './IdVerificationContext';
import { VerifiedNameContextProvider } from './VerifiedNameContext';
import ReviewRequirementsPanel from './panels/ReviewRequirementsPanel'; import ReviewRequirementsPanel from './panels/ReviewRequirementsPanel';
import ChooseModePanel from './panels/ChooseModePanel';
import RequestCameraAccessPanel from './panels/RequestCameraAccessPanel'; import RequestCameraAccessPanel from './panels/RequestCameraAccessPanel';
import PortraitPhotoContextPanel from './panels/PortraitPhotoContextPanel'; import PortraitPhotoContextPanel from './panels/PortraitPhotoContextPanel';
import TakePortraitPhotoPanel from './panels/TakePortraitPhotoPanel'; import TakePortraitPhotoPanel from './panels/TakePortraitPhotoPanel';
@@ -28,23 +22,8 @@ import messages from './IdVerification.messages';
// eslint-disable-next-line react/prefer-stateless-function // eslint-disable-next-line react/prefer-stateless-function
function IdVerificationPage(props) { function IdVerificationPage(props) {
const { path } = useRouteMatch(); const { path } = useRouteMatch();
const { search } = useLocation();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
// Course run key is passed as a query string
useEffect(() => {
if (search) {
const parsed = qs.parse(search, {
ignoreQueryPrefix: true,
interpretNumericEntities: true,
});
if (Object.prototype.hasOwnProperty.call(parsed, 'course_id') && parsed.course_id) {
sessionStorage.setItem('courseRunKey', parsed.course_id);
}
}
}, [search]);
return ( return (
<> <>
{/* If user reloads, redirect to the beginning of the process */} {/* If user reloads, redirect to the beginning of the process */}
@@ -52,58 +31,45 @@ function IdVerificationPage(props) {
<div className="page__id-verification container-fluid py-5"> <div className="page__id-verification container-fluid py-5">
<div className="row"> <div className="row">
<div className="col-lg-6 col-md-8"> <div className="col-lg-6 col-md-8">
<VerifiedNameContextProvider> <IdVerificationContextProvider>
<IdVerificationContextProvider> <Switch>
<Switch> <Route exact path={path}>
<Route path={`${path}/review-requirements`} component={ReviewRequirementsPanel} /> <Redirect to={`${path}/review-requirements`} />
<Route path={`${path}/choose-mode`} component={ChooseModePanel} /> </Route>
<Route path={`${path}/request-camera-access`} component={RequestCameraAccessPanel} /> <Route path={`${path}/review-requirements`} component={ReviewRequirementsPanel} />
<Route path={`${path}/portrait-photo-context`} component={PortraitPhotoContextPanel} /> <Route path={`${path}/request-camera-access`} component={RequestCameraAccessPanel} />
<Route path={`${path}/take-portrait-photo`} component={TakePortraitPhotoPanel} /> <Route path={`${path}/portrait-photo-context`} component={PortraitPhotoContextPanel} />
<Route path={`${path}/id-context`} component={IdContextPanel} /> <Route path={`${path}/take-portrait-photo`} component={TakePortraitPhotoPanel} />
<Route path={`${path}/get-name-id`} component={GetNameIdPanel} /> <Route path={`${path}/id-context`} component={IdContextPanel} />
<Route path={`${path}/take-id-photo`} component={TakeIdPhotoPanel} /> <Route path={`${path}/get-name-id`} component={GetNameIdPanel} />
<Route path={`${path}/summary`} component={SummaryPanel} /> <Route path={`${path}/take-id-photo`} component={TakeIdPhotoPanel} />
<Route path={`${path}/submitted`} component={SubmittedPanel} /> <Route path={`${path}/summary`} component={SummaryPanel} />
</Switch> <Route path={`${path}/submitted`} component={SubmittedPanel} />
</IdVerificationContextProvider> </Switch>
</VerifiedNameContextProvider> </IdVerificationContextProvider>
</div> </div>
<div className="col-lg-6 col-md-4 pt-md-0 pt-4 text-right"> <div className="col-lg-6 col-md-4 pt-md-0 pt-4 text-right">
<Button variant="link" className="px-0" onClick={() => setIsModalOpen(true)}> <Button className="btn-link px-0" onClick={() => setIsModalOpen(true)}>
Privacy Information Privacy Information
</Button> </Button>
</div> </div>
</div> </div>
<Modal <Modal
open={isModalOpen} open={isModalOpen}
title={props.intl.formatMessage(messages['id.verification.privacy.title'])} title={props.intl.formatMessage(messages['id.verification.privacy.title'])}
body={( body={(
<div> <div>
<h6> <h6>{props.intl.formatMessage(messages['id.verification.privacy.need.photo.question'])}</h6>
{props.intl.formatMessage(
messages['id.verification.privacy.need.photo.question'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}</p> <p>{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}</p>
<h6> <h6>{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.question'])}</h6>
{props.intl.formatMessage( <p>{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.answer'])}</p>
messages['id.verification.privacy.do.with.photo.question'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>
{props.intl.formatMessage(
messages['id.verification.privacy.do.with.photo.answer'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
</div> </div>
)} )}
onClose={() => setIsModalOpen(false)} onClose={() => setIsModalOpen(false)}
/> />
</div> </div>
</> </>
); );
@@ -113,4 +79,6 @@ IdVerificationPage.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,
}; };
export default connect(idVerificationSelector, {})(injectIntl(IdVerificationPage)); export default connect(idVerificationSelector, {
})(injectIntl(IdVerificationPage));

View File

@@ -1,64 +1,27 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback } from 'react';
import { intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Alert } from '@edx/paragon';
import messages from './IdVerification.messages';
import SupportedMediaTypes from './SupportedMediaTypes';
export default function ImageFileUpload({ onFileChange, setPhotoMode, intl }) {
const [error, setError] = useState(null);
const errorTypes = {
invalidFileType: 'invalidFileType',
fileTooLarge: 'fileTooLarge',
};
const maxFileSize = 10000000;
export default function ImageFileUpload({ onFileChange }) {
const handleChange = useCallback((e) => { const handleChange = useCallback((e) => {
if (e.target.files.length === 0) { if (e.target.files.length === 0) {
return; return;
} }
const fileObject = e.target.files[0]; const fileObject = e.target.files[0];
if (!fileObject.type.startsWith('image')) { const fileReader = new FileReader();
setError(errorTypes.invalidFileType); fileReader.addEventListener('load', () => onFileChange(fileReader.result));
} else if (fileObject.size >= maxFileSize) { fileReader.readAsDataURL(fileObject);
setError(errorTypes.fileTooLarge);
} else {
setError(null);
const fileReader = new FileReader();
fileReader.addEventListener('load', () => {
onFileChange(fileReader.result);
setPhotoMode('upload');
});
fileReader.readAsDataURL(fileObject);
}
}, []); }, []);
return ( return (
<> <input
<input type="file"
type="file" accept="image/*"
accept="image/*" onChange={handleChange}
data-testid="fileUpload" />
onChange={handleChange}
/>
{error && (
<Alert
id="fileError"
variant="danger"
tabIndex="-1"
style={{ marginTop: '1rem' }}
>
{intl.formatMessage(messages[`id.verification.id.photo.instructions.upload.error.${error}`])}
<SupportedMediaTypes />
</Alert>
)}
</>
); );
} }
ImageFileUpload.propTypes = { ImageFileUpload.propTypes = {
onFileChange: PropTypes.func.isRequired, onFileChange: PropTypes.func.isRequired,
setPhotoMode: PropTypes.func.isRequired,
intl: intlShape.isRequired,
}; };

View File

@@ -5,7 +5,7 @@ export default function ImagePreview({ src, alt, id }) {
return ( return (
<div id={id} className="image-preview"> <div id={id} className="image-preview">
<img data-hj-suppress style={{ objectFit: 'contain' }} src={src} alt={alt} /> <img style={{ objectFit: 'contain' }} src={src} alt={alt} />
</div> </div>
); );
@@ -16,7 +16,3 @@ ImagePreview.propTypes = {
alt: PropTypes.string.isRequired, alt: PropTypes.string.isRequired,
id: PropTypes.string, id: PropTypes.string,
}; };
ImagePreview.defaultProps = {
id: undefined,
};

View File

@@ -1,14 +0,0 @@
import React from 'react';
export default function SupportedMediaTypes() {
const SUPPORTED_TYPES = ['.png', '.jpeg', '.jpg', '.bmp', '.webp', '.tiff'];
const getSupportedTypes = () => SUPPORTED_TYPES.map((type, index) => {
if (index === SUPPORTED_TYPES.length - 1) {
return type;
}
return `${type}, `;
});
return <span>{getSupportedTypes()}</span>;
}

View File

@@ -1,38 +0,0 @@
import React, { createContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { getVerifiedNameHistory } from '../account-settings/data/service';
import { getMostRecentApprovedOrPendingVerifiedName } from '../utils';
import { useAsyncCall } from '../hooks';
export const VerifiedNameContext = createContext();
export function VerifiedNameContextProvider({ children }) {
const [verifiedNameEnabled, setVerifiedNameEnabled] = useState(false);
const [verifiedName, setVerifiedName] = useState('');
const [isVerifiedNameHistoryLoading, verifiedNameHistory] = useAsyncCall(getVerifiedNameHistory);
useEffect(() => {
if (verifiedNameHistory) {
const { verified_name_enabled: verifiedNameFeatureEnabled, results } = verifiedNameHistory;
setVerifiedNameEnabled(verifiedNameFeatureEnabled);
if (verifiedNameFeatureEnabled) {
const applicableVerifiedName = getMostRecentApprovedOrPendingVerifiedName(results);
setVerifiedName(applicableVerifiedName);
}
}
}, [verifiedNameHistory]);
const value = {
isVerifiedNameHistoryLoading,
verifiedNameEnabled,
verifiedName,
};
return (<VerifiedNameContext.Provider value={value}>{children}</VerifiedNameContext.Provider>);
}
VerifiedNameContextProvider.propTypes = {
children: PropTypes.node.isRequired,
};

View File

@@ -1,9 +1,6 @@
.page__id-verification { .page__id-verification {
.verification-panel { .verification-panel {
img { .card.requirements {
max-width: 100%;
}
.card.accent {
border-top: solid 4px theme-color('warning'); border-top: solid 4px theme-color('warning');
} }
.image-preview { .image-preview {
@@ -11,6 +8,7 @@
max-width: 20rem; max-width: 20rem;
img { img {
display: block; display: block;
max-width: 100%;
max-height: 10rem; max-height: 10rem;
} }
} }
@@ -46,14 +44,6 @@
width: 100%; width: 100%;
} }
.canvas-video {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.camera-btn { .camera-btn {
margin: 10px; margin: 10px;
} }
@@ -70,4 +60,4 @@
opacity: 0; opacity: 0;
background: white; background: white;
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

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