Compare commits
404 Commits
open-relea
...
mashal-m/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bec2cc206c | ||
|
|
81ac063cf8 | ||
|
|
50f2442512 | ||
|
|
be28f398d6 | ||
|
|
cd47862c1d | ||
|
|
847e3e05b0 | ||
|
|
e347b3fb23 | ||
|
|
a6a1d94d92 | ||
|
|
e54de547f6 | ||
|
|
574cfb91c2 | ||
|
|
5bdf0a4978 | ||
|
|
cd8dee1731 | ||
|
|
0e7d2c048d | ||
|
|
79c4a14f6f | ||
|
|
de8977347a | ||
|
|
783f78a9ef | ||
|
|
c1fa6efb30 | ||
|
|
b3cd370d8e | ||
|
|
8cb416d142 | ||
|
|
dbd4faf558 | ||
|
|
92c8f17e2a | ||
|
|
c858966035 | ||
|
|
547d55a31f | ||
|
|
f78420511e | ||
|
|
e3ad6e6e54 | ||
|
|
c6ffc51a5d | ||
|
|
9b28469afd | ||
|
|
2bb1388a45 | ||
|
|
bf0d1379f6 | ||
|
|
0d9bc5988d | ||
|
|
3a2075de9c | ||
|
|
ef6239a807 | ||
|
|
8cb9df6d89 | ||
|
|
fa856b54bf | ||
|
|
e9f6acff27 | ||
|
|
dc26da9cc1 | ||
|
|
9dd05761fd | ||
|
|
0d5b86df57 | ||
|
|
d8eb6e9da5 | ||
|
|
d280062fe3 | ||
|
|
f0366a98f4 | ||
|
|
9d0577fd93 | ||
|
|
dba70b557c | ||
|
|
f6840bc202 | ||
|
|
1af5f0d179 | ||
|
|
b3a91470f8 | ||
|
|
9b18588e26 | ||
|
|
3d2e7dc8a4 | ||
|
|
41f1552f9b | ||
|
|
ffa8a36ce6 | ||
|
|
3505e645e3 | ||
|
|
88db6084df | ||
|
|
27c8ab28f3 | ||
|
|
e01b595a0c | ||
|
|
0d351f5173 | ||
|
|
03dbfb5670 | ||
|
|
1e17fc9e32 | ||
|
|
da912f1e35 | ||
|
|
fb0f832832 | ||
|
|
870dd631bb | ||
|
|
5b9f251e93 | ||
|
|
98c14bbd3b | ||
|
|
00ce8b927f | ||
|
|
ab215cd909 | ||
|
|
51e7773207 | ||
|
|
6f5a1a8aa9 | ||
|
|
deb48fb9d2 | ||
|
|
20b451afb6 | ||
|
|
ad6f812974 | ||
|
|
9573516b37 | ||
|
|
f0d6a92ab2 | ||
|
|
dc4d4031e9 | ||
|
|
84a5c7aaf1 | ||
|
|
6ad666342d | ||
|
|
1252498872 | ||
|
|
f9d04e4dd4 | ||
|
|
c96b9bb77d | ||
|
|
f3c672c5ae | ||
|
|
63c396c03a | ||
|
|
0981857062 | ||
|
|
0527c73529 | ||
|
|
5c5dbc369b | ||
|
|
196719963f | ||
|
|
92ee4dfbb9 | ||
|
|
9d0b3524cb | ||
|
|
c39fd332b6 | ||
|
|
04d515f554 | ||
|
|
f425e9b94f | ||
|
|
7ec147fe6f | ||
|
|
7a8ae85b72 | ||
|
|
0f8f5a1e9a | ||
|
|
757a9ac033 | ||
|
|
9efc8d1290 | ||
|
|
5f53270148 | ||
|
|
9716495951 | ||
|
|
f9b29948e7 | ||
|
|
bededb3912 | ||
|
|
bb6390f9ae | ||
|
|
d2300d2dfd | ||
|
|
a501407907 | ||
|
|
88e63cd390 | ||
|
|
eaebe6980b | ||
|
|
d6efba63ca | ||
|
|
257e425fd9 | ||
|
|
02e3364874 | ||
|
|
9ae74708fb | ||
|
|
e946e377c6 | ||
|
|
5dbe649b2c | ||
|
|
2792902975 | ||
|
|
6f643070ea | ||
|
|
ce946f56b2 | ||
|
|
de1e67f68d | ||
|
|
79001bccd8 | ||
|
|
963884cc4c | ||
|
|
e43c1bcc9e | ||
|
|
32cc2c7835 | ||
|
|
5c39c279f3 | ||
|
|
12bca9b771 | ||
|
|
5e10c2bc18 | ||
|
|
f2fe22b8f7 | ||
|
|
d5601a21fd | ||
|
|
f3bd7a8589 | ||
|
|
7643bbd6ba | ||
|
|
fd1044b531 | ||
|
|
f25e5db422 | ||
|
|
0d569a060b | ||
|
|
350016cbe0 | ||
|
|
1619dade50 | ||
|
|
dafc34f535 | ||
|
|
9bbab2620c | ||
|
|
81bef65cc2 | ||
|
|
a18daecf8a | ||
|
|
449e4c0253 | ||
|
|
a66ba187ae | ||
|
|
2d710f7060 | ||
|
|
e109e5018e | ||
|
|
1444831833 | ||
|
|
4b4f29ae19 | ||
|
|
cbc4123e78 | ||
|
|
2c896f77d4 | ||
|
|
112ddf80e6 | ||
|
|
6f2a69acc1 | ||
|
|
d2c83b82f7 | ||
|
|
03501a8125 | ||
|
|
6e17214476 | ||
|
|
2c6cec7f8c | ||
|
|
f76797cade | ||
|
|
59325bd412 | ||
|
|
65a6bc5002 | ||
|
|
eff28d8b47 | ||
|
|
51b758a18f | ||
|
|
30bd145bdd | ||
|
|
1f15802bc6 | ||
|
|
5ba476a570 | ||
|
|
dfb13c4286 | ||
|
|
aada46f6eb | ||
|
|
8c57140640 | ||
|
|
9e967ba1ea | ||
|
|
8239209cd7 | ||
|
|
65bb042443 | ||
|
|
4dd7529799 | ||
|
|
e6684f5048 | ||
|
|
e276d6a5c4 | ||
|
|
36f1d1dbfb | ||
|
|
6c324e85fc | ||
|
|
16b5d066ff | ||
|
|
6e45abbe8b | ||
|
|
b3e6335396 | ||
|
|
08a2da5459 | ||
|
|
094361c689 | ||
|
|
b7b33ef597 | ||
|
|
afa808ff5d | ||
|
|
5279f2a9c9 | ||
|
|
4cc00fd7e3 | ||
|
|
8815626411 | ||
|
|
db319b6cdf | ||
|
|
50edcb1c50 | ||
|
|
d6519bc825 | ||
|
|
b54aeb9446 | ||
|
|
bb1bd6e648 | ||
|
|
7df9f92dd8 | ||
|
|
3627915985 | ||
|
|
9fe1a04a0a | ||
|
|
7455821500 | ||
|
|
817980be00 | ||
|
|
7d4e31f69d | ||
|
|
34dde09ccc | ||
|
|
aee4e44f8c | ||
|
|
7381cfd3b6 | ||
|
|
1d0bd3986c | ||
|
|
1c0dc36907 | ||
|
|
ac0ab9daea | ||
|
|
0d45d17cd3 | ||
|
|
a6d265b885 | ||
|
|
3508bc6c34 | ||
|
|
28de621fc7 | ||
|
|
e6df5e77ae | ||
|
|
8bc5c1fae8 | ||
|
|
6e48c9d2d1 | ||
|
|
d3469d648f | ||
|
|
cc65ffc96f | ||
|
|
5640fb95c2 | ||
|
|
7bbb889258 | ||
|
|
53b59231cb | ||
|
|
8fb25fd89b | ||
|
|
15d2bf60f9 | ||
|
|
135826bc52 | ||
|
|
d7251e6aec | ||
|
|
0b0846fb00 | ||
|
|
b1cd1b1995 | ||
|
|
50c468857a | ||
|
|
c940d3463c | ||
|
|
376deba866 | ||
|
|
37d0e6e0fb | ||
|
|
9261711d4a | ||
|
|
3e42d42ad7 | ||
|
|
7de4edc002 | ||
|
|
2936498b02 | ||
|
|
44d26c444b | ||
|
|
b1c1c6502d | ||
|
|
21dda3f25b | ||
|
|
f87b5040a3 | ||
|
|
0dc1df07d4 | ||
|
|
efa682092f | ||
|
|
0d9e6f8b87 | ||
|
|
26d2b50859 | ||
|
|
3eb63cd624 | ||
|
|
ba0774c5c4 | ||
|
|
ac47d0b180 | ||
|
|
02038b8ac9 | ||
|
|
458f9f7e3d | ||
|
|
c7d9c270f9 | ||
|
|
d0ecbbfb8a | ||
|
|
22db0d9202 | ||
|
|
34a142f55f | ||
|
|
6e00915f98 | ||
|
|
4cfa1707de | ||
|
|
a721887886 | ||
|
|
9cdbb93bf3 | ||
|
|
f9e7519e26 | ||
|
|
ff8d5a4d09 | ||
|
|
f14c71c4fb | ||
|
|
43caac8430 | ||
|
|
ee1ecb8ab9 | ||
|
|
020aa84986 | ||
|
|
24459daf6d | ||
|
|
587533703e | ||
|
|
866746d1c6 | ||
|
|
4c618a55c0 | ||
|
|
c9f6cf708e | ||
|
|
5f314ee65f | ||
|
|
7e35b23b36 | ||
|
|
842bd11d89 | ||
|
|
b1e11dfb36 | ||
|
|
aa57b69924 | ||
|
|
e7769b37e9 | ||
|
|
fcc7b26c28 | ||
|
|
16d844528d | ||
|
|
223234f623 | ||
|
|
d8a1c0ca8c | ||
|
|
1da8f630eb | ||
|
|
228eec0afa | ||
|
|
cf62b4b82c | ||
|
|
0184c1fa25 | ||
|
|
8ed103b2ad | ||
|
|
84a9de44a5 | ||
|
|
84df0a0b3e | ||
|
|
a3917ae550 | ||
|
|
bfd6a07a2c | ||
|
|
1df570989b | ||
|
|
b7e433876e | ||
|
|
1669d577f6 | ||
|
|
d1ca7decce | ||
|
|
79a43ae713 | ||
|
|
9d0b315714 | ||
|
|
4b2bc11378 | ||
|
|
fc7ce6b91e | ||
|
|
39a25fe5bc | ||
|
|
307cb1541b | ||
|
|
03e026ce4e | ||
|
|
a29876aff0 | ||
|
|
ab77246015 | ||
|
|
572b05e7f1 | ||
|
|
3e4de47ba6 | ||
|
|
6c6cedd422 | ||
|
|
43694921ca | ||
|
|
06ded1e66e | ||
|
|
aba1bb3382 | ||
|
|
d67b880028 | ||
|
|
29692add53 | ||
|
|
7f53bf32ca | ||
|
|
add22d9756 | ||
|
|
bef9bf76fd | ||
|
|
bf6b2fb8b8 | ||
|
|
a77cd6d91a | ||
|
|
9b3f222191 | ||
|
|
90f2ed8393 | ||
|
|
95c53ad380 | ||
|
|
3023cd3d55 | ||
|
|
50f85674b1 | ||
|
|
9437ee36f3 | ||
|
|
463944012c | ||
|
|
2d297aa7be | ||
|
|
1ab5901d24 | ||
|
|
ba678d92f7 | ||
|
|
884651a702 | ||
|
|
a152c631da | ||
|
|
fbe91ce7e4 | ||
|
|
13681c1360 | ||
|
|
43435f8ff3 | ||
|
|
830fb05819 | ||
|
|
f62bd5ad76 | ||
|
|
e2f9edd623 | ||
|
|
0b63613736 | ||
|
|
940828ff18 | ||
|
|
296f68f7dd | ||
|
|
e59ada660c | ||
|
|
c123daacd6 | ||
|
|
7440cd367f | ||
|
|
1d639c4a57 | ||
|
|
6db789d6ac | ||
|
|
4bbff91ad7 | ||
|
|
24e32cd0c5 | ||
|
|
d1548b7287 | ||
|
|
d1de13469f | ||
|
|
62418db3ba | ||
|
|
24f70972d8 | ||
|
|
a7c783888f | ||
|
|
00b1bf58a5 | ||
|
|
b2c08193f7 | ||
|
|
2bbe7dc3f1 | ||
|
|
4d93fd6c0f | ||
|
|
613229bbd1 | ||
|
|
39db13435f | ||
|
|
994f22ab41 | ||
|
|
579654c092 | ||
|
|
6094509679 | ||
|
|
82170f383f | ||
|
|
addc3640cc | ||
|
|
b9c0069117 | ||
|
|
11c31ba216 | ||
|
|
ccd2a5c074 | ||
|
|
ff2e86fa13 | ||
|
|
f71033232e | ||
|
|
1ff20786ff | ||
|
|
232bead0c9 | ||
|
|
bd0c879a51 | ||
|
|
a52e1ed2a5 | ||
|
|
b8356bc962 | ||
|
|
240e589310 | ||
|
|
152d3230d1 | ||
|
|
95bd1cc66f | ||
|
|
5db6d2b319 | ||
|
|
368bc00321 | ||
|
|
fa4c5ef872 | ||
|
|
01f7180bb9 | ||
|
|
ce79213b21 | ||
|
|
3870732da8 | ||
|
|
60d7a469a7 | ||
|
|
921107354c | ||
|
|
9ea6120ded | ||
|
|
31493af69a | ||
|
|
732cf11d96 | ||
|
|
42c1c49a85 | ||
|
|
976652539c | ||
|
|
1e0b273579 | ||
|
|
5995434b97 | ||
|
|
2a854d53b6 | ||
|
|
695a9e4297 | ||
|
|
8a79046f66 | ||
|
|
27b2d3f853 | ||
|
|
11a6d6b0ee | ||
|
|
0fd470ad5e | ||
|
|
251c799e18 | ||
|
|
69b902081e | ||
|
|
b30012fd7b | ||
|
|
8dcf228faa | ||
|
|
3db3f87375 | ||
|
|
6b51999890 | ||
|
|
65b6b30f01 | ||
|
|
00be4845b8 | ||
|
|
4a3270d0a8 | ||
|
|
fa91f25ad3 | ||
|
|
f7bb06e109 | ||
|
|
340ec87522 | ||
|
|
9e6a74c633 | ||
|
|
7c05188e5f | ||
|
|
a19c37fa85 | ||
|
|
600a2b8fe2 | ||
|
|
402dbae44f | ||
|
|
75fddb9b69 | ||
|
|
6fff8630d4 | ||
|
|
06b4fc641e | ||
|
|
19d5411402 | ||
|
|
14160eaeb3 | ||
|
|
b06138f96d | ||
|
|
01d0b3645b | ||
|
|
0a4dcb4eaa | ||
|
|
37e338df0f | ||
|
|
6a0e27c816 | ||
|
|
289341dd91 | ||
|
|
45f9a15885 | ||
|
|
f982187be6 | ||
|
|
9d0d03c402 |
51
.env
51
.env
@@ -1,19 +1,32 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME=null
|
||||
BASE_URL=null
|
||||
CREDENTIALS_BASE_URL=null
|
||||
CSRF_TOKEN_API_PATH=null
|
||||
ECOMMERCE_BASE_URL=null
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME=null
|
||||
LMS_BASE_URL=null
|
||||
DEMOGRAPHICS_BASE_URL=null
|
||||
LOGIN_URL=null
|
||||
LOGOUT_URL=null
|
||||
MARKETING_SITE_BASE_URL=null
|
||||
NODE_ENV=null
|
||||
ORDER_HISTORY_URL=null
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=null
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME=null
|
||||
SUPPORT_URL=null
|
||||
USER_INFO_COOKIE_NAME=null
|
||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=null
|
||||
ACCESS_TOKEN_COOKIE_NAME=''
|
||||
BASE_URL=''
|
||||
COACHING_ENABLED=''
|
||||
CREDENTIALS_BASE_URL=''
|
||||
CSRF_TOKEN_API_PATH=''
|
||||
DEMOGRAPHICS_BASE_URL=''
|
||||
DISCOVERY_API_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL=''
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
FAVICON_URL=''
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME=''
|
||||
LMS_BASE_URL=''
|
||||
LOGIN_URL=''
|
||||
LOGO_TRADEMARK_URL=''
|
||||
LOGO_URL=''
|
||||
LOGO_WHITE_URL=''
|
||||
LOGOUT_URL=''
|
||||
MARKETING_SITE_BASE_URL=''
|
||||
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=''
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
ENABLE_DOB_UPDATE=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
APP_ID=
|
||||
MFE_CONFIG_API_URL=
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:1997'
|
||||
COACHING_ENABLED=''
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
DISCOVERY_API_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
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'
|
||||
MARKETING_SITE_BASE_URL='http://localhost:5335'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
NODE_ENV='development'
|
||||
ORDER_HISTORY_URL='localhost:1996/orders'
|
||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
PORT=1997
|
||||
PUBLISHER_BASE_URL=''
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME='edX'
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME=localhost
|
||||
STUDIO_BASE_URL=''
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
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
|
||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=null='http://localhost:8080'
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
ENABLE_DOB_UPDATE=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
APP_ID=
|
||||
MFE_CONFIG_API_URL=
|
||||
|
||||
|
||||
26
.env.test
26
.env.test
@@ -1,20 +1,32 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:1997'
|
||||
COACHING_ENABLED=''
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
DISCOVERY_API_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
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'
|
||||
MARKETING_SITE_BASE_URL='http://localhost:5335'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
NODE_ENV=null
|
||||
ORDER_HISTORY_URL='localhost:1996/orders'
|
||||
NODE_ENV=''
|
||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
PUBLISHER_BASE_URL=''
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME='edX'
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME=localhost
|
||||
STUDIO_BASE_URL=''
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
COACHING_ENABLED=''
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
ENABLE_DOB_UPDATE=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
APP_ID=
|
||||
MFE_CONFIG_API_URL=
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint');
|
||||
|
||||
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Run the workflow that adds new tickets that are either:
|
||||
# - labelled "DEPR"
|
||||
# - title starts with "[DEPR]"
|
||||
# - body starts with "Proposal Date" (this is the first template field)
|
||||
# to the org-wide DEPR project board
|
||||
|
||||
name: Add newly created DEPR issues to the DEPR project board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
routeissue:
|
||||
uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
|
||||
secrets:
|
||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
30
.github/workflows/ci.yml
vendored
Normal file
30
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
npm: [8.5.0]
|
||||
npm-test:
|
||||
- i18n_extract
|
||||
- lint
|
||||
- test
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: npm install -g npm@${{ matrix.npm }}
|
||||
- run: make requirements
|
||||
- run: make test NPM_TESTS=build
|
||||
- run: make test NPM_TESTS=${{ matrix.npm-test }}
|
||||
- name: upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
fail_ci_if_error: false
|
||||
10
.github/workflows/commitlint.yml
vendored
Normal file
10
.github/workflows/commitlint.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Run commitlint on the commit messages in a pull request.
|
||||
|
||||
name: Lint Commit Messages
|
||||
|
||||
on:
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
uses: openedx/.github/.github/workflows/commitlint.yml@master
|
||||
14
.github/workflows/lockfileversion-check.yml
vendored
Normal file
14
.github/workflows/lockfileversion-check.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
#check package-lock file version
|
||||
|
||||
name: Lockfile Version check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
|
||||
41
.github/workflows/update-browserlist.yml
vendored
Normal file
41
.github/workflows/update-browserlist.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Update Browserlist DB
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-dep:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
npm: [8.5.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: npm install -g npm@${{ matrix.npm }}
|
||||
- run: make requirements
|
||||
|
||||
- name: Update dependencies
|
||||
run: npx browserslist@latest --update-db
|
||||
|
||||
- name: setup testeng-ci
|
||||
run: |
|
||||
git clone https://github.com/edx/testeng-ci.git
|
||||
cd $GITHUB_WORKSPACE/testeng-ci
|
||||
pip install -r requirements/base.txt
|
||||
- name: create pull request
|
||||
id: createpullrequest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.requirements_bot_github_token }}
|
||||
GITHUB_USER_EMAIL: ${{ secrets.requirements_bot_github_email }}
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/testeng-ci
|
||||
python -m jenkins.pull_request_creator --repo-root=$GITHUB_WORKSPACE \
|
||||
--target-branch="master" --base-branch-name="update-browserslist" \
|
||||
--commit-message="chore: update browserslist" --pr-title="Update browserslist DB" \
|
||||
--pr-body="Updated browserlist DB" --delete-old-pull-requests --output-pr-url-for-github-action
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,3 +16,4 @@ temp/babel-plugin-react-intl
|
||||
*~
|
||||
/temp
|
||||
/.vscode
|
||||
/module.config.js
|
||||
|
||||
15
.travis.yml
15
.travis.yml
@@ -1,15 +0,0 @@
|
||||
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
|
||||
@@ -1,8 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[edx-platform.frontend-app-account]
|
||||
[o:open-edx:p:edx-platform:r:frontend-app-account]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
type = KEYVALUEJSON
|
||||
|
||||
|
||||
26
Makefile
Executable file → Normal file
26
Makefile
Executable file → Normal file
@@ -1,5 +1,6 @@
|
||||
export TRANSIFEX_RESOURCE = frontend-app-account
|
||||
transifex_resource = frontend-app-account
|
||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fr_CA"
|
||||
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
@@ -10,8 +11,19 @@ tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transi
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
|
||||
requirements:
|
||||
npm install
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
.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:
|
||||
# Pulling display strings from .jsx files into .json files...
|
||||
@@ -34,15 +46,15 @@ push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
|
||||
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments
|
||||
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -f --mode reviewed --language=$(transifex_langs)
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
108
README.rst
108
README.rst
@@ -1,48 +1,110 @@
|
||||
|Build Status| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
|
||||
|ci-badge| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
|
||||
|
||||
frontend-app-account
|
||||
====================
|
||||
|
||||
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.
|
||||
Please tag **@edx/community-engineering** on any PRs or issues. Thanks!
|
||||
|
||||
Development
|
||||
-----------
|
||||
Introduction
|
||||
------------
|
||||
|
||||
Start Devstack
|
||||
^^^^^^^^^^^^^^
|
||||
This is a micro-frontend application responsible for the display and updating of a user's account information.
|
||||
|
||||
To use this application `devstack <https://github.com/edx/devstack>`__ must be running and you must be logged into it.
|
||||
What is the domain of this MFE?
|
||||
|
||||
- Start devstack
|
||||
- Log in (http://localhost:18000/login)
|
||||
In this MFE: Private user settings UIs. Public facing profile is in a `separate MFE (Profile) <https://github.com/openedx/frontend-app-profile>`_
|
||||
|
||||
Start the development server
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Account settings page
|
||||
|
||||
In this project, install requirements and start the development server by running:
|
||||
- Demographics collection
|
||||
|
||||
.. code:: bash
|
||||
- IDV (Identity Verification)
|
||||
|
||||
npm install
|
||||
npm start # The server will run on port 1997
|
||||
Installation
|
||||
------------
|
||||
|
||||
Once the dev server is up visit http://localhost:1997.
|
||||
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
|
||||
|
||||
Configuration and Deployment
|
||||
----------------------------
|
||||
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
|
||||
|
||||
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:
|
||||
2. Start up Devstack, if it's not already started.
|
||||
|
||||
3. Log in to Devstack (http://localhost:18000/login )
|
||||
|
||||
4. Within this project, install requirements and start the development server:
|
||||
|
||||
.. code-block::
|
||||
|
||||
npm install
|
||||
npm start # The server will run on port 1997
|
||||
|
||||
5. Once the dev server is up, visit http://localhost:1997 to access the MFE
|
||||
|
||||
.. image:: ./docs/images/localhost_preview.png
|
||||
|
||||
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
|
||||
|
||||
NODE_ENV=development ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' npm run build
|
||||
|
||||
|
||||
For more information see the document: `Micro-frontend applications in Open
|
||||
edX <https://github.com/edx/edx-developer-docs/blob/5191e800bf16cf42f25c58c58f983bdaf7f9305d/docs/micro-frontends-in-open-edx.rst>`__.
|
||||
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
|
||||
.. |Build Status| image:: https://api.travis-ci.org/edx/frontend-app-account.svg?branch=master
|
||||
:target: https://travis-ci.org/edx/frontend-app-account
|
||||
Known Issues
|
||||
------------
|
||||
|
||||
None
|
||||
|
||||
Development Roadmap
|
||||
-------------------
|
||||
|
||||
We don't have anything planned for the core of the MFE (the account settings page) - this MFE is currently in maintenance mode.
|
||||
There may be a replacement for IDV coming down the pipe, so that may be DEPRed.
|
||||
In the future, it's possible that demographics could be modeled as a plugin rather than being hard-coded into this MFE.
|
||||
|
||||
|
||||
==============================
|
||||
|
||||
.. |ci-badge| image:: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml/badge.svg
|
||||
:target: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml
|
||||
:alt: Continuous Integration
|
||||
.. |Codecov| image:: https://img.shields.io/codecov/c/github/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
|
||||
|
||||
BIN
docs/images/localhost_preview.png
Normal file
BIN
docs/images/localhost_preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
@@ -1,7 +1,7 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFiles: [
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
});
|
||||
|
||||
@@ -3,5 +3,4 @@
|
||||
|
||||
nick: acct
|
||||
oeps: {}
|
||||
owner: edx/arch-team
|
||||
openedx-release: {ref: master}
|
||||
|
||||
53182
package-lock.json
generated
53182
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
98
package.json
98
package.json
@@ -6,49 +6,48 @@
|
||||
"license": "AGPL-3.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-account.git"
|
||||
"url": "git+https://github.com/openedx/frontend-app-account.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"lint": "fedx-scripts eslint",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests"
|
||||
"test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/edx/frontend-app-account/issues"
|
||||
"url": "https://github.com/openedx/frontend-app-account/issues"
|
||||
},
|
||||
"homepage": "https://github.com/edx/frontend-app-account#readme",
|
||||
"homepage": "https://github.com/openedx/frontend-app-account#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
"ie 11"
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/frontend-component-footer": "10.0.11",
|
||||
"@edx/frontend-component-header": "2.0.5",
|
||||
"@edx/frontend-platform": "1.6.1",
|
||||
"@edx/paragon": "9.1.1",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.32",
|
||||
"@fortawesome/free-brands-svg-icons": "5.8.2",
|
||||
"@fortawesome/free-regular-svg-icons": "5.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "5.8.2",
|
||||
"@fortawesome/react-fontawesome": "0.1.12",
|
||||
"@tensorflow-models/blazeface": "git+https://github.com/alangsto/blazeface.git",
|
||||
"@tensorflow/tfjs-converter": "1.6.1",
|
||||
"@tensorflow/tfjs-core": "1.6.1",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"bowser": "^2.10.0",
|
||||
"classnames": "2.2.6",
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "11.5.2",
|
||||
"@edx/frontend-component-header": "3.5.0",
|
||||
"@edx/frontend-platform": "2.6.2",
|
||||
"@edx/paragon": "^20.20.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@tensorflow-models/blazeface": "0.0.7",
|
||||
"@tensorflow/tfjs-converter": "3.21.0",
|
||||
"@tensorflow/tfjs-core": "3.21.0",
|
||||
"bowser": "2.11.0",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.26.1",
|
||||
"font-awesome": "4.7.0",
|
||||
"form-urlencoded": "4.0.1",
|
||||
"formdata-polyfill": "3.0.20",
|
||||
"form-urlencoded": "4.5.1",
|
||||
"formdata-polyfill": "4.0.10",
|
||||
"history": "4.10.1",
|
||||
"jslib-html5-camera-photo": "^3.1.3",
|
||||
"jslib-html5-camera-photo": "3.3.4",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.findindex": "4.6.0",
|
||||
@@ -57,39 +56,38 @@
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.omit": "4.5.0",
|
||||
"lodash.pick": "4.4.0",
|
||||
"lodash.pickby": "^4.6.0",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"memoize-one": "5.1.1",
|
||||
"newrelic": "5.13.1",
|
||||
"lodash.pickby": "4.6.0",
|
||||
"long": "5.2.1",
|
||||
"memoize-one": "5.2.1",
|
||||
"prop-types": "15.7.2",
|
||||
"qs": "6.9.4",
|
||||
"react": "16.10.2",
|
||||
"react-dom": "16.10.2",
|
||||
"react-redux": "7.1.3",
|
||||
"react-router": "5.1.2",
|
||||
"react-router-dom": "5.1.2",
|
||||
"react-router-hash-link": "1.2.2",
|
||||
"qs": "6.10.3",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "5.2.1",
|
||||
"react-router-dom": "5.3.0",
|
||||
"react-router-hash-link": "2.4.3",
|
||||
"react-scrollspy": "3.4.3",
|
||||
"react-transition-group": "4.3.0",
|
||||
"redux": "4.0.5",
|
||||
"redux-devtools-extension": "2.13.8",
|
||||
"react-transition-group": "4.4.5",
|
||||
"redux": "4.1.2",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-saga": "1.1.3",
|
||||
"redux-thunk": "2.3.0",
|
||||
"regenerator-runtime": "0.13.11",
|
||||
"reselect": "4.0.0",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "5.3.2",
|
||||
"@testing-library/jest-dom": "^5.11.2",
|
||||
"@testing-library/react": "^10.4.7",
|
||||
"codecov": "3.7.2",
|
||||
"enzyme": "3.10.0",
|
||||
"enzyme-adapter-react-16": "1.15.5",
|
||||
"es-check": "5.0.0",
|
||||
"husky": "3.0.9",
|
||||
"jest": "^26.1.0",
|
||||
"react-test-renderer": "16.8.6",
|
||||
"@edx/browserslist-config": "1.1.1",
|
||||
"@edx/frontend-build": "12.4.0",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.7",
|
||||
"react-test-renderer": "16.14.0",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "1.5.4"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title>Account | edX</title>
|
||||
<title>Account | <%= process.env.SITE_NAME %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=webpackConfig.output.publicPath%>favicon.ico" type="image/x-icon" />
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||
<% if (process.env.OPTIMIZELY_PROJECT_ID) { %>
|
||||
<script
|
||||
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
|
||||
></script>
|
||||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
"config:base",
|
||||
"schedule:weekly",
|
||||
":automergeLinters",
|
||||
":automergeMinor",
|
||||
":automergeTesters",
|
||||
":enableVulnerabilityAlerts",
|
||||
":rebaseStalePrs",
|
||||
":semanticCommits",
|
||||
":updateNotScheduled"
|
||||
],
|
||||
"patch": {
|
||||
"automerge": true
|
||||
},
|
||||
"rebaseStalePrs": true
|
||||
"packageRules": [
|
||||
{
|
||||
"matchDepTypes": [
|
||||
"devDependencies"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"lockFileMaintenance",
|
||||
"minor",
|
||||
"patch",
|
||||
"pin"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["@edx"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"timezone": "America/New_York"
|
||||
}
|
||||
|
||||
@@ -13,40 +13,52 @@ import {
|
||||
getCountryList,
|
||||
getLanguageList,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import {
|
||||
Button, Hyperlink, Icon, Alert,
|
||||
} from '@edx/paragon';
|
||||
import { CheckCircle, Error, WarningFilled } from '@edx/paragon/icons';
|
||||
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
import { fetchSettings, saveSettings, updateDraft } from './data/actions';
|
||||
import {
|
||||
fetchSettings,
|
||||
saveMultipleSettings,
|
||||
saveSettings,
|
||||
updateDraft,
|
||||
beginNameChange,
|
||||
} from './data/actions';
|
||||
import { accountSettingsPageSelector } from './data/selectors';
|
||||
import PageLoading from './PageLoading';
|
||||
import Alert from './Alert';
|
||||
import JumpNav from './JumpNav';
|
||||
import DeleteAccount from './delete-account';
|
||||
import EditableField from './EditableField';
|
||||
import EditableSelectField from './EditableSelectField';
|
||||
import ResetPassword from './reset-password';
|
||||
import NameChange from './name-change';
|
||||
import ThirdPartyAuth from './third-party-auth';
|
||||
import BetaLanguageBanner from './BetaLanguageBanner';
|
||||
import EmailField from './EmailField';
|
||||
import OneTimeDismissibleAlert from './OneTimeDismissibleAlert';
|
||||
import DOBModal from './DOBForm';
|
||||
import {
|
||||
YEAR_OF_BIRTH_OPTIONS,
|
||||
EDUCATION_LEVELS,
|
||||
GENDER_OPTIONS,
|
||||
COUNTRY_WITH_STATES,
|
||||
COPPA_COMPLIANCE_YEAR,
|
||||
getStatesList,
|
||||
} from './data/constants';
|
||||
import { fetchSiteLanguages } from './site-language';
|
||||
import CoachingToggle from './coaching/CoachingToggle';
|
||||
import DemographicsSection from './demographics/DemographicsSection';
|
||||
|
||||
|
||||
class AccountSettingsPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
// If there is a "duplicate_provider" query parameter, that's the backend's
|
||||
// way of telling us that the provider account the user tried to link is already linked
|
||||
// to another Open edX account. We use this to display a message to that effect, and remove the
|
||||
// parameter from the URL.
|
||||
// to another user account on the platform. We use this to display a message to that effect,
|
||||
// and remove the parameter from the URL.
|
||||
const duplicateTpaProvider = getQueryParameters().duplicate_provider;
|
||||
if (duplicateTpaProvider !== undefined) {
|
||||
history.replace(history.location.pathname);
|
||||
@@ -81,10 +93,11 @@ class AccountSettingsPage extends React.Component {
|
||||
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')
|
||||
if (typeof locationHash !== 'string') {
|
||||
return;
|
||||
}
|
||||
if (Object.keys(this.navLinkRefs).includes(locationHash) && this.navLinkRefs[locationHash].current) {
|
||||
window.scrollTo(0, this.navLinkRefs[locationHash].current.offsetTop)
|
||||
window.scrollTo(0, this.navLinkRefs[locationHash].current.offsetTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,6 +149,42 @@ class AccountSettingsPage extends React.Component {
|
||||
})),
|
||||
}));
|
||||
|
||||
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) {
|
||||
return !this.props.staticFields.includes(fieldName);
|
||||
}
|
||||
@@ -146,14 +195,6 @@ class AccountSettingsPage extends React.Component {
|
||||
return Boolean(this.props.profileDataManager);
|
||||
}
|
||||
|
||||
handleEditableFieldChange = (name, value) => {
|
||||
this.props.updateDraft(name, value);
|
||||
};
|
||||
|
||||
handleSubmit = (formId, values) => {
|
||||
this.props.saveSettings(formId, values);
|
||||
};
|
||||
|
||||
renderDuplicateTpaProviderMessage() {
|
||||
if (!this.state.duplicateTpaProvider) {
|
||||
return null;
|
||||
@@ -161,13 +202,14 @@ class AccountSettingsPage extends React.Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Alert className="alert alert-danger" role="alert">
|
||||
<Alert variant="danger">
|
||||
<FormattedMessage
|
||||
id="account.settings.message.duplicate.tpa.provider"
|
||||
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 edX account"
|
||||
defaultMessage="The {provider} account you selected is already linked to another {siteName} account."
|
||||
description="alert message informing the user that the third-party account they attempted to link is already linked to another account"
|
||||
values={{
|
||||
provider: <b>{this.state.duplicateTpaProvider}</b>,
|
||||
siteName: getConfig().SITE_NAME,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
@@ -182,7 +224,7 @@ class AccountSettingsPage extends React.Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Alert className="alert alert-primary" role="alert">
|
||||
<Alert variant="info">
|
||||
<FormattedMessage
|
||||
id="account.settings.message.managed.settings"
|
||||
defaultMessage="Your profile settings are managed by {managerTitle}. Contact your administrator or {support} for help."
|
||||
@@ -205,6 +247,170 @@ class AccountSettingsPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderFullNameHelpText = (status, proctoredExamId) => {
|
||||
if (!this.props.verifiedNameHistory) {
|
||||
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text']);
|
||||
}
|
||||
|
||||
let messageString = 'account.settings.field.full.name.help.text';
|
||||
|
||||
if (status === 'submitted') {
|
||||
messageString += '.submitted';
|
||||
if (proctoredExamId) {
|
||||
messageString += '.proctored';
|
||||
}
|
||||
} else {
|
||||
messageString += '.default';
|
||||
}
|
||||
|
||||
if (!this.props.committedValues.useVerifiedNameForCerts) {
|
||||
messageString += '.certificate';
|
||||
}
|
||||
|
||||
return this.props.intl.formatMessage(messages[messageString]);
|
||||
};
|
||||
|
||||
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,
|
||||
proctored_exam_attempt_id: proctoredExamId,
|
||||
} = 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;
|
||||
}
|
||||
|
||||
if (proctoredExamId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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, proctoredExamId) => {
|
||||
let messageStr = 'account.settings.field.name.verified.help.text';
|
||||
|
||||
// add additional string based on status
|
||||
if (status === 'approved') {
|
||||
messageStr += '.verified';
|
||||
} else if (status === 'submitted') {
|
||||
messageStr += '.submitted';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// add additional string if verified name came from a proctored exam attempt
|
||||
if (proctoredExamId) {
|
||||
messageStr += '.proctored';
|
||||
}
|
||||
|
||||
// add additional string based on certificate name use
|
||||
if (this.props.committedValues.useVerifiedNameForCerts) {
|
||||
messageStr += '.certificate';
|
||||
}
|
||||
|
||||
return this.props.intl.formatMessage(messages[messageStr]);
|
||||
};
|
||||
|
||||
renderEmptyStaticFieldMessage() {
|
||||
if (this.isManagedProfile()) {
|
||||
return this.props.intl.formatMessage(messages['account.settings.static.field.empty'], {
|
||||
@@ -214,8 +420,15 @@ class AccountSettingsPage extends React.Component {
|
||||
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) {
|
||||
if (!Boolean(this.props.formValues.secondary_email_enabled)) {
|
||||
if (!this.props.formValues.secondary_email_enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -235,11 +448,10 @@ class AccountSettingsPage extends React.Component {
|
||||
// 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']}/>
|
||||
<DemographicsSection forwardRef={this.navLinkRefs['#demographics-information']} />
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
@@ -259,7 +471,9 @@ class AccountSettingsPage extends React.Component {
|
||||
} = this.getLocalizedOptions(this.context.locale, this.props.formValues.country);
|
||||
|
||||
// Show State field only if the country is US (could include Canada later)
|
||||
const showState = this.props.formValues.country == COUNTRY_WITH_STATES;
|
||||
const showState = this.props.formValues.country === COUNTRY_WITH_STATES;
|
||||
|
||||
const { verifiedName } = this.props;
|
||||
|
||||
const timeZoneOptions = this.getLocalizedTimeZoneOptions(
|
||||
this.props.timeZoneOptions,
|
||||
@@ -269,110 +483,193 @@ class AccountSettingsPage extends React.Component {
|
||||
|
||||
const hasLinkedTPA = findIndex(this.props.tpaProviders, provider => provider.connected) >= 0;
|
||||
|
||||
// if user is under 13 and does not have cookie set
|
||||
const shouldUpdateDOB = (
|
||||
getConfig().ENABLE_COPPA_COMPLIANCE
|
||||
&& getConfig().ENABLE_DOB_UPDATE
|
||||
&& this.props.formValues.year_of_birth.toString() >= COPPA_COMPLIANCE_YEAR.toString()
|
||||
&& !localStorage.getItem('submittedDOB')
|
||||
);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="account-section" id="basic-information" ref={this.navLinkRefs['#basic-information']}>
|
||||
<h2 className="section-heading">
|
||||
<>
|
||||
{ shouldUpdateDOB
|
||||
&& (
|
||||
<DOBModal
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
)}
|
||||
<div className="account-section pt-3 mb-5" id="basic-information" ref={this.navLinkRefs['#basic-information']}>
|
||||
{
|
||||
this.props.mostRecentVerifiedName
|
||||
&& this.renderVerifiedNameMessage(this.props.mostRecentVerifiedName)
|
||||
}
|
||||
{localStorage.getItem('submittedDOB')
|
||||
&& (
|
||||
<OneTimeDismissibleAlert
|
||||
id="updated-dob"
|
||||
variant="success"
|
||||
icon={CheckCircle}
|
||||
header={this.props.intl.formatMessage(messages['account.settings.field.dob.form.success'])}
|
||||
body=""
|
||||
/>
|
||||
)}
|
||||
|
||||
<h2 className="section-heading h4 mb-3">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.account.information'])}
|
||||
</h2>
|
||||
<p>{this.props.intl.formatMessage(messages['account.settings.section.account.information.description'])}</p>
|
||||
{this.renderManagedProfileMessage()}
|
||||
|
||||
{this.renderNameChangeModal()}
|
||||
|
||||
<EditableField
|
||||
name="username"
|
||||
type="text"
|
||||
value={this.props.formValues.username}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.username'])}
|
||||
helpText={this.props.intl.formatMessage(messages['account.settings.field.username.help.text'])}
|
||||
helpText={this.props.intl.formatMessage(
|
||||
messages['account.settings.field.username.help.text'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
isEditable={false}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="name"
|
||||
type="text"
|
||||
value={this.props.formValues.name}
|
||||
value={
|
||||
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'])}
|
||||
emptyLabel={
|
||||
this.isEditable('name') ?
|
||||
this.props.intl.formatMessage(messages['account.settings.field.full.name.empty']) :
|
||||
this.renderEmptyStaticFieldMessage()
|
||||
this.isEditable('name')
|
||||
? this.props.intl.formatMessage(messages['account.settings.field.full.name.empty'])
|
||||
: this.renderEmptyStaticFieldMessage()
|
||||
}
|
||||
helpText={this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])}
|
||||
isEditable={this.isEditable('name')}
|
||||
{...editableFieldProps}
|
||||
helpText={
|
||||
verifiedName
|
||||
? this.renderFullNameHelpText(verifiedName.status, verifiedName.proctored_exam_attempt_id)
|
||||
: this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])
|
||||
}
|
||||
isEditable={
|
||||
verifiedName
|
||||
? this.isEditable('verifiedName') && this.isEditable('name')
|
||||
: this.isEditable('name')
|
||||
}
|
||||
isGrayedOut={
|
||||
verifiedName && !this.isEditable('verifiedName')
|
||||
}
|
||||
onChange={this.handleEditableFieldChange}
|
||||
onSubmit={this.handleSubmitProfileName}
|
||||
/>
|
||||
{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, verifiedName.proctored_exam_attempt_id)}
|
||||
isEditable={this.isEditable('verifiedName')}
|
||||
isGrayedOut={!this.isEditable('verifiedName')}
|
||||
onChange={this.handleEditableFieldChange}
|
||||
onSubmit={this.handleSubmitVerifiedName}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EmailField
|
||||
name="email"
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.email'])}
|
||||
emptyLabel={
|
||||
this.isEditable('email') ?
|
||||
this.props.intl.formatMessage(messages['account.settings.field.email.empty']) :
|
||||
this.renderEmptyStaticFieldMessage()
|
||||
this.isEditable('email')
|
||||
? this.props.intl.formatMessage(messages['account.settings.field.email.empty'])
|
||||
: this.renderEmptyStaticFieldMessage()
|
||||
}
|
||||
value={this.props.formValues.email}
|
||||
confirmationMessageDefinition={messages['account.settings.field.email.confirmation']}
|
||||
helpText={this.props.intl.formatMessage(messages['account.settings.field.email.help.text'])}
|
||||
helpText={this.props.intl.formatMessage(
|
||||
messages['account.settings.field.email.help.text'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
isEditable={this.isEditable('email')}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
{this.renderSecondaryEmailField(editableFieldProps)}
|
||||
<ResetPassword email={this.props.formValues.email} />
|
||||
<EditableField
|
||||
name="year_of_birth"
|
||||
type="select"
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.dob'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.dob.empty'])}
|
||||
value={this.props.formValues.year_of_birth}
|
||||
options={yearOfBirthOptions}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
{(!getConfig().ENABLE_COPPA_COMPLIANCE)
|
||||
&& (
|
||||
<EditableSelectField
|
||||
name="year_of_birth"
|
||||
type="select"
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.dob'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.dob.empty'])}
|
||||
value={this.props.formValues.year_of_birth}
|
||||
options={yearOfBirthOptions}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
)}
|
||||
<EditableSelectField
|
||||
name="country"
|
||||
type="select"
|
||||
value={this.props.formValues.country}
|
||||
options={countryOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.country'])}
|
||||
emptyLabel={
|
||||
this.isEditable('country') ?
|
||||
this.props.intl.formatMessage(messages['account.settings.field.country.empty']) :
|
||||
this.renderEmptyStaticFieldMessage()
|
||||
this.isEditable('country')
|
||||
? this.props.intl.formatMessage(messages['account.settings.field.country.empty'])
|
||||
: this.renderEmptyStaticFieldMessage()
|
||||
}
|
||||
isEditable={this.isEditable('country')}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
{showState &&
|
||||
<EditableField
|
||||
{showState
|
||||
&& (
|
||||
<EditableSelectField
|
||||
name="state"
|
||||
type="select"
|
||||
value={this.props.formValues.state}
|
||||
options={stateOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.state'])}
|
||||
emptyLabel={
|
||||
this.isEditable('state') ?
|
||||
this.props.intl.formatMessage(messages['account.settings.field.state.empty']) :
|
||||
this.renderEmptyStaticFieldMessage()
|
||||
this.isEditable('state')
|
||||
? this.props.intl.formatMessage(messages['account.settings.field.state.empty'])
|
||||
: this.renderEmptyStaticFieldMessage()
|
||||
}
|
||||
isEditable={this.isEditable('state')}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="account-section" id="profile-information" ref={this.navLinkRefs['#profile-information']}>
|
||||
<h2 className="section-heading">
|
||||
<div className="account-section pt-3 mb-5" id="profile-information" ref={this.navLinkRefs['#profile-information']}>
|
||||
<h2 className="section-heading h4 mb-3">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.profile.information'])}
|
||||
</h2>
|
||||
|
||||
<EditableField
|
||||
<EditableSelectField
|
||||
name="level_of_education"
|
||||
type="select"
|
||||
value={this.props.formValues.level_of_education}
|
||||
options={educationLevelOptions}
|
||||
options={getConfig().ENABLE_COPPA_COMPLIANCE
|
||||
? educationLevelOptions.filter(option => option.value !== 'el')
|
||||
: educationLevelOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.education'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.education.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
<EditableSelectField
|
||||
name="gender"
|
||||
type="select"
|
||||
value={this.props.formValues.gender}
|
||||
@@ -381,7 +678,7 @@ class AccountSettingsPage extends React.Component {
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.gender.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
<EditableSelectField
|
||||
name="language_proficiencies"
|
||||
type="select"
|
||||
value={this.props.formValues.language_proficiencies}
|
||||
@@ -390,21 +687,27 @@ class AccountSettingsPage extends React.Component {
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
{getConfig().COACHING_ENABLED &&
|
||||
this.props.formValues.coaching.eligible_for_coaching &&
|
||||
{getConfig().COACHING_ENABLED
|
||||
&& this.props.formValues.coaching.eligible_for_coaching
|
||||
&& (
|
||||
<CoachingToggle
|
||||
name="coaching"
|
||||
phone_number={this.props.formValues.phone_number}
|
||||
coaching={this.props.formValues.coaching}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && this.renderDemographicsSection()}
|
||||
<div className="account-section" id="social-media">
|
||||
<h2 className="section-heading">
|
||||
<div className="account-section pt-3 mb-5" id="social-media">
|
||||
<h2 className="section-heading h4 mb-3">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}
|
||||
</h2>
|
||||
<p>{this.props.intl.formatMessage(messages['account.settings.section.social.media.description'])}</p>
|
||||
<p>
|
||||
{this.props.intl.formatMessage(
|
||||
messages['account.settings.section.social.media.description'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
|
||||
<EditableField
|
||||
name="social_link_linkedin"
|
||||
@@ -432,13 +735,13 @@ class AccountSettingsPage extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="account-section" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}>
|
||||
<h2 className="section-heading">
|
||||
<div className="account-section pt-3 mb-5" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}>
|
||||
<h2 className="section-heading h4 mb-3">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.site.preferences'])}
|
||||
</h2>
|
||||
|
||||
<BetaLanguageBanner />
|
||||
<EditableField
|
||||
<EditableSelectField
|
||||
name="siteLanguage"
|
||||
type="select"
|
||||
options={this.props.siteLanguageOptions}
|
||||
@@ -447,7 +750,7 @@ class AccountSettingsPage extends React.Component {
|
||||
helpText={this.props.intl.formatMessage(messages['account.settings.field.site.language.help.text'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
<EditableSelectField
|
||||
name="time_zone"
|
||||
type="select"
|
||||
value={this.props.formValues.time_zone}
|
||||
@@ -463,20 +766,25 @@ class AccountSettingsPage extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="account-section" id="linked-accounts" ref={this.navLinkRefs['#linked-accounts']}>
|
||||
<h2 className="section-heading">{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts'])}</h2>
|
||||
<p>{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts.description'])}</p>
|
||||
<div className="account-section pt-3 mb-5" id="linked-accounts" ref={this.navLinkRefs['#linked-accounts']}>
|
||||
<h2 className="section-heading h4 mb-3">{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts'])}</h2>
|
||||
<p>
|
||||
{this.props.intl.formatMessage(
|
||||
messages['account.settings.section.linked.accounts.description'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<ThirdPartyAuth />
|
||||
</div>
|
||||
|
||||
<div className="account-section" id="delete-account" ref={this.navLinkRefs['#delete-account']}>
|
||||
<div className="account-section pt-3 mb-5" id="delete-account" ref={this.navLinkRefs['#delete-account']}>
|
||||
<DeleteAccount
|
||||
isVerifiedAccount={this.props.isActive}
|
||||
hasLinkedTPA={hasLinkedTPA}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -542,11 +850,13 @@ AccountSettingsPage.propTypes = {
|
||||
name: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
secondary_email: PropTypes.string,
|
||||
secondary_email_enabled: PropTypes.bool,
|
||||
year_of_birth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
country: PropTypes.string,
|
||||
level_of_education: PropTypes.string,
|
||||
gender: PropTypes.string,
|
||||
language_proficiencies: PropTypes.string,
|
||||
pending_name_change: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
social_link_linkedin: PropTypes.string,
|
||||
social_link_facebook: PropTypes.string,
|
||||
@@ -557,7 +867,20 @@ AccountSettingsPage.propTypes = {
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
}),
|
||||
state: PropTypes.string,
|
||||
shouldDisplayDemographicsSection: PropTypes.bool,
|
||||
useVerifiedNameForCerts: PropTypes.bool.isRequired,
|
||||
verified_name: PropTypes.string,
|
||||
}).isRequired,
|
||||
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({
|
||||
previousValue: PropTypes.string,
|
||||
draft: PropTypes.string,
|
||||
@@ -581,15 +904,45 @@ AccountSettingsPage.propTypes = {
|
||||
})),
|
||||
fetchSiteLanguages: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
saveMultipleSettings: PropTypes.func.isRequired,
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
fetchSettings: PropTypes.func.isRequired,
|
||||
tpaProviders: PropTypes.arrayOf(PropTypes.object),
|
||||
beginNameChange: PropTypes.func.isRequired,
|
||||
tpaProviders: PropTypes.arrayOf(PropTypes.shape({
|
||||
connected: PropTypes.bool,
|
||||
})),
|
||||
nameChangeModal: PropTypes.shape({
|
||||
formId: PropTypes.string,
|
||||
}),
|
||||
verifiedName: PropTypes.shape({
|
||||
verified_name: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
proctored_exam_attempt_id: PropTypes.number,
|
||||
}),
|
||||
mostRecentVerifiedName: PropTypes.shape({
|
||||
verified_name: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
proctored_exam_attempt_id: PropTypes.number,
|
||||
}),
|
||||
verifiedNameHistory: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
verified_name: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
proctored_exam_attempt_id: PropTypes.number,
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
AccountSettingsPage.defaultProps = {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
loadingError: null,
|
||||
committedValues: {
|
||||
useVerifiedNameForCerts: false,
|
||||
verified_name: null,
|
||||
},
|
||||
drafts: {},
|
||||
formErrors: {},
|
||||
siteLanguage: null,
|
||||
siteLanguageOptions: [],
|
||||
timeZoneOptions: [],
|
||||
@@ -599,11 +952,17 @@ AccountSettingsPage.defaultProps = {
|
||||
tpaProviders: [],
|
||||
isActive: true,
|
||||
secondary_email_enabled: false,
|
||||
nameChangeModal: {},
|
||||
verifiedName: null,
|
||||
mostRecentVerifiedName: {},
|
||||
verifiedNameHistory: [],
|
||||
};
|
||||
|
||||
export default connect(accountSettingsPageSelector, {
|
||||
fetchSettings,
|
||||
saveSettings,
|
||||
saveMultipleSettings,
|
||||
updateDraft,
|
||||
fetchSiteLanguages,
|
||||
beginNameChange,
|
||||
})(injectIntl(AccountSettingsPage));
|
||||
|
||||
@@ -63,7 +63,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'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 edX.',
|
||||
defaultMessage: 'You can link your identity accounts to simplify signing in to {siteName}.',
|
||||
description: 'The linked accounts section heading description.',
|
||||
},
|
||||
'account.settings.field.username': {
|
||||
@@ -73,7 +73,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'account.settings.field.username.help.text': {
|
||||
id: 'account.settings.field.username.help.text',
|
||||
defaultMessage: 'The name that identifies you on edX. You cannot change your username.',
|
||||
defaultMessage: 'The name that identifies you on {siteName}. You cannot change your username.',
|
||||
description: 'Help text for the account settings username field.',
|
||||
},
|
||||
'account.settings.field.full.name': {
|
||||
@@ -91,6 +91,126 @@ const messages = defineMessages({
|
||||
defaultMessage: 'The name that is used for ID verification and that appears on your certificates.',
|
||||
description: 'Help text for the account settings name field.',
|
||||
},
|
||||
'account.settings.field.full.name.help.text.default': {
|
||||
id: 'account.settings.field.full.name.help.text.default',
|
||||
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.default.certificate': {
|
||||
id: 'account.settings.field.full.name.help.text.default.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 photo ID.',
|
||||
description: 'Help text for the account settings verified name field when the name is verified.',
|
||||
},
|
||||
'account.settings.field.name.verified.help.text.verified.proctored': {
|
||||
id: 'account.settings.field.name.verified.help.text.verified.proctored',
|
||||
defaultMessage: 'This name has been verified by proctoring.',
|
||||
description: 'Help text for the account settings verified name field when the name is verified through proctoring.',
|
||||
},
|
||||
'account.settings.field.name.verified.help.text.verified.certificate': {
|
||||
id: 'account.settings.field.name.verified.help.text.verified.certificate',
|
||||
defaultMessage: 'This name has been verified by photo ID, and is 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.verified.proctored.certificate': {
|
||||
id: 'account.settings.field.name.verified.help.text.verified.proctored.certificate',
|
||||
defaultMessage: 'This name has been verified by proctoring, and is 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, and the name is verified through proctoring.',
|
||||
},
|
||||
'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.proctored': {
|
||||
id: 'account.settings.field.name.verified.help.text.submitted.proctored',
|
||||
defaultMessage: 'Your proctored exam has been submitted. Verified name cannot be changed at this time. Please check back in 2-5 days.',
|
||||
description: 'Help text for the account settings verified name field when a verified name has been submitted through proctoring.',
|
||||
},
|
||||
'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.help.text.submitted.proctored.certificate': {
|
||||
id: 'account.settings.field.name.verified.help.text.submitted.proctored.certificate',
|
||||
defaultMessage: 'Once your proctored exam passes review, this name will appear on your certificate and public-facing records. Verified Name cannot be changed at this time.',
|
||||
description: 'Help text for the account settings verified name field when a verified name has been submitted through proctoring and will appear on certificates.',
|
||||
},
|
||||
'account.settings.field.name.verified.verification.alert': {
|
||||
id: 'account.settings.field.name.verified.verification.help',
|
||||
defaultMessage: 'Enter your name as it appears on your unexpired student, work, or government-issued identification card.',
|
||||
description: 'Form label instructing the user to enter the name on their ID.',
|
||||
},
|
||||
'account.settings.field.full.name.help.text.submitted': {
|
||||
id: 'account.settings.field.full.name.help.text.submitted',
|
||||
defaultMessage: 'Verification has been submitted. This usually takes 48 hours or less. Full name cannot be changed at this time.',
|
||||
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.proctored': {
|
||||
id: 'account.settings.field.full.name.help.text.submitted.proctored',
|
||||
defaultMessage: 'Your proctored exam has been submitted. Full name cannot be changed at this time. Please check back in 2-5 days.',
|
||||
description: 'Help text for the account settings full name field when a verified name has been submitted through proctoring.',
|
||||
},
|
||||
'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.full.name.help.text.submitted.proctored.certificate': {
|
||||
id: 'account.settings.field.full.name.help.text.submitted.proctored.certificate',
|
||||
defaultMessage: 'Once your proctored exam passes review, 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': {
|
||||
id: 'account.settings.field.email',
|
||||
defaultMessage: 'Email address (Sign in)',
|
||||
@@ -108,7 +228,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'account.settings.field.email.help.text': {
|
||||
id: 'account.settings.field.email.help.text',
|
||||
defaultMessage: 'You receive messages from edX and course teams at this address.',
|
||||
defaultMessage: 'You receive messages from {siteName} and course teams at this address.',
|
||||
description: 'Help text for the account settings email field.',
|
||||
},
|
||||
'account.settings.field.secondary.email': {
|
||||
@@ -146,6 +266,56 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Select a year of birth',
|
||||
description: 'Option for empty value on account settings year of birth field.',
|
||||
},
|
||||
'account.settings.field.dob.month': {
|
||||
id: 'account.settings.field.dob.month',
|
||||
defaultMessage: 'Month',
|
||||
description: 'Label for account settings month of birth field.',
|
||||
},
|
||||
'account.settings.field.dob.year': {
|
||||
id: 'account.settings.field.dob.year',
|
||||
defaultMessage: 'Year',
|
||||
description: 'Label for account settings year of birth field.',
|
||||
},
|
||||
'account.settings.field.dob.month.default': {
|
||||
id: 'account.settings.field.month.year.default',
|
||||
defaultMessage: 'Select month',
|
||||
description: 'Default label for account settings month of birth field.',
|
||||
},
|
||||
'account.settings.field.dob.year.default': {
|
||||
id: 'account.settings.field.dob.year.default',
|
||||
defaultMessage: 'Select year',
|
||||
description: 'Default label for account settings year of birth field.',
|
||||
},
|
||||
'account.settings.field.dob.form.button': {
|
||||
id: 'account.settings.field.dob.form.button',
|
||||
defaultMessage: 'Please confirm your date of birth',
|
||||
description: 'Message to prompt user to enter dob',
|
||||
},
|
||||
'account.settings.field.dob.form.title': {
|
||||
id: 'account.settings.field.dob.form.title',
|
||||
defaultMessage: 'Enter your birth month and year',
|
||||
description: 'Title of DOB form',
|
||||
},
|
||||
'account.settings.field.dob.form.help.text': {
|
||||
id: 'account.settings.field.dob.form.help.text',
|
||||
defaultMessage: 'We ask for birth month and year information to help us comply with our legal obligations.',
|
||||
description: 'Help text for DOB form',
|
||||
},
|
||||
'account.settings.field.dob.form.success': {
|
||||
id: 'account.settings.field.dob.form.success',
|
||||
defaultMessage: 'Thank you for entering your information.',
|
||||
description: 'Title of banner when date of birth is successfully entered',
|
||||
},
|
||||
'account.settings.field.month_of_birth.options.empty': {
|
||||
id: 'account.settings.field.month_of_birth.options.empty',
|
||||
defaultMessage: 'Select a month of birth',
|
||||
description: 'Option for empty value on account settings month of birth field.',
|
||||
},
|
||||
'account.settingsfield.dob.error.general': {
|
||||
id: 'account.settingsfield.dob.error.general',
|
||||
defaultMessage: 'A technical error occurred. Please try again.',
|
||||
description: 'Generic error message.',
|
||||
},
|
||||
'account.settings.field.country': {
|
||||
id: 'account.settings.field.country',
|
||||
defaultMessage: 'Country',
|
||||
@@ -279,18 +449,18 @@ const messages = defineMessages({
|
||||
},
|
||||
'account.settings.field.language.proficiencies': {
|
||||
id: 'account.settings.field.language.proficiencies',
|
||||
defaultMessage: 'Spoken languages',
|
||||
description: 'Label for account settings spoken languages field.',
|
||||
defaultMessage: 'Spoken language',
|
||||
description: 'Label for account settings spoken language field.',
|
||||
},
|
||||
'account.settings.field.language.proficiencies.empty': {
|
||||
id: 'account.settings.field.language.proficiencies.empty',
|
||||
defaultMessage: 'Add a spoken language',
|
||||
description: 'Placeholder for empty account settings spoken languages field.',
|
||||
description: 'Placeholder for empty account settings spoken language field.',
|
||||
},
|
||||
'account.settings.field.language_proficiencies.options.empty': {
|
||||
id: 'account.settings.field.language_proficiencies.options.empty',
|
||||
defaultMessage: 'Select a Language',
|
||||
description: 'Option for an empty value on account settings spoken languages field.',
|
||||
description: 'Option for an empty value on account settings spoken language field.',
|
||||
},
|
||||
|
||||
'account.settings.field.time.zone': {
|
||||
@@ -331,7 +501,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'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 edX profile.',
|
||||
defaultMessage: 'Optionally, link your personal accounts to the social media icons on your {siteName} profile.',
|
||||
description: 'Section subheader for social media links settings',
|
||||
},
|
||||
'account.settings.field.social.platform.name.linkedin': {
|
||||
|
||||
@@ -2,20 +2,16 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
function Alert(props) {
|
||||
return (
|
||||
<div className={classNames('alert d-flex align-items-start', props.className)}>
|
||||
<div>
|
||||
{props.icon}
|
||||
</div>
|
||||
<div>
|
||||
{props.children}
|
||||
</div>
|
||||
const Alert = (props) => (
|
||||
<div className={classNames('alert d-flex align-items-start', props.className)}>
|
||||
<div>
|
||||
{props.icon}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Alert.propTypes = {
|
||||
className: PropTypes.string,
|
||||
@@ -29,5 +25,4 @@ Alert.defaultProps = {
|
||||
children: undefined,
|
||||
};
|
||||
|
||||
|
||||
export default Alert;
|
||||
|
||||
@@ -65,7 +65,7 @@ class BetaLanguageBanner extends React.Component {
|
||||
})}
|
||||
</p>
|
||||
<div>
|
||||
<Button onClick={this.handleRevertLanguage} className="btn btn-primary mr-2">
|
||||
<Button onClick={this.handleRevertLanguage} className="mr-2">
|
||||
{this.props.intl.formatMessage(
|
||||
messages['account.settings.banner.beta.language.action.switch.back'],
|
||||
{ previous_language: previousLanguage.name },
|
||||
|
||||
162
src/account-settings/DOBForm.jsx
Normal file
162
src/account-settings/DOBForm.jsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, StatefulButton, ModalDialog, ActionRow, useToggle, Button,
|
||||
} from '@edx/paragon';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
import { YEAR_OF_BIRTH_OPTIONS } from './data/constants';
|
||||
import { editableFieldSelector } from './data/selectors';
|
||||
import { saveSettingsReset } from './data/actions';
|
||||
|
||||
const DOBModal = (props) => {
|
||||
const {
|
||||
saveState,
|
||||
error,
|
||||
onSubmit,
|
||||
intl,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [isOpen, open, close, toggle] = useToggle(true, {});
|
||||
const [monthValue, setMonthValue] = useState('');
|
||||
const [yearValue, setYearValue] = useState('');
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.target.name === 'month') {
|
||||
setMonthValue(e.target.value);
|
||||
} else if (e.target.name === 'year') {
|
||||
setYearValue(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const data = monthValue !== '' && yearValue !== '' ? [{ field_name: 'DOB', field_value: `${yearValue}-${monthValue}` }] : [];
|
||||
onSubmit('extended_profile', data);
|
||||
};
|
||||
|
||||
const handleComplete = useCallback(() => {
|
||||
localStorage.setItem('submittedDOB', 'true');
|
||||
close();
|
||||
dispatch(saveSettingsReset());
|
||||
}, [dispatch, close]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
close();
|
||||
dispatch(saveSettingsReset());
|
||||
}, [dispatch, close]);
|
||||
|
||||
function renderErrors() {
|
||||
if (saveState === 'error' || error) {
|
||||
return (
|
||||
<Form.Control.Feedback type="invalid" key="general-error">
|
||||
{intl.formatMessage(messages['account.settingsfield.dob.error.general'])}
|
||||
</Form.Control.Feedback>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (saveState === 'complete' && isOpen) {
|
||||
handleComplete();
|
||||
}
|
||||
}, [handleComplete, saveState, isOpen, monthValue, yearValue]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="primary" onClick={open}>
|
||||
{intl.formatMessage(messages['account.settings.field.dob.form.button'])}
|
||||
</Button>
|
||||
<ModalDialog
|
||||
title={intl.formatMessage(messages['account.settings.field.dob.form.title'])}
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
hasCloseButton={false}
|
||||
variant="default"
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
{intl.formatMessage(messages['account.settings.field.dob.form.title'])}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
|
||||
<ModalDialog.Body className="overflow-hidden" style={{ padding: '1.5rem' }}>
|
||||
<p>{intl.formatMessage(messages['account.settings.field.dob.form.help.text'])}</p>
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
{intl.formatMessage(messages['account.settings.field.dob.month'])}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
name="month"
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="">{intl.formatMessage(messages['account.settings.field.dob.month.default'])}</option>
|
||||
{[...Array(12).keys()].map(month => (
|
||||
<option key={month + 1} value={month + 1}>{month + 1}</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
{intl.formatMessage(messages['account.settings.field.dob.year'])}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
name="year"
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="">{intl.formatMessage(messages['account.settings.field.dob.year.default'])}</option>
|
||||
{YEAR_OF_BIRTH_OPTIONS.map(year => (
|
||||
<option key={year.value} value={year.value}>{year.label}</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
{renderErrors()}
|
||||
</ModalDialog.Body>
|
||||
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
Cancel
|
||||
</ModalDialog.CloseButton>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
state={!(monthValue && yearValue) ? 'unedited' : saveState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
|
||||
}}
|
||||
disabledStates={['unedited']}
|
||||
/>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
|
||||
</form>
|
||||
</ModalDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DOBModal.propTypes = {
|
||||
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
|
||||
error: PropTypes.string,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
DOBModal.defaultProps = {
|
||||
saveState: undefined,
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
export default connect(editableFieldSelector)(injectIntl(DOBModal));
|
||||
@@ -2,7 +2,9 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Input, StatefulButton, ValidationFormGroup } from '@edx/paragon';
|
||||
import {
|
||||
Button, Form, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@@ -14,9 +16,9 @@ import {
|
||||
closeForm,
|
||||
} from './data/actions';
|
||||
import { editableFieldSelector } from './data/selectors';
|
||||
import CertificatePreference from './certificate-preference/CertificatePreference';
|
||||
|
||||
|
||||
function EditableField(props) {
|
||||
const EditableField = (props) => {
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
@@ -24,7 +26,6 @@ function EditableField(props) {
|
||||
type,
|
||||
value,
|
||||
userSuppliedValue,
|
||||
options,
|
||||
saveState,
|
||||
error,
|
||||
confirmationMessageDefinition,
|
||||
@@ -36,6 +37,7 @@ function EditableField(props) {
|
||||
onChange,
|
||||
isEditing,
|
||||
isEditable,
|
||||
isGrayedOut,
|
||||
intl,
|
||||
...others
|
||||
} = props;
|
||||
@@ -60,33 +62,28 @@ function EditableField(props) {
|
||||
|
||||
const renderEmptyLabel = () => {
|
||||
if (isEditable) {
|
||||
return <Button onClick={handleEdit} className="btn-link p-0">{emptyLabel}</Button>;
|
||||
return <Button variant="link" onClick={handleEdit} className="p-0">{emptyLabel}</Button>;
|
||||
}
|
||||
return <span className="text-muted">{emptyLabel}</span>;
|
||||
};
|
||||
|
||||
const renderValue = (rawValue) => {
|
||||
if (!rawValue) return renderEmptyLabel();
|
||||
let value = rawValue;
|
||||
|
||||
if (options) {
|
||||
// Use == instead of === to prevent issues when HTML casts numbers as strings
|
||||
// eslint-disable-next-line eqeqeq
|
||||
const selectedOption = options.find(option => option.value == rawValue);
|
||||
if (selectedOption) {
|
||||
value = selectedOption.label;
|
||||
};
|
||||
if (!rawValue) {
|
||||
return renderEmptyLabel();
|
||||
}
|
||||
let finalValue = rawValue;
|
||||
|
||||
if (userSuppliedValue) {
|
||||
value += `: ${userSuppliedValue}`;
|
||||
finalValue += `: ${userSuppliedValue}`;
|
||||
}
|
||||
|
||||
return value;
|
||||
return finalValue;
|
||||
};
|
||||
|
||||
const renderConfirmationMessage = () => {
|
||||
if (!confirmationMessageDefinition || !confirmationValue) return null;
|
||||
if (!confirmationMessageDefinition || !confirmationValue) {
|
||||
return null;
|
||||
}
|
||||
return intl.formatMessage(confirmationMessageDefinition, {
|
||||
value: confirmationValue,
|
||||
});
|
||||
@@ -97,86 +94,83 @@ function EditableField(props) {
|
||||
expression={isEditing ? 'editing' : 'default'}
|
||||
cases={{
|
||||
editing: (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<ValidationFormGroup
|
||||
for={id}
|
||||
invalid={error != null}
|
||||
invalidMessage={error}
|
||||
helpText={helpText}
|
||||
>
|
||||
<label className="h6 d-block" htmlFor={id}>{label}</label>
|
||||
<Input
|
||||
data-hj-suppress
|
||||
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"
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Form.Group
|
||||
controlId={id}
|
||||
isInvalid={error != null}
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
||||
<Form.Label size="sm" className="h6 d-block" htmlFor={id}>{label}</Form.Label>
|
||||
<Form.Control
|
||||
data-hj-suppress
|
||||
name={name}
|
||||
id={id}
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
{...others}
|
||||
/>
|
||||
{!!helpText && <Form.Text>{helpText}</Form.Text>}
|
||||
{error != null && <Form.Control.Feedback hasIcon={false}>{error}</Form.Control.Feedback>}
|
||||
{others.children}
|
||||
</Form.Group>
|
||||
<p>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
className="mr-2"
|
||||
state={saveState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// Swallow clicks if the state is pending.
|
||||
// We do this instead of disabling the button to prevent
|
||||
// it from losing focus (disabled elements cannot have focus).
|
||||
// Disabling it would causes upstream issues in focus management.
|
||||
// Swallowing the onSubmit event on the form would be better, but
|
||||
// we would have to add that logic for every field given our
|
||||
// current structure of the application.
|
||||
if (saveState === 'pending') { e.preventDefault(); }
|
||||
}}
|
||||
disabledStates={[]}
|
||||
/>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
||||
{['name', 'verified_name'].includes(name) && <CertificatePreference fieldName={name} />}
|
||||
</>
|
||||
),
|
||||
default: (
|
||||
<div className="form-group">
|
||||
<div className="d-flex align-items-start">
|
||||
<h6 aria-level="3">{label}</h6>
|
||||
{isEditable ? (
|
||||
<Button onClick={handleEdit} className="ml-3 btn-link">
|
||||
<Button variant="link" onClick={handleEdit} className="ml-3">
|
||||
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />{intl.formatMessage(messages['account.settings.editable.field.action.edit'])}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<p data-hj-suppress>{renderValue(value)}</p>
|
||||
<p data-hj-suppress className={isGrayedOut ? 'grayed-out' : null}>{renderValue(value)}</p>
|
||||
<p className="small text-muted mt-n2">{renderConfirmationMessage() || helpText}</p>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
EditableField.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]),
|
||||
emptyLabel: PropTypes.node,
|
||||
type: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
userSuppliedValue: PropTypes.string,
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
})),
|
||||
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
|
||||
error: PropTypes.string,
|
||||
confirmationMessageDefinition: PropTypes.shape({
|
||||
@@ -192,12 +186,12 @@ EditableField.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isEditing: PropTypes.bool,
|
||||
isEditable: PropTypes.bool,
|
||||
isGrayedOut: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
EditableField.defaultProps = {
|
||||
value: undefined,
|
||||
options: undefined,
|
||||
saveState: undefined,
|
||||
label: undefined,
|
||||
emptyLabel: undefined,
|
||||
@@ -207,9 +201,10 @@ EditableField.defaultProps = {
|
||||
helpText: undefined,
|
||||
isEditing: false,
|
||||
isEditable: true,
|
||||
isGrayedOut: false,
|
||||
userSuppliedValue: undefined,
|
||||
};
|
||||
|
||||
|
||||
export default connect(editableFieldSelector, {
|
||||
onEdit: openForm,
|
||||
onCancel: closeForm,
|
||||
|
||||
232
src/account-settings/EditableSelectField.jsx
Normal file
232
src/account-settings/EditableSelectField.jsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Form, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import SwitchContent from './SwitchContent';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
|
||||
import {
|
||||
openForm,
|
||||
closeForm,
|
||||
} from './data/actions';
|
||||
import { editableFieldSelector } from './data/selectors';
|
||||
import CertificatePreference from './certificate-preference/CertificatePreference';
|
||||
|
||||
const EditableSelectField = (props) => {
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
emptyLabel,
|
||||
type,
|
||||
value,
|
||||
userSuppliedValue,
|
||||
options,
|
||||
saveState,
|
||||
error,
|
||||
confirmationMessageDefinition,
|
||||
confirmationValue,
|
||||
helpText,
|
||||
onEdit,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
onChange,
|
||||
isEditing,
|
||||
isEditable,
|
||||
isGrayedOut,
|
||||
intl,
|
||||
...others
|
||||
} = props;
|
||||
const id = `field-${name}`;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onSubmit(name, new FormData(e.target).get(name));
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
onChange(name, e.target.value);
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
onEdit(name);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
onCancel(name);
|
||||
};
|
||||
|
||||
const renderEmptyLabel = () => {
|
||||
if (isEditable) {
|
||||
return <Button variant="link" onClick={handleEdit} className="p-0">{emptyLabel}</Button>;
|
||||
}
|
||||
return <span className="text-muted">{emptyLabel}</span>;
|
||||
};
|
||||
|
||||
const renderValue = (rawValue) => {
|
||||
if (!rawValue) {
|
||||
return renderEmptyLabel();
|
||||
}
|
||||
let finalValue = rawValue;
|
||||
|
||||
if (options) {
|
||||
// Use == instead of === to prevent issues when HTML casts numbers as strings
|
||||
// eslint-disable-next-line eqeqeq
|
||||
const selectedOption = options.find(option => option.value == rawValue);
|
||||
if (selectedOption) {
|
||||
finalValue = selectedOption.label;
|
||||
}
|
||||
}
|
||||
|
||||
if (userSuppliedValue) {
|
||||
finalValue += `: ${userSuppliedValue}`;
|
||||
}
|
||||
|
||||
return finalValue;
|
||||
};
|
||||
|
||||
const renderConfirmationMessage = () => {
|
||||
if (!confirmationMessageDefinition || !confirmationValue) {
|
||||
return null;
|
||||
}
|
||||
return intl.formatMessage(confirmationMessageDefinition, {
|
||||
value: confirmationValue,
|
||||
});
|
||||
};
|
||||
const selectOptions = options.map(option => (
|
||||
<option value={option.value} key={`${option.value}-${option.label}`}>{option.label}</option>
|
||||
));
|
||||
|
||||
return (
|
||||
<SwitchContent
|
||||
expression={isEditing ? 'editing' : 'default'}
|
||||
cases={{
|
||||
editing: (
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Form.Group
|
||||
controlId={id}
|
||||
isInvalid={error != null}
|
||||
>
|
||||
<Form.Label size="sm" className="h6 d-block" htmlFor={id}>{label}</Form.Label>
|
||||
<Form.Control
|
||||
data-hj-suppress
|
||||
name={name}
|
||||
id={id}
|
||||
type={type}
|
||||
as={type}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
{...others}
|
||||
>
|
||||
{options.length > 0 && selectOptions}
|
||||
</Form.Control>
|
||||
{!!helpText && <Form.Text>{helpText}</Form.Text>}
|
||||
{error != null && <Form.Control.Feedback>{error}</Form.Control.Feedback>}
|
||||
{others.children}
|
||||
</Form.Group>
|
||||
<p>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
className="mr-2"
|
||||
state={saveState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// Swallow clicks if the state is pending.
|
||||
// We do this instead of disabling the button to prevent
|
||||
// it from losing focus (disabled elements cannot have focus).
|
||||
// Disabling it would causes upstream issues in focus management.
|
||||
// Swallowing the onSubmit event on the form would be better, but
|
||||
// we would have to add that logic for every field given our
|
||||
// current structure of the application.
|
||||
if (saveState === 'pending') { e.preventDefault(); }
|
||||
}}
|
||||
disabledStates={[]}
|
||||
/>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
||||
{['name', 'verified_name'].includes(name) && <CertificatePreference fieldName={name} />}
|
||||
</>
|
||||
),
|
||||
default: (
|
||||
<div className="form-group">
|
||||
<div className="d-flex align-items-start">
|
||||
<h6 aria-level="3">{label}</h6>
|
||||
{isEditable ? (
|
||||
<Button variant="link" onClick={handleEdit} className="ml-3">
|
||||
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />{intl.formatMessage(messages['account.settings.editable.field.action.edit'])}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<p data-hj-suppress className={isGrayedOut ? 'grayed-out' : null}>{renderValue(value)}</p>
|
||||
<p className="small text-muted mt-n2">{renderConfirmationMessage() || helpText}</p>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
EditableSelectField.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]),
|
||||
emptyLabel: PropTypes.node,
|
||||
type: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
userSuppliedValue: PropTypes.string,
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
})),
|
||||
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
|
||||
error: PropTypes.string,
|
||||
confirmationMessageDefinition: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
}),
|
||||
confirmationValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
helpText: PropTypes.node,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isEditing: PropTypes.bool,
|
||||
isEditable: PropTypes.bool,
|
||||
isGrayedOut: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
EditableSelectField.defaultProps = {
|
||||
value: undefined,
|
||||
options: [],
|
||||
saveState: undefined,
|
||||
label: undefined,
|
||||
emptyLabel: undefined,
|
||||
error: undefined,
|
||||
confirmationMessageDefinition: undefined,
|
||||
confirmationValue: undefined,
|
||||
helpText: undefined,
|
||||
isEditing: false,
|
||||
isEditable: true,
|
||||
isGrayedOut: false,
|
||||
userSuppliedValue: undefined,
|
||||
};
|
||||
|
||||
export default connect(editableFieldSelector, {
|
||||
onEdit: openForm,
|
||||
onCancel: closeForm,
|
||||
})(injectIntl(EditableSelectField));
|
||||
@@ -2,7 +2,9 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Button, StatefulButton, Input, ValidationFormGroup } from '@edx/paragon';
|
||||
import {
|
||||
Button, StatefulButton, Form,
|
||||
} from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExclamationTriangle, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@@ -16,8 +18,7 @@ import {
|
||||
} from './data/actions';
|
||||
import { editableFieldSelector } from './data/selectors';
|
||||
|
||||
|
||||
function EmailField(props) {
|
||||
const EmailField = (props) => {
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
@@ -56,7 +57,9 @@ function EmailField(props) {
|
||||
};
|
||||
|
||||
const renderConfirmationMessage = () => {
|
||||
if (!confirmationMessageDefinition || !confirmationValue) return null;
|
||||
if (!confirmationMessageDefinition || !confirmationValue) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Alert
|
||||
className="alert-warning mt-n2"
|
||||
@@ -85,13 +88,15 @@ function EmailField(props) {
|
||||
|
||||
const renderEmptyLabel = () => {
|
||||
if (isEditable) {
|
||||
return <Button onClick={handleEdit} className="btn-link p-0">{emptyLabel}</Button>;
|
||||
return <Button variant="link" onClick={handleEdit} className="p-0">{emptyLabel}</Button>;
|
||||
}
|
||||
return <span className="text-muted">{emptyLabel}</span>;
|
||||
};
|
||||
|
||||
const renderValue = () => {
|
||||
if (confirmationValue) return renderConfirmationValue();
|
||||
if (confirmationValue) {
|
||||
return renderConfirmationValue();
|
||||
}
|
||||
return value || renderEmptyLabel();
|
||||
};
|
||||
|
||||
@@ -101,14 +106,12 @@ function EmailField(props) {
|
||||
cases={{
|
||||
editing: (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<ValidationFormGroup
|
||||
for={id}
|
||||
invalid={error != null}
|
||||
invalidMessage={error}
|
||||
helpText={helpText}
|
||||
<Form.Group
|
||||
controlId={id}
|
||||
isInvalid={error != null}
|
||||
>
|
||||
<label className="h6 d-block" htmlFor={id}>{label}</label>
|
||||
<Input
|
||||
<Form.Label className="h6 d-block" htmlFor={id}>{label}</Form.Label>
|
||||
<Form.Control
|
||||
data-hj-suppress
|
||||
name={name}
|
||||
id={id}
|
||||
@@ -116,11 +119,13 @@ function EmailField(props) {
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
{!!helpText && <Form.Text>{helpText}</Form.Text>}
|
||||
{error != null && <Form.Control.Feedback hasIcon={false}>{error}</Form.Control.Feedback>}
|
||||
</Form.Group>
|
||||
<p>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
className="btn-primary mr-2"
|
||||
className="mr-2"
|
||||
state={saveState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
|
||||
@@ -133,13 +138,13 @@ function EmailField(props) {
|
||||
// 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();
|
||||
if (saveState === 'pending') { e.preventDefault(); }
|
||||
}}
|
||||
disabledStates={[]}
|
||||
/>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={handleCancel}
|
||||
className="btn-outline-primary"
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
|
||||
</Button>
|
||||
@@ -151,7 +156,7 @@ function EmailField(props) {
|
||||
<div className="d-flex align-items-start">
|
||||
<h6 aria-level="3">{label}</h6>
|
||||
{isEditable ? (
|
||||
<Button onClick={handleEdit} className="ml-3 btn-link">
|
||||
<Button variant="link" onClick={handleEdit} className="ml-3">
|
||||
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />
|
||||
{intl.formatMessage(messages['account.settings.editable.field.action.edit'])}
|
||||
</Button>
|
||||
@@ -164,8 +169,7 @@ function EmailField(props) {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
EmailField.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
@@ -203,7 +207,6 @@ EmailField.defaultProps = {
|
||||
isEditable: true,
|
||||
};
|
||||
|
||||
|
||||
export default connect(editableFieldSelector, {
|
||||
onEdit: openForm,
|
||||
onCancel: closeForm,
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { breakpoints, useWindowSize } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { NavHashLink } from 'react-router-hash-link';
|
||||
import Scrollspy from 'react-scrollspy';
|
||||
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
function JumpNav({ intl, displayDemographicsLink }) {
|
||||
const JumpNav = ({
|
||||
intl,
|
||||
displayDemographicsLink,
|
||||
}) => {
|
||||
const stickToTop = useWindowSize().width > breakpoints.small.minWidth;
|
||||
return (
|
||||
<div className="jump-nav">
|
||||
<div className={classNames('jump-nav', { 'jump-nav-sm position-sticky pt-3': stickToTop })}>
|
||||
<Scrollspy
|
||||
items={[
|
||||
'basic-information',
|
||||
@@ -34,13 +38,14 @@ function JumpNav({ intl, displayDemographicsLink }) {
|
||||
{intl.formatMessage(messages['account.settings.section.profile.information'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && displayDemographicsLink &&
|
||||
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && displayDemographicsLink
|
||||
&& (
|
||||
<li>
|
||||
<NavHashLink to="#demographics-information">
|
||||
{intl.formatMessage(messages['account.settings.section.demographics.information'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
}
|
||||
)}
|
||||
<li>
|
||||
<NavHashLink to="#social-media">
|
||||
{intl.formatMessage(messages['account.settings.section.social.media'])}
|
||||
@@ -64,16 +69,11 @@ function JumpNav({ intl, displayDemographicsLink }) {
|
||||
</Scrollspy>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
JumpNav.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
displayDemographicsLink: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
JumpNav.defaultProps = {
|
||||
displayDemographicsLink: false,
|
||||
}
|
||||
|
||||
export default injectIntl(JumpNav);
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||
<p className="my-0 py-5 text-muted" style={{ maxWidth: '32em' }}>
|
||||
<FormattedMessage
|
||||
id="error.notfound.message"
|
||||
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
||||
description="error message when a page does not exist"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const NotFoundPage = () => (
|
||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||
<p className="my-0 py-5 text-muted" style={{ maxWidth: '32em' }}>
|
||||
<FormattedMessage
|
||||
id="error.notfound.message"
|
||||
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
||||
description="Error message when a page does not exist"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NotFoundPage;
|
||||
|
||||
45
src/account-settings/OneTimeDismissibleAlert.jsx
Normal file
45
src/account-settings/OneTimeDismissibleAlert.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
const 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,
|
||||
};
|
||||
|
||||
export default OneTimeDismissibleAlert;
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TransitionReplace } from '@edx/paragon';
|
||||
|
||||
|
||||
const onChildExit = (htmlNode) => {
|
||||
// If the leaving child has focus, take control and redirect it
|
||||
if (htmlNode.contains(document.activeElement)) {
|
||||
@@ -11,7 +10,9 @@ const onChildExit = (htmlNode) => {
|
||||
const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling;
|
||||
|
||||
// There's no replacement, do nothing.
|
||||
if (!enteringChild) return;
|
||||
if (!enteringChild) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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"])');
|
||||
@@ -21,15 +22,15 @@ const onChildExit = (htmlNode) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function SwitchContent({ expression, cases, className }) {
|
||||
const SwitchContent = ({ expression, cases, className }) => {
|
||||
const getContent = (caseKey) => {
|
||||
if (cases[caseKey]) {
|
||||
if (typeof cases[caseKey] === 'string') {
|
||||
return getContent(cases[caseKey]);
|
||||
}
|
||||
return React.cloneElement(cases[caseKey], { key: caseKey });
|
||||
} else if (cases.default) {
|
||||
}
|
||||
if (cases.default) {
|
||||
if (typeof cases.default === 'string') {
|
||||
return getContent(cases.default);
|
||||
}
|
||||
@@ -47,8 +48,7 @@ function SwitchContent({ expression, cases, className }) {
|
||||
{getContent(expression)}
|
||||
</TransitionReplace>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
SwitchContent.propTypes = {
|
||||
expression: PropTypes.string,
|
||||
@@ -61,5 +61,4 @@ SwitchContent.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
|
||||
|
||||
export default SwitchContent;
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h6, .h6 {
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
line-height: 1.2;
|
||||
border: none;
|
||||
@@ -12,36 +14,33 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.jump-nav-sm {
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.jump-nav {
|
||||
@media (min-width: map-get($grid-breakpoints, "sm")) {
|
||||
padding-top: 1rem;
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: .5rem;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
@extend .h4;
|
||||
margin-bottom: map-get($spacers, 3);
|
||||
}
|
||||
.account-section {
|
||||
// These properties together will shift the hashlink position
|
||||
margin-bottom: map-get($spacers, 5);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.custom-switch {
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
|
||||
.custom-control-label {
|
||||
left: 2.25rem;
|
||||
line-height: 1.6rem;
|
||||
}
|
||||
}
|
||||
.grayed-out{
|
||||
opacity: 0.6; /* Real browsers */
|
||||
filter: alpha(opacity = 60); /* MSIE */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
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';
|
||||
|
||||
const CertificatePreference = ({
|
||||
intl,
|
||||
fieldName,
|
||||
originalFullName,
|
||||
originalVerifiedName,
|
||||
saveState,
|
||||
useVerifiedNameForCerts,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
const formId = 'useVerifiedNameForCerts';
|
||||
|
||||
const handleCheckboxChange = () => {
|
||||
if (!checked) {
|
||||
if (fieldName === 'verified_name') {
|
||||
dispatch(updateDraft(formId, true));
|
||||
} else {
|
||||
dispatch(updateDraft(formId, false));
|
||||
}
|
||||
} else {
|
||||
setModalIsOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setModalIsOpen(false);
|
||||
dispatch(resetDrafts());
|
||||
};
|
||||
|
||||
const handleModalChange = (e) => {
|
||||
if (e.target.value === 'fullName') {
|
||||
dispatch(updateDraft(formId, false));
|
||||
} else {
|
||||
dispatch(updateDraft(formId, true));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (saveState === 'pending') {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(saveSettings(formId, useVerifiedNameForCerts));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (originalVerifiedName) {
|
||||
if (fieldName === 'verified_name') {
|
||||
setChecked(useVerifiedNameForCerts);
|
||||
} else {
|
||||
setChecked(!useVerifiedNameForCerts);
|
||||
}
|
||||
}
|
||||
}, [originalVerifiedName, fieldName, useVerifiedNameForCerts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (originalVerifiedName) {
|
||||
if (modalIsOpen && saveState === 'complete') {
|
||||
setModalIsOpen(false);
|
||||
dispatch(closeForm(fieldName));
|
||||
}
|
||||
}
|
||||
}, [dispatch, originalVerifiedName, fieldName, modalIsOpen, saveState]);
|
||||
|
||||
// If the user doesn't have an approved verified name, do not display this component
|
||||
|
||||
return originalVerifiedName ? (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
CertificatePreference.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
fieldName: PropTypes.string.isRequired,
|
||||
originalFullName: PropTypes.string,
|
||||
originalVerifiedName: PropTypes.string,
|
||||
saveState: PropTypes.string,
|
||||
useVerifiedNameForCerts: PropTypes.bool,
|
||||
};
|
||||
|
||||
CertificatePreference.defaultProps = {
|
||||
originalFullName: '',
|
||||
originalVerifiedName: '',
|
||||
saveState: null,
|
||||
useVerifiedNameForCerts: false,
|
||||
};
|
||||
|
||||
export default connect(certPreferenceSelector)(injectIntl(CertificatePreference));
|
||||
22
src/account-settings/certificate-preference/data/service.js
Normal file
22
src/account-settings/certificate-preference/data/service.js
Normal file
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
}
|
||||
36
src/account-settings/certificate-preference/messages.js
Normal file
36
src/account-settings/certificate-preference/messages.js
Normal file
@@ -0,0 +1,36 @@
|
||||
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 user‘s 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 user’s 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 user’s 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 user’s name choice.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,174 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
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,
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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],
|
||||
}
|
||||
`;
|
||||
@@ -17,13 +17,9 @@ import LogoSVG from '../../logo.svg';
|
||||
import { fetchSettings } from '../data/actions';
|
||||
import { coachingConsentPageSelector } from '../data/selectors';
|
||||
|
||||
const Logo = ({ src, alt, ...attributes }) => (
|
||||
<>
|
||||
<img src={src} alt={alt} {...attributes} />
|
||||
</>
|
||||
);
|
||||
const Logo = ({ src, alt, ...attributes }) => <img src={src} alt={alt} {...attributes} />;
|
||||
|
||||
const SuccessMessage = props => (
|
||||
const SuccessMessage = (props) => (
|
||||
<div className="col-12 col-lg-6 shadow-lg mx-auto mt-4 p-5">
|
||||
<FontAwesomeIcon className="text-success" icon={faCheck} size="5x" />
|
||||
<div className="h3">{props.header}</div>
|
||||
@@ -36,6 +32,7 @@ const SuccessMessage = props => (
|
||||
|
||||
const AutoRedirect = (props) => {
|
||||
window.location.href = props.redirectUrl;
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@@ -71,6 +68,23 @@ class CoachingConsent extends React.Component {
|
||||
this.props.fetchSettings();
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const fullName = e.target.fullName.value;
|
||||
const phoneNumber = e.target.phoneNumber.value;
|
||||
const body = {
|
||||
coaching_consent: true,
|
||||
consent_form_seen: true,
|
||||
phone_number: phoneNumber,
|
||||
full_name: fullName,
|
||||
};
|
||||
this.setState({
|
||||
formErrors: {},
|
||||
formSubmitted: true,
|
||||
declineSubmitted: false,
|
||||
}, () => this.patchUsingCoachingConsentForm(body));
|
||||
}
|
||||
|
||||
sanitizeForwardingUrl(url) {
|
||||
// Redirect to root of MFE if invalid next param is sent
|
||||
return url && url.startsWith(getConfig().LMS_BASE_URL) ? url : `${getConfig().LMS_BASE_URL}/dashboard/`;
|
||||
@@ -97,24 +111,6 @@ class CoachingConsent extends React.Component {
|
||||
if (get(data, 'status') === 200) {
|
||||
this.setState({ submissionSuccess: true });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const fullName = e.target.fullName.value;
|
||||
const phoneNumber = e.target.phoneNumber.value;
|
||||
const body = {
|
||||
coaching_consent: true,
|
||||
consent_form_seen: true,
|
||||
phone_number: phoneNumber,
|
||||
full_name: fullName,
|
||||
};
|
||||
this.setState({
|
||||
formErrors: {},
|
||||
formSubmitted: true,
|
||||
declineSubmitted: false,
|
||||
}, () => this.patchUsingCoachingConsentForm(body));
|
||||
}
|
||||
|
||||
declineCoaching(e) {
|
||||
@@ -135,28 +131,33 @@ class CoachingConsent extends React.Component {
|
||||
case VIEWS.NOT_LOADED:
|
||||
return <PageLoading srMessage="" />;
|
||||
case VIEWS.LOADED:
|
||||
return (<CoachingConsentForm
|
||||
onSubmit={this.handleSubmit}
|
||||
declineCoaching={this.declineCoaching}
|
||||
formErrors={this.state.formErrors}
|
||||
formValues={this.props.formValues}
|
||||
redirectUrl={this.state.redirectUrl}
|
||||
profileDataManager={this.props.profileDataManager}
|
||||
/>);
|
||||
return (
|
||||
<CoachingConsentForm
|
||||
onSubmit={this.handleSubmit}
|
||||
declineCoaching={this.declineCoaching}
|
||||
formErrors={this.state.formErrors}
|
||||
formValues={this.props.formValues}
|
||||
redirectUrl={this.state.redirectUrl}
|
||||
profileDataManager={this.props.profileDataManager}
|
||||
/>
|
||||
);
|
||||
case VIEWS.SUCCESS_PENDING:
|
||||
return <PageLoading srMessage="Submitting..." />;
|
||||
case VIEWS.SUCCESS:
|
||||
return (<SuccessMessage
|
||||
continueUrl={this.state.redirectUrl}
|
||||
header={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.header'])}
|
||||
message={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.message'])}
|
||||
continue={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.continue'])}
|
||||
/>);
|
||||
return (
|
||||
<SuccessMessage
|
||||
continueUrl={this.state.redirectUrl}
|
||||
header={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.header'])}
|
||||
message={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.message'])}
|
||||
continue={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.continue'])}
|
||||
/>
|
||||
);
|
||||
case VIEWS.DECLINE_PENDING:
|
||||
return <PageLoading srMessage="Redirecting..." />;
|
||||
case VIEWS.DECLINED:
|
||||
return <AutoRedirect redirectUrl={this.state.redirectUrl} />;
|
||||
default:
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
@@ -233,7 +234,6 @@ AutoRedirect.propTypes = {
|
||||
|
||||
CoachingConsent.defaultProps = {
|
||||
loaded: false,
|
||||
saveState: undefined,
|
||||
profileDataManager: null,
|
||||
};
|
||||
|
||||
@@ -251,15 +251,14 @@ CoachingConsent.propTypes = {
|
||||
}),
|
||||
}).isRequired,
|
||||
formErrors: PropTypes.shape({
|
||||
coaching: PropTypes.object,
|
||||
coaching: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
confirmationValues: PropTypes.shape({
|
||||
coaching: PropTypes.object,
|
||||
name: PropTypes.object,
|
||||
phone_number: PropTypes.object,
|
||||
coaching: PropTypes.shape({}),
|
||||
name: PropTypes.shape({}),
|
||||
phone_number: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
fetchSettings: PropTypes.func.isRequired,
|
||||
saveState: PropTypes.string,
|
||||
profileDataManager: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Input, Button, Hyperlink } from '@edx/paragon';
|
||||
import { Form, Button, Hyperlink } from '@edx/paragon';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Alert from '../Alert';
|
||||
import messages from './CoachingConsent.messages';
|
||||
|
||||
const ErrorMessage = props => (
|
||||
<div className="alert-warning mb-2">{props.message}</div>
|
||||
);
|
||||
const ErrorMessage = (props) => <div className="alert-warning mb-2">{props.message}</div>;
|
||||
|
||||
const ManagedProfileAlert = ({ profileDataManager }) => (
|
||||
<Alert className="alert alert-primary" role="alert">
|
||||
<FormattedMessage
|
||||
id="account.settings.coaching.managed.alert"
|
||||
defaultMessage="Your name is managed by {managerTitle}. Contact your administrator for help."
|
||||
description="alert message informing the user their account data is managed by a third party"
|
||||
description="Alert message informing the user their account data is managed by a third party"
|
||||
values={{
|
||||
managerTitle: <b>{profileDataManager}</b>,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
const CoachingForm = props => (
|
||||
const CoachingForm = (props) => (
|
||||
<div className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg">
|
||||
<h2 className="h2">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.welcome.header'])}
|
||||
@@ -33,13 +31,14 @@ const CoachingForm = props => (
|
||||
<div>
|
||||
<form onSubmit={props.onSubmit}>
|
||||
<div className="py-3">
|
||||
{
|
||||
!!props.profileDataManager &&
|
||||
<ManagedProfileAlert profileDataManager={props.profileDataManager} />
|
||||
}
|
||||
{!!props.profileDataManager && (
|
||||
<ManagedProfileAlert profileDataManager={props.profileDataManager} />
|
||||
)}
|
||||
<ErrorMessage message={props.formErrors.full_name} />
|
||||
<label className="h6" htmlFor="fullName">{props.intl.formatMessage(messages['account.settings.coaching.consent.label.name'])}</label>
|
||||
<Input
|
||||
<label className="h6" htmlFor="fullName">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.name'])}
|
||||
</label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="full-name"
|
||||
id="fullName"
|
||||
@@ -49,8 +48,10 @@ const CoachingForm = props => (
|
||||
</div>
|
||||
<div className="py-3">
|
||||
<ErrorMessage message={props.formErrors.phone_number} />
|
||||
<label className="h6" htmlFor="phoneNumber">{props.intl.formatMessage(messages['account.settings.coaching.consent.label.phone-number'])}</label>
|
||||
<Input
|
||||
<label className="h6" htmlFor="phoneNumber">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.phone-number'])}
|
||||
</label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="phone_number"
|
||||
id="phoneNumber"
|
||||
@@ -64,7 +65,7 @@ const CoachingForm = props => (
|
||||
</div>
|
||||
<ErrorMessage message={props.formErrors.coaching} />
|
||||
<div className="d-flex flex-column align-items-center">
|
||||
<Button className="w-100 btn-outline-primary" type="submit">
|
||||
<Button variant="outline-primary" className="w-100" type="submit">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.accept-coaching'])}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -123,7 +124,6 @@ ErrorMessage.propTypes = {
|
||||
|
||||
ManagedProfileAlert.propTypes = {
|
||||
profileDataManager: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CoachingForm);
|
||||
|
||||
@@ -2,14 +2,13 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { ValidationFormGroup, Input } from '@edx/paragon';
|
||||
import { Form } from '@edx/paragon';
|
||||
import messages from './CoachingToggle.messages';
|
||||
import { editableFieldSelector } from '../data/selectors';
|
||||
import { saveSettings, updateDraft, saveMultipleSettings } from '../data/actions';
|
||||
import EditableField from '../EditableField';
|
||||
|
||||
|
||||
const CoachingToggle = props => (
|
||||
const CoachingToggle = (props) => (
|
||||
<>
|
||||
<EditableField
|
||||
name="phone_number"
|
||||
@@ -33,25 +32,23 @@ const CoachingToggle = props => (
|
||||
formId: 'phone_number',
|
||||
commitValues: props.phone_number,
|
||||
},
|
||||
], 'phone_number');
|
||||
}
|
||||
], 'phone_number');
|
||||
}
|
||||
return props.saveSettings('phone_number', props.phone_number);
|
||||
}}
|
||||
}}
|
||||
/>
|
||||
<ValidationFormGroup
|
||||
for="coachingConsent"
|
||||
helpText={props.intl.formatMessage(messages['account.settings.field.coaching_consent.tooltip'])}
|
||||
invalid={!!props.error}
|
||||
invalidMessage={props.intl.formatMessage(messages['account.settings.field.coaching_consent.error'])}
|
||||
<Form.Group
|
||||
isInvalid={!!props.error}
|
||||
className="custom-control custom-switch"
|
||||
>
|
||||
<Input
|
||||
<Form.Switch
|
||||
name={props.name}
|
||||
className="custom-control-input"
|
||||
disabled={props.saveState === 'pending'}
|
||||
type="checkbox"
|
||||
id="coachingConsent"
|
||||
checked={props.coaching.coaching_consent}
|
||||
helperText={props.intl.formatMessage(messages['account.settings.field.coaching_consent.tooltip'])}
|
||||
value={props.coaching.coaching_consent}
|
||||
onChange={async (e) => {
|
||||
const { name } = e.target;
|
||||
@@ -59,14 +56,21 @@ const CoachingToggle = props => (
|
||||
const { user, eligible_for_coaching } = props.coaching;
|
||||
const value = {
|
||||
user,
|
||||
// eslint-disable-next-line camelcase
|
||||
eligible_for_coaching,
|
||||
coaching_consent: e.target.checked,
|
||||
};
|
||||
props.saveSettings(name, value);
|
||||
}}
|
||||
/>
|
||||
<label className="custom-control-label" htmlFor="coachingConsent">{props.intl.formatMessage(messages['account.settings.field.coaching_consent'])}</label>
|
||||
</ValidationFormGroup>
|
||||
>
|
||||
{props.intl.formatMessage(messages['account.settings.field.coaching_consent'])}
|
||||
</Form.Switch>
|
||||
{!!props.error && (
|
||||
<Form.Control.Feedback>
|
||||
{props.intl.formatMessage(messages['account.settings.field.coaching_consent.error'])}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import renderer from 'react-test-renderer';
|
||||
@@ -13,9 +14,7 @@ jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
const IntlCoachingConsent = injectIntl(CoachingConsent);
|
||||
|
||||
jest.mock('../../data/selectors', () => {
|
||||
return jest.fn().mockImplementation(() => ({ coachingConsentPageSelector: () => ({}) }));
|
||||
});
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ coachingConsentPageSelector: () => ({}) })));
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
|
||||
@@ -34,13 +34,11 @@ exports[`CoachingConsent disables name field on enterprise user 1`] = `
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<span>
|
||||
Your name is managed by
|
||||
<b>
|
||||
test person
|
||||
</b>
|
||||
. Contact your administrator for help.
|
||||
</span>
|
||||
Your name is managed by
|
||||
<b>
|
||||
test person
|
||||
</b>
|
||||
. Contact your administrator for help.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -54,14 +52,20 @@ exports[`CoachingConsent disables name field on enterprise user 1`] = `
|
||||
>
|
||||
Please confirm your name
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
defaultValue="edx edx"
|
||||
disabled={true}
|
||||
id="fullName"
|
||||
name="full-name"
|
||||
type="text"
|
||||
/>
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
<input
|
||||
className="has-value form-control"
|
||||
defaultValue="edx edx"
|
||||
disabled={true}
|
||||
id="fullName"
|
||||
name="full-name"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="py-3"
|
||||
@@ -77,13 +81,19 @@ exports[`CoachingConsent disables name field on enterprise user 1`] = `
|
||||
>
|
||||
Enter your mobile number
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
defaultValue="1234567890"
|
||||
id="phoneNumber"
|
||||
name="phone_number"
|
||||
type="text"
|
||||
/>
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
<input
|
||||
className="has-value form-control"
|
||||
defaultValue="1234567890"
|
||||
id="phoneNumber"
|
||||
name="phone_number"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className=" py-3"
|
||||
@@ -103,10 +113,8 @@ exports[`CoachingConsent disables name field on enterprise user 1`] = `
|
||||
className="d-flex flex-column align-items-center"
|
||||
>
|
||||
<button
|
||||
className="btn w-100 btn-outline-primary"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
className="w-100 btn btn-outline-primary"
|
||||
disabled={false}
|
||||
type="submit"
|
||||
>
|
||||
Sign up for coaching
|
||||
@@ -116,7 +124,7 @@ exports[`CoachingConsent disables name field on enterprise user 1`] = `
|
||||
className="mt-3"
|
||||
>
|
||||
<a
|
||||
className="mt-3 text-dark btn-link small"
|
||||
className="pgn__hyperlink default-link standalone-link mt-3 text-dark btn-link small"
|
||||
href="http://localhost:18000/dashboard/"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
@@ -205,14 +213,20 @@ exports[`CoachingConsent should render 1`] = `
|
||||
>
|
||||
Please confirm your name
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
defaultValue="edx edx"
|
||||
disabled={false}
|
||||
id="fullName"
|
||||
name="full-name"
|
||||
type="text"
|
||||
/>
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
<input
|
||||
className="has-value form-control"
|
||||
defaultValue="edx edx"
|
||||
disabled={false}
|
||||
id="fullName"
|
||||
name="full-name"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="py-3"
|
||||
@@ -228,13 +242,19 @@ exports[`CoachingConsent should render 1`] = `
|
||||
>
|
||||
Enter your mobile number
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
defaultValue="1234567890"
|
||||
id="phoneNumber"
|
||||
name="phone_number"
|
||||
type="text"
|
||||
/>
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
<input
|
||||
className="has-value form-control"
|
||||
defaultValue="1234567890"
|
||||
id="phoneNumber"
|
||||
name="phone_number"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className=" py-3"
|
||||
@@ -254,10 +274,8 @@ exports[`CoachingConsent should render 1`] = `
|
||||
className="d-flex flex-column align-items-center"
|
||||
>
|
||||
<button
|
||||
className="btn w-100 btn-outline-primary"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
className="w-100 btn btn-outline-primary"
|
||||
disabled={false}
|
||||
type="submit"
|
||||
>
|
||||
Sign up for coaching
|
||||
@@ -267,7 +285,7 @@ exports[`CoachingConsent should render 1`] = `
|
||||
className="mt-3"
|
||||
>
|
||||
<a
|
||||
className="mt-3 text-dark btn-link small"
|
||||
className="pgn__hyperlink default-link standalone-link mt-3 text-dark btn-link small"
|
||||
href="http://localhost:18000/dashboard/"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
|
||||
@@ -9,6 +9,7 @@ export const OPEN_FORM = 'OPEN_FORM';
|
||||
export const CLOSE_FORM = 'CLOSE_FORM';
|
||||
export const UPDATE_DRAFT = 'UPDATE_DRAFT';
|
||||
export const RESET_DRAFTS = 'RESET_DRAFTS';
|
||||
export const BEGIN_NAME_CHANGE = 'BEGIN_NAME_CHANGE';
|
||||
|
||||
// FETCH SETTINGS ACTIONS
|
||||
|
||||
@@ -25,6 +26,7 @@ export const fetchSettingsSuccess = ({
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
verifiedNameHistory,
|
||||
}) => ({
|
||||
type: FETCH_SETTINGS.SUCCESS,
|
||||
payload: {
|
||||
@@ -32,6 +34,7 @@ export const fetchSettingsSuccess = ({
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
verifiedNameHistory,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -44,7 +47,6 @@ export const fetchSettingsReset = () => ({
|
||||
type: FETCH_SETTINGS.RESET,
|
||||
});
|
||||
|
||||
|
||||
// FORM STATE ACTIONS
|
||||
|
||||
export const openForm = formId => ({
|
||||
@@ -69,7 +71,10 @@ export const resetDrafts = () => ({
|
||||
type: RESET_DRAFTS,
|
||||
});
|
||||
|
||||
|
||||
export const beginNameChange = (formId) => ({
|
||||
type: BEGIN_NAME_CHANGE,
|
||||
payload: { formId },
|
||||
});
|
||||
// SAVE SETTINGS ACTIONS
|
||||
|
||||
export const saveSettings = (formId, commitValues) => ({
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export const YEAR_OF_BIRTH_OPTIONS = (() => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const years = [];
|
||||
@@ -11,6 +10,11 @@ export const YEAR_OF_BIRTH_OPTIONS = (() => {
|
||||
return years.reverse();
|
||||
})();
|
||||
|
||||
export const COPPA_COMPLIANCE_YEAR = (() => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
return currentYear - 13;
|
||||
})();
|
||||
|
||||
export const EDUCATION_LEVELS = [
|
||||
'',
|
||||
'p',
|
||||
|
||||
@@ -8,11 +8,13 @@ import {
|
||||
UPDATE_DRAFT,
|
||||
RESET_DRAFTS,
|
||||
SAVE_MULTIPLE_SETTINGS,
|
||||
BEGIN_NAME_CHANGE,
|
||||
} from './actions';
|
||||
|
||||
import { reducer as deleteAccountReducer, DELETE_ACCOUNT } from '../delete-account';
|
||||
import { reducer as siteLanguageReducer, FETCH_SITE_LANGUAGES } from '../site-language';
|
||||
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';
|
||||
|
||||
export const defaultState = {
|
||||
@@ -31,10 +33,15 @@ export const defaultState = {
|
||||
deleteAccount: deleteAccountReducer(),
|
||||
siteLanguage: siteLanguageReducer(),
|
||||
resetPassword: resetPasswordReducer(),
|
||||
nameChange: nameChangeReducer(),
|
||||
thirdPartyAuth: thirdPartyAuthReducer(),
|
||||
nameChangeModal: false,
|
||||
verifiedName: null,
|
||||
mostRecentVerifiedName: {},
|
||||
verifiedNameHistory: {},
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
const reducer = (state = defaultState, action = {}) => {
|
||||
let dispatcherIsOpenForm;
|
||||
|
||||
switch (action.type) {
|
||||
@@ -48,16 +55,15 @@ const reducer = (state = defaultState, action) => {
|
||||
case FETCH_SETTINGS.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
values: Object.assign({}, state.values, action.payload.values),
|
||||
values: { ...state.values, ...action.payload.values },
|
||||
// Dump the providers into thirdPartyAuth.
|
||||
thirdPartyAuth: Object.assign({}, state.thirdPartyAuth, {
|
||||
providers: action.payload.thirdPartyAuthProviders,
|
||||
}),
|
||||
thirdPartyAuth: { ...state.thirdPartyAuth, providers: action.payload.thirdPartyAuthProviders },
|
||||
profileDataManager: action.payload.profileDataManager,
|
||||
timeZones: action.payload.timeZones,
|
||||
loading: false,
|
||||
loaded: true,
|
||||
loadingError: null,
|
||||
verifiedNameHistory: action.payload.verifiedNameHistory,
|
||||
};
|
||||
case FETCH_SETTINGS.FAILURE:
|
||||
return {
|
||||
@@ -91,15 +97,14 @@ const reducer = (state = defaultState, action) => {
|
||||
saveState: null,
|
||||
errors: {},
|
||||
drafts: {},
|
||||
nameChangeModal: false,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
case UPDATE_DRAFT:
|
||||
return {
|
||||
...state,
|
||||
drafts: Object.assign({}, state.drafts, {
|
||||
[action.payload.name]: action.payload.value,
|
||||
}),
|
||||
drafts: { ...state.drafts, [action.payload.name]: action.payload.value },
|
||||
saveState: null,
|
||||
errors: {},
|
||||
};
|
||||
@@ -110,6 +115,15 @@ const reducer = (state = defaultState, action) => {
|
||||
drafts: {},
|
||||
};
|
||||
|
||||
case BEGIN_NAME_CHANGE:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'error',
|
||||
nameChangeModal: {
|
||||
formId: action.payload.formId,
|
||||
},
|
||||
};
|
||||
|
||||
case SAVE_SETTINGS.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
@@ -120,19 +134,18 @@ const reducer = (state = defaultState, action) => {
|
||||
return {
|
||||
...state,
|
||||
saveState: 'complete',
|
||||
values: Object.assign({}, state.values, action.payload.values),
|
||||
values: { ...state.values, ...action.payload.values },
|
||||
errors: {},
|
||||
confirmationValues: Object.assign(
|
||||
{},
|
||||
state.confirmationValues,
|
||||
action.payload.confirmationValues,
|
||||
),
|
||||
confirmationValues: {
|
||||
...state.confirmationValues,
|
||||
...action.payload.confirmationValues,
|
||||
},
|
||||
};
|
||||
case SAVE_SETTINGS.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'error',
|
||||
errors: Object.assign({}, state.errors, action.payload.errors),
|
||||
errors: { ...state.errors, ...action.payload.errors },
|
||||
};
|
||||
case SAVE_SETTINGS.RESET:
|
||||
return {
|
||||
@@ -161,7 +174,7 @@ const reducer = (state = defaultState, action) => {
|
||||
return {
|
||||
...state,
|
||||
saveState: 'error',
|
||||
errors: Object.assign({}, state.errors, action.payload.errors),
|
||||
errors: { ...state.errors, ...action.payload.errors },
|
||||
};
|
||||
|
||||
case FETCH_TIME_ZONES.SUCCESS:
|
||||
@@ -202,6 +215,15 @@ const reducer = (state = defaultState, 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.SUCCESS:
|
||||
case DISCONNECT_AUTH.FAILURE:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { call, put, delay, takeEvery, all } from 'redux-saga/effects';
|
||||
import {
|
||||
call, put, delay, takeEvery, all,
|
||||
} from 'redux-saga/effects';
|
||||
|
||||
import { publish } from '@edx/frontend-platform';
|
||||
import { getLocale, handleRtl, LOCALE_CHANGED } from '@edx/frontend-platform/i18n';
|
||||
@@ -23,11 +25,13 @@ import {
|
||||
saveMultipleSettingsBegin,
|
||||
saveMultipleSettingsSuccess,
|
||||
saveMultipleSettingsFailure,
|
||||
beginNameChange,
|
||||
} from './actions';
|
||||
|
||||
// Sub-modules
|
||||
import { saga as deleteAccountSaga } from '../delete-account';
|
||||
import { saga as resetPasswordSaga } from '../reset-password';
|
||||
import { saga as nameChangeSaga } from '../name-change';
|
||||
import {
|
||||
saga as siteLanguageSaga,
|
||||
patchPreferences,
|
||||
@@ -36,7 +40,12 @@ import {
|
||||
import { saga as thirdPartyAuthSaga } from '../third-party-auth';
|
||||
|
||||
// Services
|
||||
import { getSettings, patchSettings, getTimeZones } from './service';
|
||||
import {
|
||||
getSettings,
|
||||
patchSettings,
|
||||
getTimeZones,
|
||||
getVerifiedNameHistory,
|
||||
} from './service';
|
||||
|
||||
export function* handleFetchSettings() {
|
||||
try {
|
||||
@@ -52,13 +61,16 @@ export function* handleFetchSettings() {
|
||||
userId,
|
||||
);
|
||||
|
||||
if (values.country) yield put(fetchTimeZones(values.country));
|
||||
const verifiedNameHistory = yield call(getVerifiedNameHistory);
|
||||
|
||||
if (values.country) { yield put(fetchTimeZones(values.country)); }
|
||||
|
||||
yield put(fetchSettingsSuccess({
|
||||
values,
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
verifiedNameHistory,
|
||||
}));
|
||||
} catch (e) {
|
||||
yield put(fetchSettingsFailure(e.message));
|
||||
@@ -91,11 +103,14 @@ export function* handleSaveSettings(action) {
|
||||
savedValues = yield call(patchSettings, username, commitData, userId);
|
||||
}
|
||||
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 put(closeForm(action.payload.formId));
|
||||
} catch (e) {
|
||||
if (e.fieldErrors) {
|
||||
if (e.fieldErrors.name?.includes('verification')) {
|
||||
yield put(beginNameChange('name'));
|
||||
}
|
||||
yield put(saveSettingsFailure({ fieldErrors: e.fieldErrors }));
|
||||
} else {
|
||||
yield put(saveSettingsFailure(e.message));
|
||||
@@ -104,7 +119,6 @@ export function* handleSaveSettings(action) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// handles mutiple settings saved at once, in order, and stops executing on first failure.
|
||||
export function* handleSaveMultipleSettings(action) {
|
||||
try {
|
||||
@@ -125,6 +139,9 @@ export function* handleSaveMultipleSettings(action) {
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.fieldErrors) {
|
||||
if (e.fieldErrors.name?.includes('verification')) {
|
||||
yield put(beginNameChange('name'));
|
||||
}
|
||||
yield put(saveMultipleSettingsFailure({ fieldErrors: e.fieldErrors }));
|
||||
} else {
|
||||
yield put(saveMultipleSettingsFailure(e.message));
|
||||
@@ -138,7 +155,6 @@ export function* handleFetchTimeZones(action) {
|
||||
yield put(fetchTimeZonesSuccess(response, action.payload.country));
|
||||
}
|
||||
|
||||
|
||||
export default function* saga() {
|
||||
yield takeEvery(FETCH_SETTINGS.BASE, handleFetchSettings);
|
||||
yield takeEvery(SAVE_SETTINGS.BASE, handleSaveSettings);
|
||||
@@ -148,6 +164,7 @@ export default function* saga() {
|
||||
deleteAccountSaga(),
|
||||
siteLanguageSaga(),
|
||||
resetPasswordSaga(),
|
||||
nameChangeSaga(),
|
||||
thirdPartyAuthSaga(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createSelector, createStructuredSelector } from 'reselect';
|
||||
import { siteLanguageListSelector, siteLanguageOptionsSelector } from '../site-language';
|
||||
import { compareVerifiedNamesByCreatedDate } from '../../utils';
|
||||
|
||||
export const storeName = 'accountSettings';
|
||||
|
||||
@@ -7,9 +8,74 @@ export const accountSettingsSelector = state => ({ ...state[storeName] });
|
||||
|
||||
const editableFieldNameSelector = (state, props) => props.name;
|
||||
|
||||
const verifiedNameSettingsSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => ({
|
||||
history: accountSettings.verifiedNameHistory.results,
|
||||
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(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.values,
|
||||
mostRecentApprovedVerifiedNameValueSelector,
|
||||
(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(
|
||||
@@ -50,6 +116,11 @@ const errorSelector = createSelector(
|
||||
accountSettings => accountSettings.errors,
|
||||
);
|
||||
|
||||
const nameChangeModalSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.nameChangeModal,
|
||||
);
|
||||
|
||||
const saveStateSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.saveState,
|
||||
@@ -69,9 +140,19 @@ export const profileDataManagerSelector = createSelector(
|
||||
|
||||
export const staticFieldsSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => (accountSettings.profileDataManager ? ['name', 'email', 'country'] : []),
|
||||
);
|
||||
mostRecentVerifiedNameSelector,
|
||||
(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.
|
||||
@@ -86,7 +167,11 @@ const formValuesSelector = createSelector(
|
||||
(values, drafts) => {
|
||||
const formValues = {};
|
||||
Object.entries(values).forEach(([name, value]) => {
|
||||
formValues[name] = chooseFormValue(drafts[name], value) || '';
|
||||
if (typeof value === 'boolean') {
|
||||
formValues[name] = chooseFormValue(drafts[name], value);
|
||||
} else {
|
||||
formValues[name] = chooseFormValue(drafts[name], value) || '';
|
||||
}
|
||||
});
|
||||
return formValues;
|
||||
},
|
||||
@@ -94,7 +179,7 @@ const formValuesSelector = createSelector(
|
||||
|
||||
const transformTimeZonesToOptions = timeZoneArr => timeZoneArr
|
||||
.map(({ time_zone, description }) => ({ // eslint-disable-line camelcase
|
||||
value: time_zone, label: description,
|
||||
value: time_zone, label: description, // eslint-disable-line camelcase
|
||||
}));
|
||||
|
||||
const timeZonesSelector = createSelector(
|
||||
@@ -131,21 +216,35 @@ export const accountSettingsPageSelector = createSelector(
|
||||
siteLanguageOptionsSelector,
|
||||
siteLanguageSelector,
|
||||
formValuesSelector,
|
||||
valuesSelector,
|
||||
draftsSelector,
|
||||
errorSelector,
|
||||
profileDataManagerSelector,
|
||||
staticFieldsSelector,
|
||||
timeZonesSelector,
|
||||
countryTimeZonesSelector,
|
||||
activeAccountSelector,
|
||||
nameChangeModalSelector,
|
||||
mostRecentApprovedVerifiedNameValueSelector,
|
||||
mostRecentVerifiedNameSelector,
|
||||
sortedVerifiedNameHistorySelector,
|
||||
(
|
||||
accountSettings,
|
||||
siteLanguageOptions,
|
||||
siteLanguage,
|
||||
formValues,
|
||||
committedValues,
|
||||
drafts,
|
||||
formErrors,
|
||||
profileDataManager,
|
||||
staticFields,
|
||||
timeZoneOptions,
|
||||
countryTimeZoneOptions,
|
||||
activeAccount,
|
||||
nameChangeModal,
|
||||
verifiedName,
|
||||
mostRecentVerifiedName,
|
||||
verifiedNameHistory,
|
||||
) => ({
|
||||
siteLanguageOptions,
|
||||
siteLanguage,
|
||||
@@ -156,9 +255,37 @@ export const accountSettingsPageSelector = createSelector(
|
||||
countryTimeZoneOptions,
|
||||
isActive: activeAccount,
|
||||
formValues,
|
||||
committedValues,
|
||||
drafts,
|
||||
formErrors,
|
||||
profileDataManager,
|
||||
staticFields,
|
||||
tpaProviders: accountSettings.thirdPartyAuth.providers,
|
||||
nameChangeModal,
|
||||
verifiedName,
|
||||
mostRecentVerifiedName,
|
||||
verifiedNameHistory,
|
||||
}),
|
||||
);
|
||||
|
||||
export const certPreferenceSelector = createSelector(
|
||||
valuesSelector,
|
||||
formValuesSelector,
|
||||
mostRecentApprovedVerifiedNameValueSelector,
|
||||
saveStateSelector,
|
||||
errorSelector,
|
||||
(
|
||||
committedValues,
|
||||
formValues,
|
||||
mostRecentApprovedVerifiedNameValue,
|
||||
saveState,
|
||||
errors,
|
||||
) => ({
|
||||
originalFullName: committedValues?.name || '',
|
||||
originalVerifiedName: mostRecentApprovedVerifiedNameValue?.verified_name || '',
|
||||
useVerifiedNameForCerts: formValues.useVerifiedNameForCerts || false,
|
||||
saveState,
|
||||
formErrors: errors,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -205,3 +332,12 @@ export const demographicsSectionSelector = createSelector(
|
||||
formErrors: errors,
|
||||
}),
|
||||
);
|
||||
|
||||
export const nameChangeSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
formValuesSelector,
|
||||
(accountSettings, formValues) => ({
|
||||
...accountSettings.nameChange,
|
||||
formValues,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { handleRequestError, unpackFieldErrors } from './utils';
|
||||
import { getThirdPartyAuthProviders } from '../third-party-auth';
|
||||
import { postVerifiedNameConfig } from '../certificate-preference/data/service';
|
||||
import { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service';
|
||||
import { getDemographics, getDemographicsOptions, patchDemographics } from '../demographics/data/service';
|
||||
import { DEMOGRAPHICS_FIELDS } from '../demographics/data/utils';
|
||||
@@ -44,7 +45,9 @@ function packAccountCommitData(commitData) {
|
||||
|
||||
SOCIAL_PLATFORMS.forEach(({ id, key }) => {
|
||||
// Skip missing values. Empty strings are valid values and should be preserved.
|
||||
if (commitData[key] === undefined) return;
|
||||
if (commitData[key] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
packedData.social_links = [{ platform: id, social_link: commitData[key] }];
|
||||
delete packedData[key];
|
||||
@@ -174,12 +177,57 @@ export async function shouldDisplayDemographicsQuestions() {
|
||||
return false;
|
||||
}
|
||||
|
||||
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.
|
||||
* Currently encapsulates Account, Preferences, Coaching, ThirdPartyAuth, and Demographics
|
||||
*/
|
||||
export async function getSettings(username, userRoles, userId) {
|
||||
const results = await Promise.all([
|
||||
const [
|
||||
account,
|
||||
preferences,
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
coaching,
|
||||
shouldDisplayDemographicsQuestionsResponse,
|
||||
demographics,
|
||||
demographicsOptions,
|
||||
] = await Promise.all([
|
||||
getAccount(username),
|
||||
getPreferences(username),
|
||||
getThirdPartyAuthProviders(),
|
||||
@@ -192,15 +240,15 @@ export async function getSettings(username, userRoles, userId) {
|
||||
]);
|
||||
|
||||
return {
|
||||
...results[0],
|
||||
...results[1],
|
||||
thirdPartyAuthProviders: results[2],
|
||||
profileDataManager: results[3],
|
||||
timeZones: results[4],
|
||||
coaching: results[5],
|
||||
shouldDisplayDemographicsSection: results[6],
|
||||
...results[7], // demographics
|
||||
demographicsOptions: results[8],
|
||||
...account,
|
||||
...preferences,
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
coaching,
|
||||
shouldDisplayDemographicsSection: shouldDisplayDemographicsQuestionsResponse,
|
||||
...demographics,
|
||||
demographicsOptions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -215,11 +263,19 @@ export async function patchSettings(username, commitValues, userId) {
|
||||
const preferenceKeys = ['time_zone'];
|
||||
const coachingKeys = ['coaching'];
|
||||
const demographicsKeys = DEMOGRAPHICS_FIELDS;
|
||||
const certificateKeys = ['useVerifiedNameForCerts'];
|
||||
const isDemographicsKey = (value, key) => key.includes('demographics');
|
||||
const accountCommitValues = omit(commitValues, preferenceKeys, coachingKeys, demographicsKeys);
|
||||
const accountCommitValues = omit(
|
||||
commitValues,
|
||||
preferenceKeys,
|
||||
coachingKeys,
|
||||
demographicsKeys,
|
||||
certificateKeys,
|
||||
);
|
||||
const preferenceCommitValues = pick(commitValues, preferenceKeys);
|
||||
const coachingCommitValues = pick(commitValues, coachingKeys);
|
||||
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
|
||||
const certCommitValues = pick(commitValues, certificateKeys);
|
||||
const patchRequests = [];
|
||||
|
||||
if (!isEmpty(accountCommitValues)) {
|
||||
@@ -234,6 +290,9 @@ export async function patchSettings(username, commitValues, userId) {
|
||||
if (!isEmpty(demographicsCommitValues)) {
|
||||
patchRequests.push(patchDemographics(userId, demographicsCommitValues));
|
||||
}
|
||||
if (!isEmpty(certCommitValues)) {
|
||||
patchRequests.push(postVerifiedNameConfig(username, certCommitValues));
|
||||
}
|
||||
|
||||
const results = await Promise.all(patchRequests);
|
||||
// Assigns in order of requests. Preference keys
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,3 @@
|
||||
export {
|
||||
camelCaseObject,
|
||||
convertKeyNames,
|
||||
modifyObjectKeys,
|
||||
snakeCaseObject,
|
||||
} from './dataUtils';
|
||||
export {
|
||||
AsyncActionType,
|
||||
getModuleState,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
// Messages
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
|
||||
// Components
|
||||
@@ -22,13 +23,14 @@ const BeforeProceedingBanner = (props) => {
|
||||
<FormattedMessage
|
||||
id="account.settings.delete.account.before.proceeding"
|
||||
defaultMessage="Before proceeding, please {actionLink}."
|
||||
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'."
|
||||
description="Error that appears if you are trying to delete your account, but something about your account needs attention first. The actionLink will be instructions, such as 'unlink your Facebook account'."
|
||||
values={{
|
||||
actionLink: (
|
||||
<Hyperlink destination={supportArticleUrl}>
|
||||
{intl.formatMessage(messages[instructionMessageId])}
|
||||
</Hyperlink>
|
||||
),
|
||||
siteName: getConfig().SITE_NAME,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button, Input, Modal, ValidationFormGroup } from '@edx/paragon';
|
||||
import {
|
||||
AlertModal,
|
||||
Button, Input, ValidationFormGroup, ActionRow,
|
||||
} from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { faExclamationCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
import Alert from '../Alert';
|
||||
import PrintingInstructions from './PrintingInstructions';
|
||||
@@ -33,10 +36,9 @@ export class ConfirmationModal extends Component {
|
||||
return null;
|
||||
}
|
||||
const headerMessageId = this.getShortErrorMessageId(errorType);
|
||||
const detailsMessageId =
|
||||
reason === 'empty-password'
|
||||
? null
|
||||
: 'account.settings.delete.account.error.unable.to.delete.details';
|
||||
const detailsMessageId = reason === 'empty-password'
|
||||
? null
|
||||
: 'account.settings.delete.account.error.unable.to.delete.details';
|
||||
|
||||
return (
|
||||
<Alert
|
||||
@@ -64,52 +66,66 @@ export class ConfirmationModal extends Component {
|
||||
const open = ['confirming', 'pending', 'failed'].includes(status);
|
||||
const passwordFieldId = 'passwordFieldId';
|
||||
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 (
|
||||
<Modal
|
||||
open={open}
|
||||
<AlertModal
|
||||
isOpen={open}
|
||||
title={intl.formatMessage(messages['account.settings.delete.account.modal.header'])}
|
||||
body={
|
||||
<div>
|
||||
{this.renderError()}
|
||||
<Alert
|
||||
className="alert-warning mt-n2"
|
||||
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
|
||||
>
|
||||
<h6>
|
||||
{intl.formatMessage(messages['account.settings.delete.account.modal.text.1'])}
|
||||
</h6>
|
||||
<p>{intl.formatMessage(messages['account.settings.delete.account.modal.text.2'])}</p>
|
||||
<p>
|
||||
<PrintingInstructions />
|
||||
</p>
|
||||
</Alert>
|
||||
<ValidationFormGroup
|
||||
for={passwordFieldId}
|
||||
invalid={errorType !== null}
|
||||
invalidMessage={intl.formatMessage(invalidMessage)}
|
||||
>
|
||||
<label className="d-block" htmlFor={passwordFieldId}>
|
||||
{intl.formatMessage(messages['account.settings.delete.account.modal.enter.password'])}
|
||||
</label>
|
||||
<Input
|
||||
name="password"
|
||||
id={passwordFieldId}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
</div>
|
||||
}
|
||||
buttons={[
|
||||
<Button className="btn-danger" onClick={onSubmit}>
|
||||
{intl.formatMessage(messages['account.settings.delete.account.modal.confirm.delete'])}
|
||||
</Button>,
|
||||
]}
|
||||
closeText={intl.formatMessage(messages['account.settings.delete.account.modal.confirm.cancel'])}
|
||||
renderHeaderCloseButton={false}
|
||||
onClose={onCancel}
|
||||
/>
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<Button variant="link" onClick={onCancel}>Cancel</Button>
|
||||
<Button variant="danger" onClick={onSubmit}>Yes, Delete</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
>
|
||||
<div className="p-3">
|
||||
{this.renderError()}
|
||||
<Alert
|
||||
className="alert-warning mt-n2"
|
||||
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
|
||||
>
|
||||
<h6>
|
||||
{intl.formatMessage(
|
||||
messages['account.settings.delete.account.modal.text.1'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</h6>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages[deleteAccountModalText2MessageKey],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<PrintingInstructions />
|
||||
</p>
|
||||
</Alert>
|
||||
<ValidationFormGroup
|
||||
for={passwordFieldId}
|
||||
invalid={errorType !== null}
|
||||
invalidMessage={intl.formatMessage(invalidMessage)}
|
||||
>
|
||||
<label className="d-block" htmlFor={passwordFieldId}>
|
||||
{intl.formatMessage(messages['account.settings.delete.account.modal.enter.password'])}
|
||||
</label>
|
||||
<Input
|
||||
name="password"
|
||||
id={passwordFieldId}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
</div>
|
||||
|
||||
</AlertModal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,13 @@ import ConnectedSuccessModal from './SuccessModal';
|
||||
import BeforeProceedingBanner from './BeforeProceedingBanner';
|
||||
|
||||
export class DeleteAccount extends React.Component {
|
||||
state = {
|
||||
password: '',
|
||||
};
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
password: '',
|
||||
};
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
if (this.state.password === '') {
|
||||
@@ -56,19 +60,42 @@ export class DeleteAccount extends React.Component {
|
||||
} = this.props;
|
||||
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';
|
||||
|
||||
const optInInstructionMessageId = getConfig().MARKETING_EMAILS_OPT_IN
|
||||
? 'account.settings.delete.account.please.confirm'
|
||||
: 'account.settings.delete.account.please.activate';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="section-heading">
|
||||
<h2 className="section-heading h4 mb-3">
|
||||
{intl.formatMessage(messages['account.settings.delete.account.header'])}
|
||||
</h2>
|
||||
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
|
||||
<p>{intl.formatMessage(messages['account.settings.delete.account.text.1'])}</p>
|
||||
<p>{intl.formatMessage(messages['account.settings.delete.account.text.2'])}</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages['account.settings.delete.account.text.1'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages[deleteAccountText2MessageKey],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<PrintingInstructions />
|
||||
</p>
|
||||
<p className="text-danger h6">
|
||||
{intl.formatMessage(messages['account.settings.delete.account.text.warning'])}
|
||||
{intl.formatMessage(
|
||||
messages['account.settings.delete.account.text.warning'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<Hyperlink destination="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings">
|
||||
@@ -77,7 +104,7 @@ export class DeleteAccount extends React.Component {
|
||||
</p>
|
||||
<p>
|
||||
<Button
|
||||
className="btn-outline-danger"
|
||||
variant="outline-danger"
|
||||
onClick={canDelete ? this.props.deleteAccountConfirmation : null}
|
||||
disabled={!canDelete}
|
||||
>
|
||||
@@ -87,8 +114,8 @@ export class DeleteAccount extends React.Component {
|
||||
|
||||
{isVerifiedAccount ? null : (
|
||||
<BeforeProceedingBanner
|
||||
instructionMessageId="account.settings.delete.account.please.activate"
|
||||
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-activate-my-account-"
|
||||
instructionMessageId={optInInstructionMessageId}
|
||||
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email-"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// Testing the modals separately, they just clutter up the snapshots if included here.
|
||||
jest.mock('./ConfirmationModal');
|
||||
jest.mock('./SuccessModal');
|
||||
jest.mock('./ConfirmationModal', () => function () {
|
||||
return <></>;
|
||||
});
|
||||
jest.mock('./SuccessModal', () => function () {
|
||||
return <></>;
|
||||
});
|
||||
|
||||
import { DeleteAccount } from './DeleteAccount'; // eslint-disable-line import/first
|
||||
|
||||
@@ -37,6 +42,7 @@ describe('DeleteAccount', () => {
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
||||
@@ -2,22 +2,39 @@ import React from 'react';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
|
||||
const PrintingInstructions = (props) => {
|
||||
const actionLink = (
|
||||
<Hyperlink
|
||||
destination="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
|
||||
// TODO: What would a generic version of this link look like? Should
|
||||
// CERTIFICATE_SHARING_HELP_URL really be a configuration variable? In the meantime,
|
||||
// We've removed the link from the default message.
|
||||
destination="https://support.edx.org/hc/en-us/sections/115004173027-Receive-and-Share-edX-Certificates"
|
||||
>
|
||||
{props.intl.formatMessage(messages['account.settings.delete.account.text.3.link'])}
|
||||
</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 (
|
||||
<FormattedMessage
|
||||
id="account.settings.delete.account.text.3"
|
||||
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"
|
||||
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."
|
||||
description="A message in the user account deletion area warning users that deleting their account will prevent them from accessing their certificates."
|
||||
values={{ actionLink }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Modal } from '@edx/paragon';
|
||||
import { ModalLayer, ModalCloseButton } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const SuccessModal = (props) => {
|
||||
const { status, intl, onClose } = props;
|
||||
return (
|
||||
<Modal
|
||||
open={status === 'deleted'}
|
||||
title={intl.formatMessage(messages['account.settings.delete.account.modal.after.header'])}
|
||||
body={
|
||||
<div>
|
||||
|
||||
<ModalLayer isOpen={status === 'deleted'} onClose={onClose}>
|
||||
<div className="mw-sm p-5 bg-white mx-auto my-3">
|
||||
<h3>
|
||||
{intl.formatMessage(messages['account.settings.delete.account.modal.after.header'])}
|
||||
</h3>
|
||||
<div className="p-3">
|
||||
<p className="h6">
|
||||
{intl.formatMessage(messages['account.settings.delete.account.modal.after.text'])}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
closeText={intl.formatMessage(messages['account.settings.delete.account.modal.after.button'])}
|
||||
renderHeaderCloseButton={false}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<p>
|
||||
<ModalCloseButton className="float-right" variant="link">Close</ModalCloseButton>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</ModalLayer>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,439 +1,370 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConfirmationModal should match default closed confirmation modal snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click fade"
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id2"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id2"
|
||||
>
|
||||
Are you sure?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6>
|
||||
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. 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="paragonCloseModalButton1"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
exports[`ConfirmationModal should match default closed confirmation modal snapshot 1`] = `null`;
|
||||
|
||||
exports[`ConfirmationModal should match empty password confirmation modal snapshot 1`] = `
|
||||
<div>
|
||||
Array [
|
||||
<div
|
||||
className="modal-backdrop show"
|
||||
role="presentation"
|
||||
/>
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
/>,
|
||||
<div
|
||||
className="modal js-close-modal-on-click show d-block"
|
||||
className="pgn__modal-layer"
|
||||
data-focus-lock-disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id6"
|
||||
aria-modal={true}
|
||||
className="modal-dialog"
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
className="pgn__modal-content-container"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
className="pgn__modal-backdrop"
|
||||
onClick={[MockFunction]}
|
||||
onKeyDown={[MockFunction]}
|
||||
/>
|
||||
<div
|
||||
aria-label="Are you sure?"
|
||||
className="pgn__modal pgn__modal-md pgn__modal-default pgn__alert-modal"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
className="pgn__modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id6"
|
||||
className="pgn__modal-title"
|
||||
>
|
||||
Are you sure?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
className="pgn__modal-body pgn__modal-body-scroll-top pgn__modal-body-scroll-bottom"
|
||||
>
|
||||
<div>
|
||||
<div />
|
||||
<div
|
||||
className="pgn__modal-body-content"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-danger mt-n2"
|
||||
className="p-3"
|
||||
>
|
||||
<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"
|
||||
<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 {}}
|
||||
/>
|
||||
</svg>
|
||||
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>
|
||||
<h6>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6>
|
||||
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
|
||||
</h6>
|
||||
<p>
|
||||
If you proceed, you will be unable to use this account to take courses on localhost.
|
||||
</p>
|
||||
<p>
|
||||
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="d-block"
|
||||
htmlFor="passwordFieldId"
|
||||
>
|
||||
If you still wish to continue and delete your account, please enter your account password:
|
||||
</label>
|
||||
<input
|
||||
aria-describedby="passwordFieldId-invalid-feedback"
|
||||
className="form-control is-invalid"
|
||||
id="passwordFieldId"
|
||||
name="password"
|
||||
onChange={[MockFunction]}
|
||||
type="password"
|
||||
value="fluffy bunnies"
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="passwordFieldId-invalid-feedback"
|
||||
>
|
||||
A password is required
|
||||
</h6>
|
||||
<p
|
||||
className="text-danger"
|
||||
>
|
||||
Sorry, there was an error trying to process your request. Please try again later.
|
||||
</p>
|
||||
</strong>
|
||||
</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>
|
||||
<div
|
||||
className="modal-footer"
|
||||
className="pgn__modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
<div
|
||||
className="pgn__action-row"
|
||||
>
|
||||
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>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
disabled={false}
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
Yes, Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
/>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
|
||||
<div>
|
||||
Array [
|
||||
<div
|
||||
className="modal-backdrop show"
|
||||
role="presentation"
|
||||
/>
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>,
|
||||
<div
|
||||
className="modal js-close-modal-on-click show d-block"
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
className="pgn__modal-layer"
|
||||
data-focus-lock-disabled="disabled"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id4"
|
||||
aria-modal={true}
|
||||
className="modal-dialog"
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
className="pgn__modal-content-container"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
className="pgn__modal-backdrop"
|
||||
onClick={[MockFunction]}
|
||||
onKeyDown={[MockFunction]}
|
||||
/>
|
||||
<div
|
||||
aria-label="Are you sure?"
|
||||
className="pgn__modal pgn__modal-md pgn__modal-default pgn__alert-modal"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
className="pgn__modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id4"
|
||||
className="pgn__modal-title"
|
||||
>
|
||||
Are you sure?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
className="pgn__modal-body pgn__modal-body-scroll-top pgn__modal-body-scroll-bottom"
|
||||
>
|
||||
<div>
|
||||
<div />
|
||||
<div
|
||||
className="pgn__modal-body-content"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
className="p-3"
|
||||
>
|
||||
<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"
|
||||
<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 {}}
|
||||
/>
|
||||
</svg>
|
||||
viewBox="0 0 576 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6>
|
||||
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
|
||||
</h6>
|
||||
<p>
|
||||
If you proceed, you will be unable to use this account to take courses on localhost.
|
||||
</p>
|
||||
<p>
|
||||
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<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
|
||||
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
|
||||
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>
|
||||
<div
|
||||
className="modal-footer"
|
||||
className="pgn__modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
<div
|
||||
className="pgn__action-row"
|
||||
>
|
||||
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>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
disabled={false}
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
Yes, Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>,
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`DeleteAccount should match default section snapshot 1`] = `
|
||||
<div>
|
||||
<h2
|
||||
className="section-heading"
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Delete My Account
|
||||
</h2>
|
||||
@@ -11,31 +11,22 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
|
||||
We're sorry to see you go!
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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 employer’s or university’s system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
|
||||
Once your account is deleted, you cannot use it to take courses on localhost.
|
||||
</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>
|
||||
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
|
||||
</p>
|
||||
<p
|
||||
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 edX.
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
@@ -47,9 +38,7 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
Delete My Account
|
||||
@@ -61,7 +50,7 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
|
||||
exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
<div>
|
||||
<h2
|
||||
className="section-heading"
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Delete My Account
|
||||
</h2>
|
||||
@@ -69,31 +58,22 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
We're sorry to see you go!
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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 employer’s or university’s system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
|
||||
Once your account is deleted, you cannot use it to take courses on localhost.
|
||||
</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>
|
||||
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
|
||||
</p>
|
||||
<p
|
||||
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 edX.
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
@@ -105,9 +85,7 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onClick={null}
|
||||
type="button"
|
||||
>
|
||||
Delete My Account
|
||||
@@ -136,17 +114,16 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
Before proceeding, please
|
||||
<a
|
||||
href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-activate-my-account-"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
activate your account
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
Before proceeding, please
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email-"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
activate your account
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -155,7 +132,7 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
exports[`DeleteAccount should match unverified account section snapshot 2`] = `
|
||||
<div>
|
||||
<h2
|
||||
className="section-heading"
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Delete My Account
|
||||
</h2>
|
||||
@@ -163,31 +140,22 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
|
||||
We're sorry to see you go!
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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 employer’s or university’s system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
|
||||
Once your account is deleted, you cannot use it to take courses on localhost.
|
||||
</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>
|
||||
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
|
||||
</p>
|
||||
<p
|
||||
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 edX.
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
@@ -199,9 +167,7 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onClick={null}
|
||||
type="button"
|
||||
>
|
||||
Delete My Account
|
||||
@@ -230,17 +196,16 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
Before proceeding, please
|
||||
<a
|
||||
href="https://support.edx.org/hc/en-us/articles/207206067"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
unlink all social media accounts
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
Before proceeding, please
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/articles/207206067"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
unlink all social media accounts
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,311 +1,89 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SuccessModal should match default closed success modal snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click fade"
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id2"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id2"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton1"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
exports[`SuccessModal should match default closed success modal snapshot 1`] = `null`;
|
||||
|
||||
exports[`SuccessModal should match default closed success modal snapshot 2`] = `
|
||||
<div>
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click fade"
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id4"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id4"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton3"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
exports[`SuccessModal should match default closed success modal snapshot 2`] = `null`;
|
||||
|
||||
exports[`SuccessModal should match default closed success modal snapshot 3`] = `
|
||||
<div>
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click fade"
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
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 3`] = `null`;
|
||||
|
||||
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 default closed success modal snapshot 4`] = `null`;
|
||||
|
||||
exports[`SuccessModal should match open success modal snapshot 1`] = `
|
||||
<div>
|
||||
Array [
|
||||
<div
|
||||
className="modal-backdrop show"
|
||||
role="presentation"
|
||||
/>
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>,
|
||||
<div
|
||||
className="modal js-close-modal-on-click show d-block"
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
className="pgn__modal-layer"
|
||||
data-focus-lock-disabled="disabled"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id10"
|
||||
aria-modal={true}
|
||||
className="modal-dialog"
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
className="pgn__modal-content-container"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
className="pgn__modal-backdrop"
|
||||
onClick={[MockFunction]}
|
||||
onKeyDown={[MockFunction]}
|
||||
/>
|
||||
<div
|
||||
className="mw-sm p-5 bg-white mx-auto my-3"
|
||||
>
|
||||
<h3>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h3>
|
||||
<div
|
||||
className="modal-header"
|
||||
className="p-3"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id10"
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
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
|
||||
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"
|
||||
>
|
||||
<p>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton9"
|
||||
onBlur={[Function]}
|
||||
className="pgn__modal-close-button float-right btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</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}
|
||||
/>,
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default } from './DeleteAccount';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
|
||||
@@ -13,22 +13,27 @@ const messages = defineMessages({
|
||||
},
|
||||
'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. edX 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. {siteName} will not be able to recover your account or the data that is deleted.',
|
||||
description: 'A message in the user account deletion area',
|
||||
},
|
||||
'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 employer’s or university’s 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',
|
||||
},
|
||||
'account.settings.delete.account.text.3.link': {
|
||||
id: 'account.settings.delete.account.text.3.link',
|
||||
defaultMessage: 'follow the instructions for printing or downloading a certificate',
|
||||
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, ______ .',
|
||||
defaultMessage: 'Follow these 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.',
|
||||
},
|
||||
'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 edX.',
|
||||
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}.',
|
||||
description: 'A message in the user account deletion area',
|
||||
},
|
||||
'account.settings.delete.account.text.change.instead': {
|
||||
@@ -39,13 +44,18 @@ const messages = defineMessages({
|
||||
'account.settings.delete.account.button': {
|
||||
id: 'account.settings.delete.account.button',
|
||||
defaultMessage: 'Delete My Account',
|
||||
description: 'Button label to permanently delete your edX account',
|
||||
description: 'Button label to permanently delete your platform account',
|
||||
},
|
||||
'account.settings.delete.account.please.activate': {
|
||||
id: 'account.settings.delete.account.please.activate',
|
||||
defaultMessage: 'activate your account',
|
||||
description: 'This is the text on a link that goes to the support page. It is part of this sentence: Before proceeding, please activate your account.',
|
||||
},
|
||||
'account.settings.delete.account.please.confirm': {
|
||||
id: 'account.settings.delete.account.please.confirm',
|
||||
defaultMessage: 'confirm your account',
|
||||
description: 'This is the text on a link that goes to the support page. It is part of this sentence: Before proceeding, please confirm your account.',
|
||||
},
|
||||
'account.settings.delete.account.please.unlink': {
|
||||
id: 'account.settings.delete.account.please.unlink',
|
||||
defaultMessage: 'unlink all social media accounts',
|
||||
@@ -58,11 +68,16 @@ const messages = defineMessages({
|
||||
},
|
||||
'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. edX 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. {siteName} 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',
|
||||
},
|
||||
'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.',
|
||||
description: 'Messaging in the dialog asking user to confirm that they want to delete their entire account',
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CheckBox } from '@edx/paragon';
|
||||
import { Form } from '@edx/paragon';
|
||||
import { DECLINED } from '../data/constants';
|
||||
|
||||
export const Checkboxes = (props) => {
|
||||
const Checkboxes = (props) => {
|
||||
const {
|
||||
id,
|
||||
options,
|
||||
@@ -13,12 +13,12 @@ export const Checkboxes = (props) => {
|
||||
|
||||
const [selected, setSelected] = useState(values);
|
||||
useEffect(() => {
|
||||
onChange(id, selected)
|
||||
}, [selected])
|
||||
onChange(id, selected);
|
||||
}, [id, onChange, selected]);
|
||||
|
||||
const handleToggle = (value, option) => {
|
||||
// If the user checked 'declined', uncheck all other options
|
||||
if (value && option == DECLINED) {
|
||||
if (value && option === DECLINED) {
|
||||
setSelected([DECLINED]);
|
||||
return;
|
||||
}
|
||||
@@ -33,46 +33,48 @@ export const Checkboxes = (props) => {
|
||||
if (!value) {
|
||||
setSelected(selected.filter(i => i !== option));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderCheckboxes = () => {
|
||||
return options.map((option, index) => {
|
||||
const isFirst = index == 0;
|
||||
const isChecked = selected.includes(option.value);
|
||||
return (
|
||||
<div key={index} className="checkboxOption">
|
||||
<CheckBox
|
||||
type="checkbox"
|
||||
id={option.value}
|
||||
name={option.value}
|
||||
value={option.value}
|
||||
checked={isChecked}
|
||||
autoFocus={isFirst}
|
||||
label={option.label}
|
||||
onChange={(value) => handleToggle(value, option.value)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
const renderCheckboxes = () => options.map((option, index) => {
|
||||
const isFirst = index === 0;
|
||||
const isChecked = selected.includes(option.value);
|
||||
return (
|
||||
<div key={option.value} className="checkboxOption">
|
||||
<Form.Checkbox
|
||||
type="checkbox"
|
||||
id={option.value}
|
||||
name={option.value}
|
||||
value={option.value}
|
||||
checked={isChecked}
|
||||
autoFocus={isFirst}
|
||||
onChange={(event) => handleToggle(event.target.checked, option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</Form.Checkbox>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div role="group">
|
||||
{renderCheckboxes()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
Checkboxes.propTypes = {
|
||||
id: PropTypes.string,
|
||||
options: PropTypes.array,
|
||||
values: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
id: PropTypes.string.isRequired,
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
value: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
})),
|
||||
values: PropTypes.arrayOf(PropTypes.string),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Checkboxes.defaultProps = {
|
||||
options: [],
|
||||
values: [],
|
||||
}
|
||||
};
|
||||
|
||||
export default Checkboxes;
|
||||
|
||||
@@ -1,31 +1,109 @@
|
||||
import {
|
||||
OTHER,
|
||||
SELF_DESCRIBE,
|
||||
} from '../data/constants';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { saveMultipleSettings, updateDraft } from '../data/actions';
|
||||
|
||||
import Alert from '../Alert';
|
||||
import Checkboxes from './Checkboxes';
|
||||
import EditableField from '../EditableField';
|
||||
import { Input } from '@edx/paragon';
|
||||
import { Hyperlink, Form } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { demographicsSectionSelector } from '../data/selectors';
|
||||
import get from 'lodash.get';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import memoize from 'memoize-one';
|
||||
import { demographicsSectionSelector } from '../data/selectors';
|
||||
import EditableSelectField from '../EditableSelectField';
|
||||
import Checkboxes from './Checkboxes';
|
||||
import Alert from '../Alert';
|
||||
import { saveMultipleSettings, updateDraft } from '../data/actions';
|
||||
import {
|
||||
OTHER,
|
||||
SELF_DESCRIBE,
|
||||
} from '../data/constants';
|
||||
import messages from './DemographicsSection.messages';
|
||||
|
||||
class DemographicsSection extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
// We check the `demographicsOptions` prop to see if it is empty before we attempt to extract and
|
||||
// format the available options for each question from the API response.
|
||||
getApiOptions = memoize((demographicsOptions) => (this.hasRetrievedDemographicsOptions() && {
|
||||
demographicsGenderOptions: this.addDefaultOption('account.settings.field.demographics.gender.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.gender.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
/* Ethnicity options don't need the blank/default option */
|
||||
demographicsEthnicityOptions: demographicsOptions.actions.POST.user_ethnicity.child.children.ethnicity.choices.map(
|
||||
key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}),
|
||||
),
|
||||
demographicsIncomeOptions: this.addDefaultOption('account.settings.field.demographics.income.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.income.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsMilitaryHistoryOptions: this.addDefaultOption('account.settings.field.demographics.military_history.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.military_history.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsEducationLevelOptions: this.addDefaultOption('account.settings.field.demographics.education_level.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.learner_education_level.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsWorkStatusOptions: this.addDefaultOption('account.settings.field.demographics.work_status.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.work_status.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsWorkSectorOptions: this.addDefaultOption('account.settings.field.demographics.work_sector.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.current_work_sector.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
}));
|
||||
|
||||
ethnicityFieldDisplay = (demographicsEthnicityOptions) => {
|
||||
let ethnicities = [];
|
||||
if (get(this, 'props.formValues.demographics_user_ethnicity')) {
|
||||
ethnicities = this.props.formValues.demographics_user_ethnicity;
|
||||
}
|
||||
return ethnicities.map((e) => {
|
||||
const matchingOption = demographicsEthnicityOptions.filter(option => option.value === e)[0];
|
||||
return matchingOption && matchingOption.label;
|
||||
}).join(', ');
|
||||
};
|
||||
|
||||
handleEditableFieldChange = (name, value) => {
|
||||
this.props.updateDraft(name, value);
|
||||
};
|
||||
|
||||
handleSubmit = (formId) => {
|
||||
// We have some custom fields in this section. Instead of relying on the
|
||||
// submitted values, submit the values stored in 'drafts'.
|
||||
const { drafts } = this.props;
|
||||
const settingsArray = Object.entries(drafts).map(([field, value]) => ({
|
||||
formId: field,
|
||||
commitValues: value,
|
||||
}));
|
||||
|
||||
this.props.saveMultipleSettings(settingsArray, formId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility method that adds the specified message as a default option to the list of available
|
||||
* choices.
|
||||
*
|
||||
* @param {*} messageId id of message matching desired default label text
|
||||
*/
|
||||
addDefaultOption(messageId) {
|
||||
return [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages[messageId]),
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,100 +116,19 @@ class DemographicsSection extends React.Component {
|
||||
return !isEmpty(this.props.formValues.demographicsOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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]),
|
||||
}];
|
||||
}
|
||||
|
||||
// We check the `demographicsOptions` prop to see if it is empty before we attempt to extract and
|
||||
// format the available options for each question from the API response.
|
||||
getApiOptions = memoize((demographicsOptions) => ( this.hasRetrievedDemographicsOptions() && {
|
||||
demographicsGenderOptions: this.addDefaultOption('account.settings.field.demographics.gender.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.gender.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name
|
||||
}))),
|
||||
/* Ethnicity options don't need the blank/default option */
|
||||
demographicsEthnicityOptions: demographicsOptions.actions.POST.user_ethnicity.child.children.ethnicity.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name
|
||||
})),
|
||||
demographicsIncomeOptions: this.addDefaultOption('account.settings.field.demographics.income.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.income.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name
|
||||
}))),
|
||||
demographicsMilitaryHistoryOptions: this.addDefaultOption('account.settings.field.demographics.military_history.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.military_history.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name
|
||||
}))),
|
||||
demographicsEducationLevelOptions: this.addDefaultOption('account.settings.field.demographics.education_level.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.learner_education_level.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name
|
||||
}))),
|
||||
demographicsWorkStatusOptions: this.addDefaultOption('account.settings.field.demographics.work_status.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.work_status.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name
|
||||
}))),
|
||||
demographicsWorkSectorOptions: this.addDefaultOption('account.settings.field.demographics.work_sector.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.current_work_sector.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name
|
||||
}))),
|
||||
}));
|
||||
|
||||
ethnicityFieldDisplay = (demographicsEthnicityOptions) => {
|
||||
if (get(this, 'props.formValues.demographics_user_ethnicity')) {
|
||||
const ethnicities = this.props.formValues.demographics_user_ethnicity;
|
||||
return ethnicities.map((e) => {
|
||||
var matchingOption = demographicsEthnicityOptions.filter(option => option.value === e)[0];
|
||||
return matchingOption && matchingOption.label;
|
||||
}).join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
handleEditableFieldChange = (name, value) => {
|
||||
this.props.updateDraft(name, value);
|
||||
};
|
||||
|
||||
handleSubmit = (formId, values) => {
|
||||
// We have some custom fields in this section. Instead of relying on the
|
||||
// submitted values, submit the values stored in 'drafts'.
|
||||
const drafts = this.props.drafts;
|
||||
const settingsArray = []
|
||||
for (let field in drafts) {
|
||||
settingsArray.push({
|
||||
formId: field,
|
||||
commitValues: drafts[field]
|
||||
})
|
||||
}
|
||||
|
||||
this.props.saveMultipleSettings(settingsArray, formId);
|
||||
};
|
||||
|
||||
/**
|
||||
* If an error is encountered when trying to communicate with the Demographics IDA then we will
|
||||
* display an Alert letting the user know that their info will not be displayed and temporarily
|
||||
* cannot be updated.
|
||||
*/
|
||||
renderDemographicsServiceIssueWarning() {
|
||||
if (!isEmpty(this.props.formErrors.demographicsError) |
|
||||
this.hasRetrievedDemographicsOptions() == false) {
|
||||
if (!isEmpty(this.props.formErrors.demographicsError)
|
||||
|| !this.hasRetrievedDemographicsOptions()) {
|
||||
return (
|
||||
<div
|
||||
tabIndex="-1"
|
||||
ref={this.alertRef}>
|
||||
ref={this.alertRef}
|
||||
>
|
||||
<Alert className="alert alert-danger" role="alert">
|
||||
<FormattedMessage
|
||||
id="account.settings.message.demographics.service.issue"
|
||||
@@ -141,9 +138,8 @@ class DemographicsSection extends React.Component {
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -162,28 +158,37 @@ class DemographicsSection extends React.Component {
|
||||
demographicsWorkSectorOptions,
|
||||
} = this.getApiOptions(this.props.formValues.demographicsOptions);
|
||||
|
||||
const showSelfDescribe = this.props.formValues.demographics_gender == SELF_DESCRIBE;
|
||||
const showWorkStatusDescribe = this.props.formValues.demographics_work_status == OTHER;
|
||||
const showSelfDescribe = this.props.formValues.demographics_gender === SELF_DESCRIBE;
|
||||
const showWorkStatusDescribe = this.props.formValues.demographics_work_status === OTHER;
|
||||
|
||||
return (
|
||||
<div className="account-section" id="demographics-information" ref={this.props.forwardRef}>
|
||||
<h2 className="section-heading">
|
||||
<div className="account-section pt-3 mb-5" id="demographics-information" ref={this.props.forwardRef}>
|
||||
<h2 className="section-heading h4 mb-3">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.demographics.information'])}
|
||||
</h2>
|
||||
<p>
|
||||
<a href={getConfig().MARKETING_SITE_BASE_URL + '/demographics'} target="_blank">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.demographics.why'])}
|
||||
</a>
|
||||
<Hyperlink
|
||||
destination={`${getConfig().MARKETING_SITE_BASE_URL}/demographics`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{this.props.intl.formatMessage(
|
||||
messages['account.settings.section.demographics.why'],
|
||||
{
|
||||
siteName: getConfig().SITE_NAME,
|
||||
},
|
||||
)}
|
||||
</Hyperlink>
|
||||
</p>
|
||||
{this.renderDemographicsServiceIssueWarning()}
|
||||
{/*
|
||||
{/*
|
||||
If the demographicsOptions props are empty then there is no need to display the fields as
|
||||
the user will not have any choices available to select, nor will they be able to update
|
||||
their answers.
|
||||
*/}
|
||||
{ this.hasRetrievedDemographicsOptions() &&
|
||||
{this.hasRetrievedDemographicsOptions() && (
|
||||
<div id="demographics-fields">
|
||||
<EditableField
|
||||
<EditableSelectField
|
||||
name="demographics_gender"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_gender}
|
||||
@@ -193,20 +198,20 @@ class DemographicsSection extends React.Component {
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender.empty'])}
|
||||
{...editableFieldProps}
|
||||
>
|
||||
{showSelfDescribe &&
|
||||
<Input
|
||||
name='demographics_gender_description'
|
||||
id='field-demographics_gender_description'
|
||||
type='text'
|
||||
{showSelfDescribe && (
|
||||
<Form.Control
|
||||
name="demographics_gender_description"
|
||||
id="field-demographics_gender_description"
|
||||
type="text"
|
||||
placeholder={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender_description.empty'])}
|
||||
value={this.props.formValues.demographics_gender_description}
|
||||
onChange={(e) => this.handleEditableFieldChange(`demographics_gender_description`, e.target.value)}
|
||||
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
|
||||
)}
|
||||
</EditableSelectField>
|
||||
<EditableSelectField
|
||||
name="demographics_user_ethnicity"
|
||||
type="select"
|
||||
hidden
|
||||
@@ -221,8 +226,8 @@ class DemographicsSection extends React.Component {
|
||||
values={this.props.formValues.demographics_user_ethnicity}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
</EditableField>
|
||||
<EditableField
|
||||
</EditableSelectField>
|
||||
<EditableSelectField
|
||||
name="demographics_income"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_income}
|
||||
@@ -231,7 +236,7 @@ class DemographicsSection extends React.Component {
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.income.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
<EditableSelectField
|
||||
name="demographics_military_history"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_military_history}
|
||||
@@ -240,7 +245,7 @@ class DemographicsSection extends React.Component {
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.military_history.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
<EditableSelectField
|
||||
name="demographics_learner_education_level"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_learner_education_level}
|
||||
@@ -249,7 +254,7 @@ class DemographicsSection extends React.Component {
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.learner_education_level.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
<EditableSelectField
|
||||
name="demographics_parent_education_level"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_parent_education_level}
|
||||
@@ -258,30 +263,32 @@ class DemographicsSection extends React.Component {
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.parent_education_level.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
<EditableSelectField
|
||||
name="demographics_work_status"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_work_status}
|
||||
userSuppliedValue={showWorkStatusDescribe ? this.props.formValues.demographics_work_status_description : null}
|
||||
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'
|
||||
{showWorkStatusDescribe && (
|
||||
<Form.Control
|
||||
name="demographics_work_status_description"
|
||||
id="field-demographics_work_status_description"
|
||||
type="text"
|
||||
placeholder={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status_description.empty'])}
|
||||
value={this.props.formValues.demographics_work_status_description}
|
||||
onChange={(e) => this.handleEditableFieldChange(`demographics_work_status_description`, e.target.value)}
|
||||
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
|
||||
)}
|
||||
</EditableSelectField>
|
||||
<EditableSelectField
|
||||
name="demographics_current_work_sector"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_current_work_sector}
|
||||
@@ -290,7 +297,7 @@ class DemographicsSection extends React.Component {
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.current_work_sector.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
<EditableSelectField
|
||||
name="demographics_future_work_sector"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_future_work_sector}
|
||||
@@ -300,17 +307,17 @@ class DemographicsSection extends React.Component {
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
DemographicsSection.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
formValues: PropTypes.shape({
|
||||
demographics_gender: PropTypes.string,
|
||||
demographics_user_ethnicity: PropTypes.array,
|
||||
demographics_user_ethnicity: PropTypes.shape([]),
|
||||
demographics_income: PropTypes.string,
|
||||
demographics_military_history: PropTypes.string,
|
||||
demographics_learner_education_level: PropTypes.string,
|
||||
@@ -318,11 +325,30 @@ DemographicsSection.propTypes = {
|
||||
demographics_work_status: PropTypes.string,
|
||||
demographics_current_work_sector: PropTypes.string,
|
||||
demographics_future_work_sector: PropTypes.string,
|
||||
demographics_work_status_description: PropTypes.string,
|
||||
demographics_gender_description: PropTypes.string,
|
||||
demographicsOptions: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
drafts: PropTypes.shape({
|
||||
demographics_gender: PropTypes.string,
|
||||
demographics_user_ethnicity: PropTypes.shape([]),
|
||||
demographics_income: PropTypes.string,
|
||||
demographics_military_history: PropTypes.string,
|
||||
demographics_learner_education_level: PropTypes.string,
|
||||
demographics_parent_education_level: PropTypes.string,
|
||||
demographics_work_status: PropTypes.string,
|
||||
demographics_current_work_sector: PropTypes.string,
|
||||
demographics_future_work_sector: PropTypes.string,
|
||||
demographics_work_status_description: PropTypes.string,
|
||||
demographics_gender_description: PropTypes.string,
|
||||
demographicsOptions: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
formErrors: PropTypes.shape({
|
||||
demographicsError: PropTypes.string,
|
||||
}).isRequired,
|
||||
updateDraft: PropTypes.func.isRequired
|
||||
forwardRef: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
saveMultipleSettings: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(demographicsSectionSelector, {
|
||||
|
||||
@@ -162,7 +162,7 @@ const messages = defineMessages({
|
||||
/* Legal copy link text */
|
||||
'account.settings.section.demographics.why': {
|
||||
id: 'account.settings.section.demographics.why',
|
||||
defaultMessage: 'Why does edX collect this information?',
|
||||
defaultMessage: 'Why does {siteName} collect this information?',
|
||||
description: 'Link text for a link to external legal text',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -22,8 +22,7 @@ export function createDemographicsError(error) {
|
||||
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];
|
||||
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
|
||||
|
||||
@@ -1,583 +1,584 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import DemographicsSection from '../DemographicsSection';
|
||||
import { Provider } from 'react-redux';
|
||||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import renderer from 'react-test-renderer';
|
||||
import DemographicsSection from '../DemographicsSection';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
const IntlDemographicsSection = injectIntl(DemographicsSection);
|
||||
|
||||
jest.mock('../../data/selectors', () => {
|
||||
return jest.fn().mockImplementation(() => ({ demographicsSectionSelector: () => ({}) }));
|
||||
});
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ demographicsSectionSelector: () => ({}) })));
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('DemographicsSection', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
updateDraft: jest.fn(),
|
||||
formValues: {
|
||||
demographics_gender: 'declined',
|
||||
demographics_gender_description: '',
|
||||
demographics_user_ethnicity: [],
|
||||
demographics_income: 'declined',
|
||||
demographics_military_history: 'declined',
|
||||
demographics_learner_education_level: 'declined',
|
||||
demographics_parent_education_level: 'declined',
|
||||
demographics_work_status: 'declined',
|
||||
demographics_work_status_description: '',
|
||||
demographics_current_work_sector: 'declined',
|
||||
demographics_future_work_sector: 'declined',
|
||||
demographics_user: 1,
|
||||
demographicsOptions: {
|
||||
actions: {
|
||||
POST: {
|
||||
gender: {
|
||||
choices: [
|
||||
{
|
||||
"value": "woman",
|
||||
"display_name": "Woman"
|
||||
},
|
||||
{
|
||||
"value": "man",
|
||||
"display_name": "Man"
|
||||
},
|
||||
{
|
||||
"value": "non-binary",
|
||||
"display_name": "Non-binary"
|
||||
},
|
||||
{
|
||||
"value": "self-describe",
|
||||
"display_name": "Prefer to self describe"
|
||||
},
|
||||
{
|
||||
"value": "declined",
|
||||
"display_name": "Prefer not to respond"
|
||||
}
|
||||
]
|
||||
},
|
||||
income: {
|
||||
choices: [
|
||||
{
|
||||
"value": "less-than-10k",
|
||||
"display_name": "Less than US $10,000"
|
||||
},
|
||||
{
|
||||
"value": "10k-25k",
|
||||
"display_name": "US $10,000 - $25,000"
|
||||
},
|
||||
{
|
||||
"value": "25k-50k",
|
||||
"display_name": "US $25,000 - $50,000"
|
||||
},
|
||||
{
|
||||
"value": "50k-75k",
|
||||
"display_name": "US $50,000 - $75,000"
|
||||
},
|
||||
{
|
||||
"value": "75k-100k",
|
||||
"display_name": "US $75,000 - $100,000"
|
||||
},
|
||||
{
|
||||
"value": "over-100k",
|
||||
"display_name": "Over US $100,000"
|
||||
},
|
||||
{
|
||||
"value": "unsure",
|
||||
"display_name": "I don't know"
|
||||
},
|
||||
{
|
||||
"value": "declined",
|
||||
"display_name": "Prefer not to respond"
|
||||
}
|
||||
]
|
||||
},
|
||||
learner_education_level: {
|
||||
choices: [
|
||||
{
|
||||
"value": "no-high-school",
|
||||
"display_name": "No High School"
|
||||
},
|
||||
{
|
||||
"value": "some-high-school",
|
||||
"display_name": "Some High School"
|
||||
},
|
||||
{
|
||||
"value": "high-school-ged-equivalent",
|
||||
"display_name": "High School diploma, GED, or equivalent"
|
||||
},
|
||||
{
|
||||
"value": "some-college",
|
||||
"display_name": "Some college, but no degree"
|
||||
},
|
||||
{
|
||||
"value": "associates",
|
||||
"display_name": "Associates degree"
|
||||
},
|
||||
{
|
||||
"value": "bachelors",
|
||||
"display_name": "Bachelors degree"
|
||||
},
|
||||
{
|
||||
"value": "masters",
|
||||
"display_name": "Masters degree"
|
||||
},
|
||||
{
|
||||
"value": "professional",
|
||||
"display_name": "Professional degree"
|
||||
},
|
||||
{
|
||||
"value": "doctorate",
|
||||
"display_name": "Doctorate degree"
|
||||
},
|
||||
{
|
||||
"value": "declined",
|
||||
"display_name": "Prefer not to respond"
|
||||
}
|
||||
]
|
||||
},
|
||||
parent_education_level: {
|
||||
choices: [
|
||||
{
|
||||
"value": "no-high-school",
|
||||
"display_name": "No High School"
|
||||
},
|
||||
{
|
||||
"value": "some-high-school",
|
||||
"display_name": "Some High School"
|
||||
},
|
||||
{
|
||||
"value": "high-school-ged-equivalent",
|
||||
"display_name": "High School diploma, GED, or equivalent"
|
||||
},
|
||||
{
|
||||
"value": "some-college",
|
||||
"display_name": "Some college, but no degree"
|
||||
},
|
||||
{
|
||||
"value": "associates",
|
||||
"display_name": "Associates degree"
|
||||
},
|
||||
{
|
||||
"value": "bachelors",
|
||||
"display_name": "Bachelors degree"
|
||||
},
|
||||
{
|
||||
"value": "masters",
|
||||
"display_name": "Masters degree"
|
||||
},
|
||||
{
|
||||
"value": "professional",
|
||||
"display_name": "Professional degree"
|
||||
},
|
||||
{
|
||||
"value": "doctorate",
|
||||
"display_name": "Doctorate degree"
|
||||
},
|
||||
{
|
||||
"value": "declined",
|
||||
"display_name": "Prefer not to respond"
|
||||
}
|
||||
]
|
||||
},
|
||||
military_history: {
|
||||
choices: [
|
||||
{
|
||||
"value": "never-served",
|
||||
"display_name": "Never served in the military"
|
||||
},
|
||||
{
|
||||
"value": "training",
|
||||
"display_name": "Only on active duty for training"
|
||||
},
|
||||
{
|
||||
"value": "active",
|
||||
"display_name": "Now on active duty"
|
||||
},
|
||||
{
|
||||
"value": "previously-active",
|
||||
"display_name": "On active duty in the past, but not now"
|
||||
},
|
||||
{
|
||||
"value": "declined",
|
||||
"display_name": "Prefer not to respond"
|
||||
}
|
||||
]
|
||||
},
|
||||
work_status: {
|
||||
choices: [
|
||||
{
|
||||
"value": "full-time",
|
||||
"display_name": "Employed, working full-time"
|
||||
},
|
||||
{
|
||||
"value": "part-time",
|
||||
"display_name": "Employed, working part-time"
|
||||
},
|
||||
{
|
||||
"value": "self-employed",
|
||||
"display_name": "Self-Employed"
|
||||
},
|
||||
{
|
||||
"value": "not-employed-looking",
|
||||
"display_name": "Not employed, looking for work"
|
||||
},
|
||||
{
|
||||
"value": "not-employed-not-looking",
|
||||
"display_name": "Not employed, not looking for work"
|
||||
},
|
||||
{
|
||||
"value": "unable",
|
||||
"display_name": "Unable to work"
|
||||
},
|
||||
{
|
||||
"value": "retired",
|
||||
"display_name": "Retired"
|
||||
},
|
||||
{
|
||||
"value": "other",
|
||||
"display_name": "Other"
|
||||
},
|
||||
{
|
||||
"value": "declined",
|
||||
"display_name": "Prefer not to respond"
|
||||
}
|
||||
]
|
||||
},
|
||||
current_work_sector: {
|
||||
choices: [
|
||||
{
|
||||
"value": "accommodation-food",
|
||||
"display_name": "Accommodation and Food Services"
|
||||
},
|
||||
{
|
||||
"value": "administrative-support-waste-remediation",
|
||||
"display_name": "Administrative and Support and Waste Management and Remediation Services"
|
||||
},
|
||||
{
|
||||
"value": "agriculture-forestry-fishing-hunting",
|
||||
"display_name": "Agriculture, Forestry, Fishing and Hunting"
|
||||
},
|
||||
{
|
||||
"value": "arts-entertainment-recreation",
|
||||
"display_name": "Arts, Entertainment, and Recreation"
|
||||
},
|
||||
{
|
||||
"value": "construction",
|
||||
"display_name": "Construction"
|
||||
},
|
||||
{
|
||||
"value": "educational",
|
||||
"display_name": "Education Services"
|
||||
},
|
||||
{
|
||||
"value": "finance-insurance",
|
||||
"display_name": "Finance and Insurance"
|
||||
},
|
||||
{
|
||||
"value": "healthcare-social",
|
||||
"display_name": "Health Care and Social Assistance"
|
||||
},
|
||||
{
|
||||
"value": "information",
|
||||
"display_name": "Information"
|
||||
},
|
||||
{
|
||||
"value": "management",
|
||||
"display_name": "Management of Companies and Enterprises"
|
||||
},
|
||||
{
|
||||
"value": "manufacturing",
|
||||
"display_name": "Manufacturing"
|
||||
},
|
||||
{
|
||||
"value": "mining-quarry-oil-gas",
|
||||
"display_name": "Mining, Quarrying, and Oil and Gas Extraction"
|
||||
},
|
||||
{
|
||||
"value": "professional-scientific-technical",
|
||||
"display_name": "Professional, Scientific, and Technical Services"
|
||||
},
|
||||
{
|
||||
"value": "public-admin",
|
||||
"display_name": "Public Administration"
|
||||
},
|
||||
{
|
||||
"value": "real-estate",
|
||||
"display_name": "Real Estate and Rental and Leasing"
|
||||
},
|
||||
{
|
||||
"value": "retail",
|
||||
"display_name": "Retail Trade"
|
||||
},
|
||||
{
|
||||
"value": "transport-warehousing",
|
||||
"display_name": "Transportation and Warehousing"
|
||||
},
|
||||
{
|
||||
"value": "utilities",
|
||||
"display_name": "Utilities"
|
||||
},
|
||||
{
|
||||
"value": "trade",
|
||||
"display_name": "Wholesale Trade"
|
||||
},
|
||||
{
|
||||
"value": "other",
|
||||
"display_name": "Other"
|
||||
},
|
||||
{
|
||||
"value": "declined",
|
||||
"display_name": "Prefer not to respond"
|
||||
}
|
||||
]
|
||||
},
|
||||
future_work_sector: {
|
||||
choices: [
|
||||
{
|
||||
"value": "accommodation-food",
|
||||
"display_name": "Accommodation and Food Services"
|
||||
},
|
||||
{
|
||||
"value": "administrative-support-waste-remediation",
|
||||
"display_name": "Administrative and Support and Waste Management and Remediation Services"
|
||||
},
|
||||
{
|
||||
"value": "agriculture-forestry-fishing-hunting",
|
||||
"display_name": "Agriculture, Forestry, Fishing and Hunting"
|
||||
},
|
||||
{
|
||||
"value": "arts-entertainment-recreation",
|
||||
"display_name": "Arts, Entertainment, and Recreation"
|
||||
},
|
||||
{
|
||||
"value": "construction",
|
||||
"display_name": "Construction"
|
||||
},
|
||||
{
|
||||
"value": "educational",
|
||||
"display_name": "Education Services"
|
||||
},
|
||||
{
|
||||
"value": "finance-insurance",
|
||||
"display_name": "Finance and Insurance"
|
||||
},
|
||||
{
|
||||
"value": "healthcare-social",
|
||||
"display_name": "Health Care and Social Assistance"
|
||||
},
|
||||
{
|
||||
"value": "information",
|
||||
"display_name": "Information"
|
||||
},
|
||||
{
|
||||
"value": "management",
|
||||
"display_name": "Management of Companies and Enterprises"
|
||||
},
|
||||
{
|
||||
"value": "manufacturing",
|
||||
"display_name": "Manufacturing"
|
||||
},
|
||||
{
|
||||
"value": "mining-quarry-oil-gas",
|
||||
"display_name": "Mining, Quarrying, and Oil and Gas Extraction"
|
||||
},
|
||||
{
|
||||
"value": "professional-scientific-technical",
|
||||
"display_name": "Professional, Scientific, and Technical Services"
|
||||
},
|
||||
{
|
||||
"value": "public-admin",
|
||||
"display_name": "Public Administration"
|
||||
},
|
||||
{
|
||||
"value": "real-estate",
|
||||
"display_name": "Real Estate and Rental and Leasing"
|
||||
},
|
||||
{
|
||||
"value": "retail",
|
||||
"display_name": "Retail Trade"
|
||||
},
|
||||
{
|
||||
"value": "transport-warehousing",
|
||||
"display_name": "Transportation and Warehousing"
|
||||
},
|
||||
{
|
||||
"value": "utilities",
|
||||
"display_name": "Utilities"
|
||||
},
|
||||
{
|
||||
"value": "trade",
|
||||
"display_name": "Wholesale Trade"
|
||||
},
|
||||
{
|
||||
"value": "other",
|
||||
"display_name": "Other"
|
||||
},
|
||||
{
|
||||
"value": "declined",
|
||||
"display_name": "Prefer not to respond"
|
||||
}
|
||||
]
|
||||
},
|
||||
user_ethnicity: {
|
||||
child: {
|
||||
children: {
|
||||
ethnicity: {
|
||||
choices: [
|
||||
{
|
||||
"value": "american-indian-or-alaska-native",
|
||||
"display_name": "American Indian or Alaska Native"
|
||||
},
|
||||
{
|
||||
"value": "asian",
|
||||
"display_name": "Asian"
|
||||
},
|
||||
{
|
||||
"value": "black-or-african-american",
|
||||
"display_name": "Black or African American"
|
||||
},
|
||||
{
|
||||
"value": "hispanic-latin-spanish",
|
||||
"display_name": "Hispanic, Latin, or Spanish origin"
|
||||
},
|
||||
{
|
||||
"value": "middle-eastern-or-north-african",
|
||||
"display_name": "Middle Eastern or North African"
|
||||
},
|
||||
{
|
||||
"value": "native-hawaiian-or-pacific-islander",
|
||||
"display_name": "Native Hawaiian or Other Pacific Islander"
|
||||
},
|
||||
{
|
||||
"value": "white",
|
||||
"display_name": "White"
|
||||
},
|
||||
{
|
||||
"value": "other",
|
||||
"display_name": "Some other race, ethnicity, or origin"
|
||||
},
|
||||
{
|
||||
"value": "declined",
|
||||
"display_name": "Prefer not to respond"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
updateDraft: jest.fn(),
|
||||
formValues: {
|
||||
demographics_gender: 'declined',
|
||||
demographics_gender_description: '',
|
||||
demographics_user_ethnicity: [],
|
||||
demographics_income: 'declined',
|
||||
demographics_military_history: 'declined',
|
||||
demographics_learner_education_level: 'declined',
|
||||
demographics_parent_education_level: 'declined',
|
||||
demographics_work_status: 'declined',
|
||||
demographics_work_status_description: '',
|
||||
demographics_current_work_sector: 'declined',
|
||||
demographics_future_work_sector: 'declined',
|
||||
demographics_user: 1,
|
||||
demographicsOptions: {
|
||||
actions: {
|
||||
POST: {
|
||||
gender: {
|
||||
choices: [
|
||||
{
|
||||
value: 'woman',
|
||||
display_name: 'Woman',
|
||||
},
|
||||
{
|
||||
value: 'man',
|
||||
display_name: 'Man',
|
||||
},
|
||||
{
|
||||
value: 'non-binary',
|
||||
display_name: 'Non-binary',
|
||||
},
|
||||
{
|
||||
value: 'self-describe',
|
||||
display_name: 'Prefer to self describe',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
income: {
|
||||
choices: [
|
||||
{
|
||||
value: 'less-than-10k',
|
||||
display_name: 'Less than US $10,000',
|
||||
},
|
||||
{
|
||||
value: '10k-25k',
|
||||
display_name: 'US $10,000 - $25,000',
|
||||
},
|
||||
{
|
||||
value: '25k-50k',
|
||||
display_name: 'US $25,000 - $50,000',
|
||||
},
|
||||
{
|
||||
value: '50k-75k',
|
||||
display_name: 'US $50,000 - $75,000',
|
||||
},
|
||||
{
|
||||
value: '75k-100k',
|
||||
display_name: 'US $75,000 - $100,000',
|
||||
},
|
||||
{
|
||||
value: 'over-100k',
|
||||
display_name: 'Over US $100,000',
|
||||
},
|
||||
{
|
||||
value: 'unsure',
|
||||
display_name: "I don't know",
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
learner_education_level: {
|
||||
choices: [
|
||||
{
|
||||
value: 'no-high-school',
|
||||
display_name: 'No High School',
|
||||
},
|
||||
{
|
||||
value: 'some-high-school',
|
||||
display_name: 'Some High School',
|
||||
},
|
||||
{
|
||||
value: 'high-school-ged-equivalent',
|
||||
display_name: 'High School diploma, GED, or equivalent',
|
||||
},
|
||||
{
|
||||
value: 'some-college',
|
||||
display_name: 'Some college, but no degree',
|
||||
},
|
||||
{
|
||||
value: 'associates',
|
||||
display_name: 'Associates degree',
|
||||
},
|
||||
{
|
||||
value: 'bachelors',
|
||||
display_name: 'Bachelors degree',
|
||||
},
|
||||
{
|
||||
value: 'masters',
|
||||
display_name: 'Masters degree',
|
||||
},
|
||||
{
|
||||
value: 'professional',
|
||||
display_name: 'Professional degree',
|
||||
},
|
||||
{
|
||||
value: 'doctorate',
|
||||
display_name: 'Doctorate degree',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
parent_education_level: {
|
||||
choices: [
|
||||
{
|
||||
value: 'no-high-school',
|
||||
display_name: 'No High School',
|
||||
},
|
||||
{
|
||||
value: 'some-high-school',
|
||||
display_name: 'Some High School',
|
||||
},
|
||||
{
|
||||
value: 'high-school-ged-equivalent',
|
||||
display_name: 'High School diploma, GED, or equivalent',
|
||||
},
|
||||
{
|
||||
value: 'some-college',
|
||||
display_name: 'Some college, but no degree',
|
||||
},
|
||||
{
|
||||
value: 'associates',
|
||||
display_name: 'Associates degree',
|
||||
},
|
||||
{
|
||||
value: 'bachelors',
|
||||
display_name: 'Bachelors degree',
|
||||
},
|
||||
{
|
||||
value: 'masters',
|
||||
display_name: 'Masters degree',
|
||||
},
|
||||
{
|
||||
value: 'professional',
|
||||
display_name: 'Professional degree',
|
||||
},
|
||||
{
|
||||
value: 'doctorate',
|
||||
display_name: 'Doctorate degree',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
military_history: {
|
||||
choices: [
|
||||
{
|
||||
value: 'never-served',
|
||||
display_name: 'Never served in the military',
|
||||
},
|
||||
{
|
||||
value: 'training',
|
||||
display_name: 'Only on active duty for training',
|
||||
},
|
||||
{
|
||||
value: 'active',
|
||||
display_name: 'Now on active duty',
|
||||
},
|
||||
{
|
||||
value: 'previously-active',
|
||||
display_name: 'On active duty in the past, but not now',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
work_status: {
|
||||
choices: [
|
||||
{
|
||||
value: 'full-time',
|
||||
display_name: 'Employed, working full-time',
|
||||
},
|
||||
{
|
||||
value: 'part-time',
|
||||
display_name: 'Employed, working part-time',
|
||||
},
|
||||
{
|
||||
value: 'self-employed',
|
||||
display_name: 'Self-Employed',
|
||||
},
|
||||
{
|
||||
value: 'not-employed-looking',
|
||||
display_name: 'Not employed, looking for work',
|
||||
},
|
||||
{
|
||||
value: 'not-employed-not-looking',
|
||||
display_name: 'Not employed, not looking for work',
|
||||
},
|
||||
{
|
||||
value: 'unable',
|
||||
display_name: 'Unable to work',
|
||||
},
|
||||
{
|
||||
value: 'retired',
|
||||
display_name: 'Retired',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Other',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
current_work_sector: {
|
||||
choices: [
|
||||
{
|
||||
value: 'accommodation-food',
|
||||
display_name: 'Accommodation and Food Services',
|
||||
},
|
||||
{
|
||||
value: 'administrative-support-waste-remediation',
|
||||
display_name: 'Administrative and Support and Waste Management and Remediation Services',
|
||||
},
|
||||
{
|
||||
value: 'agriculture-forestry-fishing-hunting',
|
||||
display_name: 'Agriculture, Forestry, Fishing and Hunting',
|
||||
},
|
||||
{
|
||||
value: 'arts-entertainment-recreation',
|
||||
display_name: 'Arts, Entertainment, and Recreation',
|
||||
},
|
||||
{
|
||||
value: 'construction',
|
||||
display_name: 'Construction',
|
||||
},
|
||||
{
|
||||
value: 'educational',
|
||||
display_name: 'Education Services',
|
||||
},
|
||||
{
|
||||
value: 'finance-insurance',
|
||||
display_name: 'Finance and Insurance',
|
||||
},
|
||||
{
|
||||
value: 'healthcare-social',
|
||||
display_name: 'Health Care and Social Assistance',
|
||||
},
|
||||
{
|
||||
value: 'information',
|
||||
display_name: 'Information',
|
||||
},
|
||||
{
|
||||
value: 'management',
|
||||
display_name: 'Management of Companies and Enterprises',
|
||||
},
|
||||
{
|
||||
value: 'manufacturing',
|
||||
display_name: 'Manufacturing',
|
||||
},
|
||||
{
|
||||
value: 'mining-quarry-oil-gas',
|
||||
display_name: 'Mining, Quarrying, and Oil and Gas Extraction',
|
||||
},
|
||||
{
|
||||
value: 'professional-scientific-technical',
|
||||
display_name: 'Professional, Scientific, and Technical Services',
|
||||
},
|
||||
{
|
||||
value: 'public-admin',
|
||||
display_name: 'Public Administration',
|
||||
},
|
||||
{
|
||||
value: 'real-estate',
|
||||
display_name: 'Real Estate and Rental and Leasing',
|
||||
},
|
||||
{
|
||||
value: 'retail',
|
||||
display_name: 'Retail Trade',
|
||||
},
|
||||
{
|
||||
value: 'transport-warehousing',
|
||||
display_name: 'Transportation and Warehousing',
|
||||
},
|
||||
{
|
||||
value: 'utilities',
|
||||
display_name: 'Utilities',
|
||||
},
|
||||
{
|
||||
value: 'trade',
|
||||
display_name: 'Wholesale Trade',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Other',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
future_work_sector: {
|
||||
choices: [
|
||||
{
|
||||
value: 'accommodation-food',
|
||||
display_name: 'Accommodation and Food Services',
|
||||
},
|
||||
{
|
||||
value: 'administrative-support-waste-remediation',
|
||||
display_name: 'Administrative and Support and Waste Management and Remediation Services',
|
||||
},
|
||||
{
|
||||
value: 'agriculture-forestry-fishing-hunting',
|
||||
display_name: 'Agriculture, Forestry, Fishing and Hunting',
|
||||
},
|
||||
{
|
||||
value: 'arts-entertainment-recreation',
|
||||
display_name: 'Arts, Entertainment, and Recreation',
|
||||
},
|
||||
{
|
||||
value: 'construction',
|
||||
display_name: 'Construction',
|
||||
},
|
||||
{
|
||||
value: 'educational',
|
||||
display_name: 'Education Services',
|
||||
},
|
||||
{
|
||||
value: 'finance-insurance',
|
||||
display_name: 'Finance and Insurance',
|
||||
},
|
||||
{
|
||||
value: 'healthcare-social',
|
||||
display_name: 'Health Care and Social Assistance',
|
||||
},
|
||||
{
|
||||
value: 'information',
|
||||
display_name: 'Information',
|
||||
},
|
||||
{
|
||||
value: 'management',
|
||||
display_name: 'Management of Companies and Enterprises',
|
||||
},
|
||||
{
|
||||
value: 'manufacturing',
|
||||
display_name: 'Manufacturing',
|
||||
},
|
||||
{
|
||||
value: 'mining-quarry-oil-gas',
|
||||
display_name: 'Mining, Quarrying, and Oil and Gas Extraction',
|
||||
},
|
||||
{
|
||||
value: 'professional-scientific-technical',
|
||||
display_name: 'Professional, Scientific, and Technical Services',
|
||||
},
|
||||
{
|
||||
value: 'public-admin',
|
||||
display_name: 'Public Administration',
|
||||
},
|
||||
{
|
||||
value: 'real-estate',
|
||||
display_name: 'Real Estate and Rental and Leasing',
|
||||
},
|
||||
{
|
||||
value: 'retail',
|
||||
display_name: 'Retail Trade',
|
||||
},
|
||||
{
|
||||
value: 'transport-warehousing',
|
||||
display_name: 'Transportation and Warehousing',
|
||||
},
|
||||
{
|
||||
value: 'utilities',
|
||||
display_name: 'Utilities',
|
||||
},
|
||||
{
|
||||
value: 'trade',
|
||||
display_name: 'Wholesale Trade',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Other',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
user_ethnicity: {
|
||||
child: {
|
||||
children: {
|
||||
ethnicity: {
|
||||
choices: [
|
||||
{
|
||||
value: 'american-indian-or-alaska-native',
|
||||
display_name: 'American Indian or Alaska Native',
|
||||
},
|
||||
{
|
||||
value: 'asian',
|
||||
display_name: 'Asian',
|
||||
},
|
||||
{
|
||||
value: 'black-or-african-american',
|
||||
display_name: 'Black or African American',
|
||||
},
|
||||
{
|
||||
value: 'hispanic-latin-spanish',
|
||||
display_name: 'Hispanic, Latin, or Spanish origin',
|
||||
},
|
||||
{
|
||||
value: 'middle-eastern-or-north-african',
|
||||
display_name: 'Middle Eastern or North African',
|
||||
},
|
||||
{
|
||||
value: 'native-hawaiian-or-pacific-islander',
|
||||
display_name: 'Native Hawaiian or Other Pacific Islander',
|
||||
},
|
||||
{
|
||||
value: 'white',
|
||||
display_name: 'White',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Some other race, ethnicity, or origin',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
formErrors: {},
|
||||
intl: {},
|
||||
};
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
patch: async () => ({
|
||||
data: { status: 200 },
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 1 }));
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
formErrors: {},
|
||||
intl: {},
|
||||
forwardRef: () => {},
|
||||
drafts: {},
|
||||
};
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
patch: async () => ({
|
||||
data: { status: 200 },
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 1 }));
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('should render', () => {
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render an Alert if an error occurs', () => {
|
||||
props = {
|
||||
...props,
|
||||
formErrors: {
|
||||
demographicsError: "api-error"
|
||||
}
|
||||
};
|
||||
it('should render an Alert if an error occurs', () => {
|
||||
props = {
|
||||
...props,
|
||||
formErrors: {
|
||||
demographicsError: 'api-error',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should set user input correctly when user provides gender self-description', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_gender: 'self-describe',
|
||||
demographics_gender_description: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should set user input correctly when user provides answers to work_status question', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_work_status: 'other',
|
||||
demographics_work_status_description: 'test',
|
||||
}
|
||||
}
|
||||
it('should set user input correctly when user provides gender self-description', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_gender: 'self-describe',
|
||||
demographics_gender_description: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render ethnicity text correctly', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_user_ethnicity: ['asian']
|
||||
}
|
||||
}
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('should set user input correctly when user provides answers to work_status question', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_work_status: 'other',
|
||||
demographics_work_status_description: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('should render ethnicity text correctly', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_user_ethnicity: ['asian'],
|
||||
},
|
||||
};
|
||||
|
||||
it('should render an Alert when demographicsOptions props are empty', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
demographicsOptions: ""
|
||||
}
|
||||
}
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,20 +2,58 @@
|
||||
|
||||
exports[`DemographicsSection should render 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does edX collect this information?
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
@@ -48,10 +86,9 @@ exports[`DemographicsSection should render 1`] = `
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -75,6 +112,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -112,10 +150,9 @@ exports[`DemographicsSection should render 1`] = `
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -139,13 +176,13 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="btn btn-link p-0"
|
||||
onBlur={[Function]}
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
@@ -184,10 +221,9 @@ exports[`DemographicsSection should render 1`] = `
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -211,6 +247,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -248,10 +285,9 @@ exports[`DemographicsSection should render 1`] = `
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -275,6 +311,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -312,10 +349,9 @@ exports[`DemographicsSection should render 1`] = `
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -339,6 +375,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -376,10 +413,9 @@ exports[`DemographicsSection should render 1`] = `
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -403,6 +439,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -440,10 +477,9 @@ exports[`DemographicsSection should render 1`] = `
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -467,6 +503,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -504,10 +541,9 @@ exports[`DemographicsSection should render 1`] = `
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -531,6 +567,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -568,10 +605,9 @@ exports[`DemographicsSection should render 1`] = `
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -595,6 +631,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -611,20 +648,58 @@ exports[`DemographicsSection should render 1`] = `
|
||||
|
||||
exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does edX collect this information?
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
@@ -635,9 +710,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<span>
|
||||
An error occurred attempting to retrieve or save your account information. Please try again later.
|
||||
</span>
|
||||
An error occurred attempting to retrieve or save your account information. Please try again later.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -671,10 +744,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -698,6 +770,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -735,10 +808,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -762,13 +834,13 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="btn btn-link p-0"
|
||||
onBlur={[Function]}
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
@@ -807,10 +879,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -834,6 +905,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -871,10 +943,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -898,6 +969,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -935,10 +1007,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -962,6 +1033,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -999,10 +1071,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -1026,6 +1097,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1063,10 +1135,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -1090,6 +1161,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1127,10 +1199,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -1154,6 +1225,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1191,10 +1263,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -1218,6 +1289,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1234,20 +1306,58 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
|
||||
exports[`DemographicsSection should render an Alert when demographicsOptions props are empty 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does edX collect this information?
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
@@ -1258,9 +1368,7 @@ exports[`DemographicsSection should render an Alert when demographicsOptions pro
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<span>
|
||||
An error occurred attempting to retrieve or save your account information. Please try again later.
|
||||
</span>
|
||||
An error occurred attempting to retrieve or save your account information. Please try again later.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1269,20 +1377,58 @@ exports[`DemographicsSection should render an Alert when demographicsOptions pro
|
||||
|
||||
exports[`DemographicsSection should render ethnicity correctly when multiple options are selected 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does edX collect this information?
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
@@ -1315,10 +1461,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -1342,6 +1487,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1379,10 +1525,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -1406,6 +1551,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Hispanic, Latin, or Spanish origin, White
|
||||
@@ -1443,10 +1589,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -1470,6 +1615,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1507,10 +1653,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -1534,6 +1679,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1571,10 +1717,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -1598,6 +1743,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1635,10 +1781,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -1662,6 +1807,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1699,10 +1845,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -1726,6 +1871,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1763,10 +1909,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -1790,6 +1935,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1827,10 +1973,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -1854,6 +1999,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1870,20 +2016,58 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
|
||||
exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does edX collect this information?
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
@@ -1916,10 +2100,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -1943,6 +2126,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1980,10 +2164,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -2007,6 +2190,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Asian
|
||||
@@ -2044,10 +2228,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -2071,6 +2254,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2108,10 +2292,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -2135,6 +2318,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2172,10 +2356,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -2199,6 +2382,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2236,10 +2420,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -2263,6 +2446,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2300,10 +2484,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -2327,6 +2510,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2364,10 +2548,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -2391,6 +2574,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2428,10 +2612,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -2455,6 +2638,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2471,20 +2655,58 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
|
||||
exports[`DemographicsSection should set user input correctly when user provides answers to work_status question 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does edX collect this information?
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
@@ -2517,10 +2739,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -2544,6 +2765,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2581,10 +2803,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -2608,13 +2829,13 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="btn btn-link p-0"
|
||||
onBlur={[Function]}
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
@@ -2653,10 +2874,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -2680,6 +2900,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2717,10 +2938,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -2744,6 +2964,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2781,10 +3002,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -2808,6 +3028,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2845,10 +3066,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -2872,6 +3092,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2909,10 +3130,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -2936,6 +3156,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Other: test
|
||||
@@ -2973,10 +3194,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -3000,6 +3220,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3037,10 +3258,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -3064,6 +3284,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3080,20 +3301,58 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
|
||||
exports[`DemographicsSection should set user input correctly when user provides gender self-description 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does edX collect this information?
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
@@ -3126,10 +3385,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -3153,6 +3411,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer to self describe: test
|
||||
@@ -3190,10 +3449,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -3217,13 +3475,13 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="btn btn-link p-0"
|
||||
onBlur={[Function]}
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
@@ -3262,10 +3520,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -3289,6 +3546,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3326,10 +3584,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -3353,6 +3610,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3390,10 +3648,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -3417,6 +3674,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3454,10 +3712,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -3481,6 +3738,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3518,10 +3776,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -3545,6 +3802,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3582,10 +3840,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -3609,6 +3866,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3646,10 +3904,9 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="btn ml-3 btn-link"
|
||||
onBlur={[Function]}
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -3673,6 +3930,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default } from './AccountSettingsPage';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
|
||||
203
src/account-settings/name-change/NameChange.jsx
Normal file
203
src/account-settings/name-change/NameChange.jsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import React, { useCallback, 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';
|
||||
|
||||
const 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);
|
||||
|
||||
const resetLocalState = useCallback(() => {
|
||||
setConfirmedWarning(false);
|
||||
dispatch(requestNameChangeReset());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setVerifiedNameInput(e.target.value);
|
||||
};
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
resetLocalState();
|
||||
dispatch(closeForm(targetFormId));
|
||||
dispatch(saveSettingsReset());
|
||||
}, [dispatch, resetLocalState, targetFormId]);
|
||||
|
||||
const 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?next=${encodeURIComponent('account/settings')}`);
|
||||
}
|
||||
}, [handleClose, push, 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));
|
||||
25
src/account-settings/name-change/data/actions.js
Normal file
25
src/account-settings/name-change/data/actions.js
Normal file
@@ -0,0 +1,25 @@
|
||||
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,
|
||||
});
|
||||
44
src/account-settings/name-change/data/reducers.js
Normal file
44
src/account-settings/name-change/data/reducers.js
Normal file
@@ -0,0 +1,44 @@
|
||||
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;
|
||||
40
src/account-settings/name-change/data/sagas.js
Normal file
40
src/account-settings/name-change/data/sagas.js
Normal file
@@ -0,0 +1,40 @@
|
||||
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);
|
||||
}
|
||||
17
src/account-settings/name-change/data/service.js
Normal file
17
src/account-settings/name-change/data/service.js
Normal file
@@ -0,0 +1,17 @@
|
||||
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;
|
||||
}
|
||||
5
src/account-settings/name-change/index.js
Normal file
5
src/account-settings/name-change/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
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';
|
||||
56
src/account-settings/name-change/messages.js
Normal file
56
src/account-settings/name-change/messages.js
Normal file
@@ -0,0 +1,56 @@
|
||||
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 unexpired student, work, or government-issued identification card.',
|
||||
description: 'Form label instructing the user to enter the name on their ID.',
|
||||
},
|
||||
'account.settings.name.change.id.name.placeholder': {
|
||||
id: 'account.settings.name.change.id.name.placeholder',
|
||||
defaultMessage: 'Enter the name on your photo 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 doesn’t 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;
|
||||
173
src/account-settings/name-change/test/NameChange.test.jsx
Normal file
173
src/account-settings/name-change/test/NameChange.test.jsx
Normal file
@@ -0,0 +1,173 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
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 photo 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 photo 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 photo 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 photo 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 photo 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');
|
||||
});
|
||||
});
|
||||
@@ -5,20 +5,17 @@ import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import Alert from '../Alert';
|
||||
|
||||
const RequestInProgressAlert = (props) => {
|
||||
|
||||
return (
|
||||
<Alert
|
||||
className="alert-warning mt-n2"
|
||||
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="account.settings.editable.field.password.reset.button.forbidden"
|
||||
defaultMessage="Your previous request is in progress, please try again in few moments."
|
||||
description="A message displayed when a previous password reset request is still in progress."
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
const RequestInProgressAlert = () => (
|
||||
<Alert
|
||||
className="alert-warning mt-n2"
|
||||
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="account.settings.editable.field.password.reset.button.forbidden"
|
||||
defaultMessage="Your previous request is in progress, please try again in few moments."
|
||||
description="A message displayed when a previous password reset request is still in progress."
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
export default RequestInProgressAlert;
|
||||
|
||||
@@ -22,7 +22,7 @@ const ResetPassword = (props) => {
|
||||
</h6>
|
||||
<p>
|
||||
<StatefulButton
|
||||
className="btn-link"
|
||||
variant="link"
|
||||
state={status}
|
||||
onClick={(e) => {
|
||||
// Swallow clicks if the state is pending.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { put, call, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
import { resetPasswordBegin, resetPasswordForbidden, resetPasswordSuccess, RESET_PASSWORD } from './actions';
|
||||
import {
|
||||
resetPasswordBegin, resetPasswordForbidden, resetPasswordSuccess, RESET_PASSWORD,
|
||||
} from './actions';
|
||||
import { postResetPassword } from './service';
|
||||
|
||||
function* handleResetPassword(action) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default } from './ResetPassword';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { RESET_PASSWORD } from './data/actions';
|
||||
|
||||
@@ -12,9 +12,8 @@ export const siteLanguageListSelector = createSelector(
|
||||
|
||||
export const siteLanguageOptionsSelector = createSelector(
|
||||
siteLanguageSelector,
|
||||
siteLanguage =>
|
||||
siteLanguage.siteLanguageList.map(({ code, name }) => ({
|
||||
value: code,
|
||||
label: name,
|
||||
})),
|
||||
siteLanguage => siteLanguage.siteLanguageList.map(({ code, name }) => ({
|
||||
value: code,
|
||||
label: name,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { convertKeyNames, snakeCaseObject } from '@edx/frontend-platform/utils';
|
||||
import siteLanguageList from './constants';
|
||||
import { snakeCaseObject, convertKeyNames } from '../data/utils';
|
||||
|
||||
export async function getSiteLanguageList() {
|
||||
return siteLanguageList;
|
||||
|
||||
@@ -2,61 +2,61 @@ import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import JumpNav from "../JumpNav";
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { mergeConfig, setConfig } from '@edx/frontend-platform';
|
||||
import JumpNav from '../JumpNav';
|
||||
|
||||
const IntlJumpNav = injectIntl(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();
|
||||
mergeConfig({
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION: false,
|
||||
});
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
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,
|
||||
});
|
||||
|
||||
it('should render Optional Information link', () => {
|
||||
setConfig({
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION: true,
|
||||
});
|
||||
props = {
|
||||
...props,
|
||||
displayDemographicsLink: true,
|
||||
};
|
||||
|
||||
props = {
|
||||
...props,
|
||||
displayDemographicsLink: true,
|
||||
}
|
||||
const tree = renderer.create((
|
||||
// Same as previous test
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IntlJumpNav {...props} />
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
))
|
||||
.toJSON();
|
||||
|
||||
const tree = renderer.create((
|
||||
// Same as previous test
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IntlJumpNav {...props} />
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
))
|
||||
.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
<div
|
||||
className="jump-nav"
|
||||
className="jump-nav jump-nav-sm position-sticky pt-3"
|
||||
>
|
||||
<ul
|
||||
className="list-unstyled"
|
||||
@@ -12,11 +12,9 @@ exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
aria-current={null}
|
||||
href="/#basic-information"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Account Information
|
||||
</a>
|
||||
@@ -25,11 +23,9 @@ exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
aria-current={null}
|
||||
href="/#profile-information"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Profile Information
|
||||
</a>
|
||||
@@ -38,11 +34,9 @@ exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
aria-current={null}
|
||||
href="/#social-media"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Social Media Links
|
||||
</a>
|
||||
@@ -51,11 +45,9 @@ exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
aria-current={null}
|
||||
href="/#site-preferences"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Site Preferences
|
||||
</a>
|
||||
@@ -64,11 +56,9 @@ exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
aria-current={null}
|
||||
href="/#linked-accounts"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Linked Accounts
|
||||
</a>
|
||||
@@ -77,11 +67,9 @@ exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
aria-current={null}
|
||||
href="/#delete-account"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Delete My Account
|
||||
</a>
|
||||
@@ -92,7 +80,7 @@ exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
|
||||
exports[`JumpNav should render Optional Information link 1`] = `
|
||||
<div
|
||||
className="jump-nav"
|
||||
className="jump-nav jump-nav-sm position-sticky pt-3"
|
||||
>
|
||||
<ul
|
||||
className="list-unstyled"
|
||||
@@ -102,11 +90,9 @@ exports[`JumpNav should render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
aria-current={null}
|
||||
href="/#basic-information"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Account Information
|
||||
</a>
|
||||
@@ -115,11 +101,9 @@ exports[`JumpNav should render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
aria-current={null}
|
||||
href="/#profile-information"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Profile Information
|
||||
</a>
|
||||
@@ -128,11 +112,9 @@ exports[`JumpNav should render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
aria-current={null}
|
||||
href="/#demographics-information"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Optional Information
|
||||
</a>
|
||||
@@ -141,11 +123,9 @@ exports[`JumpNav should render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
aria-current={null}
|
||||
href="/#social-media"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Social Media Links
|
||||
</a>
|
||||
@@ -154,11 +134,9 @@ exports[`JumpNav should render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
aria-current={null}
|
||||
href="/#site-preferences"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Site Preferences
|
||||
</a>
|
||||
@@ -167,11 +145,9 @@ exports[`JumpNav should render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
aria-current={null}
|
||||
href="/#linked-accounts"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Linked Accounts
|
||||
</a>
|
||||
@@ -180,11 +156,9 @@ exports[`JumpNav should render Optional Information link 1`] = `
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
aria-current={null}
|
||||
href="/#delete-account"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Delete My Account
|
||||
</a>
|
||||
|
||||
@@ -11,14 +11,16 @@ class ThirdPartyAuth extends Component {
|
||||
onClickDisconnect = (e) => {
|
||||
e.preventDefault();
|
||||
const providerId = e.currentTarget.getAttribute('data-provider-id');
|
||||
if (this.props.disconnectionStatuses[providerId] === 'pending') return;
|
||||
if (this.props.disconnectionStatuses[providerId] === 'pending') {
|
||||
return;
|
||||
}
|
||||
const disconnectUrl = e.currentTarget.getAttribute('data-disconnect-url');
|
||||
this.props.disconnectAuth(disconnectUrl, providerId);
|
||||
}
|
||||
};
|
||||
|
||||
renderUnconnectedProvider(url, name) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<h6 aria-level="3">{name}</h6>
|
||||
<Hyperlink destination={url} className="btn btn-outline-primary">
|
||||
<FormattedMessage
|
||||
@@ -28,7 +30,7 @@ class ThirdPartyAuth extends Component {
|
||||
values={{ name }}
|
||||
/>
|
||||
</Hyperlink>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +38,7 @@ class ThirdPartyAuth extends Component {
|
||||
const hasError = this.props.errors[id];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
<h6 aria-level="3">
|
||||
{name}
|
||||
<span className="small font-weight-normal text-muted ml-2">
|
||||
@@ -58,7 +60,7 @@ class ThirdPartyAuth extends Component {
|
||||
) : null}
|
||||
|
||||
<StatefulButton
|
||||
className="btn-link"
|
||||
variant="link"
|
||||
state={this.props.disconnectionStatuses[id]}
|
||||
labels={{
|
||||
default: (
|
||||
@@ -75,7 +77,7 @@ class ThirdPartyAuth extends Component {
|
||||
data-disconnect-url={url}
|
||||
data-provider-id={id}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,9 +87,9 @@ class ThirdPartyAuth extends Component {
|
||||
return (
|
||||
<div className="form-group" key={id}>
|
||||
{
|
||||
connected ?
|
||||
this.renderConnectedProvider(disconnectUrl, name, id) :
|
||||
this.renderUnconnectedProvider(connectUrl, name)
|
||||
connected
|
||||
? this.renderConnectedProvider(disconnectUrl, name, id)
|
||||
: this.renderUnconnectedProvider(connectUrl, name)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
@@ -98,13 +100,15 @@ class ThirdPartyAuth extends Component {
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.no.providers"
|
||||
defaultMessage="No accounts can be linked at this time."
|
||||
description="Displayed when no third party accounts are available to link an edX account to"
|
||||
description="Displayed when no third-party accounts are available for the user to link to their account on the platform."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.providers === undefined) return null;
|
||||
if (this.props.providers === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.props.providers.length === 0) {
|
||||
return this.renderNoProviders();
|
||||
@@ -114,7 +118,6 @@ class ThirdPartyAuth extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ThirdPartyAuth.propTypes = {
|
||||
providers: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export { default } from './ThirdPartyAuth';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
4
src/constants.js
Normal file
4
src/constants.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const IDLE_STATUS = 'idle';
|
||||
export const LOADING_STATUS = 'loading';
|
||||
export const SUCCESS_STATUS = 'success';
|
||||
export const FAILURE_STATUS = 'failure';
|
||||
21
src/head/Head.jsx
Normal file
21
src/head/Head.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const Head = ({ intl }) => (
|
||||
<Helmet>
|
||||
<title>
|
||||
{intl.formatMessage(messages['account.page.title'], { siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
);
|
||||
|
||||
Head.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(Head);
|
||||
17
src/head/Head.test.jsx
Normal file
17
src/head/Head.test.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { mount } from 'enzyme';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import Head from './Head';
|
||||
|
||||
describe('Head', () => {
|
||||
const props = {};
|
||||
it('should match render title tag and fivicon with the site configuration values', () => {
|
||||
mount(<IntlProvider locale="en"><Head {...props} /></IntlProvider>);
|
||||
const helmet = Helmet.peek();
|
||||
expect(helmet.title).toEqual(`Account | ${getConfig().SITE_NAME}`);
|
||||
expect(helmet.linkTags[0].rel).toEqual('shortcut icon');
|
||||
expect(helmet.linkTags[0].href).toEqual(getConfig().FAVICON_URL);
|
||||
});
|
||||
});
|
||||
11
src/head/messages.js
Normal file
11
src/head/messages.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.page.title': {
|
||||
id: 'account.page.title',
|
||||
defaultMessage: 'Account | {siteName}',
|
||||
description: 'Title tag',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
55
src/hooks.js
Normal file
55
src/hooks.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
IDLE_STATUS, LOADING_STATUS, SUCCESS_STATUS, FAILURE_STATUS,
|
||||
} from './constants';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function useAsyncCall(asyncFunc) {
|
||||
// React doesn't batch setStates call in async useEffect hooks,
|
||||
// so we use a combined object here to ensure that users
|
||||
// re-render once.
|
||||
const [data, setData] = useState({ status: IDLE_STATUS });
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
(async () => {
|
||||
setData(currData => ({ ...currData, status: LOADING_STATUS }));
|
||||
const response = await asyncFunc();
|
||||
|
||||
if (Object.keys(response).length === 0) {
|
||||
setData(currData => ({ ...currData, status: FAILURE_STATUS, data: response }));
|
||||
} else {
|
||||
setData(currData => ({ ...currData, status: SUCCESS_STATUS, data: response }));
|
||||
}
|
||||
})();
|
||||
},
|
||||
[asyncFunc],
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Redirect the user to their original location based on session storage
|
||||
export function useRedirect() {
|
||||
const [redirect, setRedirect] = useState({
|
||||
location: 'dashboard',
|
||||
text: 'id.verification.return.dashboard',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionStorage.getItem('courseId')) {
|
||||
setRedirect({
|
||||
location: `courses/${sessionStorage.getItem('courseId')}`,
|
||||
text: 'id.verification.return.course',
|
||||
});
|
||||
} else if (sessionStorage.getItem('next')) {
|
||||
setRedirect({
|
||||
location: sessionStorage.getItem('next'),
|
||||
text: 'id.verification.return.generic',
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return redirect;
|
||||
}
|
||||
@@ -1,31 +1,27 @@
|
||||
import arMessages from './messages/ar.json';
|
||||
import caMessages from './messages/ca.json';
|
||||
// no need to import en messages-- they are in the defaultMessage field
|
||||
import es419Messages from './messages/es_419.json';
|
||||
import frMessages from './messages/fr.json';
|
||||
import es419Messages from './messages/es_419.json';
|
||||
import zhcnMessages from './messages/zh_CN.json';
|
||||
import heMessages from './messages/he.json';
|
||||
import idMessages from './messages/id.json';
|
||||
import kokrMessages from './messages/ko_kr.json';
|
||||
import plMessages from './messages/pl.json';
|
||||
import ptbrMessages from './messages/pt_br.json';
|
||||
import ruMessages from './messages/ru.json';
|
||||
import thMessages from './messages/th.json';
|
||||
import ptMessages from './messages/pt.json';
|
||||
import itMessages from './messages/it.json';
|
||||
import ukMessages from './messages/uk.json';
|
||||
import deMessages from './messages/de.json';
|
||||
import ruMessages from './messages/ru.json';
|
||||
import hiMessages from './messages/hi.json';
|
||||
import frCAMessages from './messages/fr_CA.json';
|
||||
// no need to import en messages-- they are in the defaultMessage field
|
||||
|
||||
const messages = {
|
||||
ar: arMessages,
|
||||
'es-419': es419Messages,
|
||||
fr: frMessages,
|
||||
'zh-cn': zhcnMessages,
|
||||
ca: caMessages,
|
||||
he: heMessages,
|
||||
id: idMessages,
|
||||
'ko-kr': kokrMessages,
|
||||
pl: plMessages,
|
||||
'pt-br': ptbrMessages,
|
||||
pt: ptMessages,
|
||||
it: itMessages,
|
||||
de: deMessages,
|
||||
hi: hiMessages,
|
||||
'fr-ca': frCAMessages,
|
||||
ru: ruMessages,
|
||||
th: thMessages,
|
||||
uk: ukMessages,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,292 +1,361 @@
|
||||
{
|
||||
"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.support": "support",
|
||||
"account.settings.page.heading": "Account Settings",
|
||||
"account.settings.loading.message": "Loading...",
|
||||
"account.settings.loading.error": "Error: {error}",
|
||||
"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": "Switch Back to {previous_language}",
|
||||
"account.settings.banner.beta.language.action.help.translate": "Help Translate into {beta_language}",
|
||||
"account.settings.section.account.information": "Account Information",
|
||||
"account.settings.section.account.information.description": "These settings include basic information about your account.",
|
||||
"account.settings.section.profile.information": "Profile Information",
|
||||
"account.settings.section.demographics.information": "Optional Information",
|
||||
"account.settings.section.site.preferences": "Site Preferences",
|
||||
"account.settings.section.linked.accounts": "Linked Accounts",
|
||||
"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.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.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.email": "Email address (Sign in)",
|
||||
"account.settings.field.email.empty": "Add email address",
|
||||
"account.settings.field.email.confirmation": "We’ve 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 edX and course teams at this 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.confirmation": "We’ve sent a confirmation message to {value}. Click the link in the message to update your recovery email address.",
|
||||
"account.settings.email.field.confirmation.header": "Pending confirmation",
|
||||
"account.settings.field.dob": "Year of birth",
|
||||
"account.settings.field.dob.empty": "Add year of birth",
|
||||
"account.settings.field.year_of_birth.options.empty": "Select a year of birth",
|
||||
"account.settings.field.country": "Country",
|
||||
"account.settings.field.country.empty": "Add country",
|
||||
"account.settings.field.country.options.empty": "Select a Country",
|
||||
"account.settings.field.state": "State",
|
||||
"account.settings.field.state.empty": "Add state",
|
||||
"account.settings.field.state.options.empty": "Select a State",
|
||||
"account.settings.field.site.language": "Site language",
|
||||
"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.education": "Education",
|
||||
"account.settings.field.education.empty": "Add level of education",
|
||||
"account.settings.field.education.levels.empty": "Select a level of education",
|
||||
"account.settings.field.education.levels.p": "Doctorate",
|
||||
"account.settings.field.education.levels.m": "Master's or professional degree",
|
||||
"account.settings.field.education.levels.b": "Bachelor's Degree",
|
||||
"account.settings.field.education.levels.a": "Associate's degree",
|
||||
"account.settings.field.education.levels.hs": "Secondary/high school",
|
||||
"account.settings.field.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"account.settings.field.education.levels.el": "Elementary/primary school",
|
||||
"account.settings.field.education.levels.none": "No formal education",
|
||||
"account.settings.field.education.levels.o": "Other education",
|
||||
"account.settings.field.gender": "Gender",
|
||||
"account.settings.field.gender.empty": "Add gender",
|
||||
"account.settings.field.gender.options.empty": "Select a gender",
|
||||
"account.settings.field.gender.options.f": "Female",
|
||||
"account.settings.field.gender.options.m": "Male",
|
||||
"account.settings.field.gender.options.o": "Other",
|
||||
"account.settings.field.language.proficiencies": "Spoken languages",
|
||||
"account.settings.field.language.proficiencies.empty": "Add a spoken language",
|
||||
"account.settings.field.language_proficiencies.options.empty": "Select a Language",
|
||||
"account.settings.field.time.zone": "Time zone",
|
||||
"account.settings.field.time.zone.empty": "Set time zone",
|
||||
"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 browser’s local time zone.",
|
||||
"account.settings.field.time.zone.default": "Default (Local Time Zone)",
|
||||
"account.settings.field.time.zone.all": "All time zones",
|
||||
"account.settings.field.time.zone.country": "Country time zones",
|
||||
"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 edX profile.",
|
||||
"account.settings.field.social.platform.name.linkedin": "LinkedIn",
|
||||
"account.settings.field.social.platform.name.linkedin.empty": "Add LinkedIn profile",
|
||||
"account.settings.jump.nav.delete.account": "Delete My Account",
|
||||
"account.settings.field.social.platform.name.twitter": "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.empty": "Add Facebook profile",
|
||||
"account.settings.editable.field.action.save": "Save",
|
||||
"account.settings.editable.field.action.cancel": "Cancel",
|
||||
"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.admin": "No value set.",
|
||||
"account.settings.coaching.consent.welcome.header": "Let’s get started.",
|
||||
"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 you’re 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.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.coaching.consent.accept-coaching": "Sign up for coaching",
|
||||
"account.settings.coaching.consent.decline-coaching": "I prefer not to be contacted with free coaching services",
|
||||
"account.settings.coaching.consent.label.name": "Please confirm your name",
|
||||
"account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
|
||||
"account.settings.coaching.consent.success.header": "Success!",
|
||||
"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": "Start my course",
|
||||
"account.settings.coaching.managed.support": "support",
|
||||
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
|
||||
"account.settings.field.phone_number": "Phone Number",
|
||||
"account.settings.field.phone_number.empty": "Add a phone number",
|
||||
"account.settings.field.coaching_consent": "Coaching consent",
|
||||
"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": "A valid US phone number is required to opt into coaching",
|
||||
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
|
||||
"account.settings.delete.account.header": "Delete My Account",
|
||||
"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. 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 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.text.3.link": "follow the 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.change.instead": "Want to change your email, name, or password instead?",
|
||||
"account.settings.delete.account.button": "Delete My 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.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. 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 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.confirm.delete": "Yes, Delete",
|
||||
"account.settings.delete.account.modal.confirm.cancel": "Cancel",
|
||||
"account.settings.delete.account.error.unable.to.delete": "Unable to delete account",
|
||||
"account.settings.delete.account.error.no.password": "A password is required",
|
||||
"account.settings.delete.account.error.invalid.password": "Password is incorrect",
|
||||
"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.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.button": "Close",
|
||||
"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.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.empty": "Add gender identity",
|
||||
"account.settings.field.demographics.gender.options.empty": "Select a gender identity",
|
||||
"account.settings.field.demographics.gender_description": "Gender identity description",
|
||||
"account.settings.field.demographics.gender_description.empty": "Enter description",
|
||||
"account.settings.field.demographics.ethnicity": "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.income": "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.military_history": "U.S. 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.learner_education_level": "Your 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.empty": "Add education level",
|
||||
"account.settings.field.demographics.education_level.options.empty": "Select education level",
|
||||
"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_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.section.demographics.why": "Why does edX collect this information?",
|
||||
"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": "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": "Reset Password",
|
||||
"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": "Password",
|
||||
"account.settings.sso.link.account": "Sign in with {name}",
|
||||
"account.settings.sso.account.connected": "Linked",
|
||||
"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.no.providers": "No accounts can be linked at this time.",
|
||||
"id.verification.existing.request.denied.text": "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.requirements.title": "Photo Verification Requirements",
|
||||
"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.allow": "Allow",
|
||||
"id.verification.requirements.card.id.title": "Photo Identification",
|
||||
"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.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.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 edX after the verification process is complete.",
|
||||
"id.verification.existing.request.title": "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 5 days).",
|
||||
"id.verification.photo.take": "Take 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.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.enable": "Enable Camera",
|
||||
"id.verification.camera.access.problems": "Having problems?",
|
||||
"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.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.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.list.title": "Photo Tips",
|
||||
"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.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.upload": "Upload a Photo of Yourself",
|
||||
"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.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.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.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.difficulty.question.portrait": "What if I have difficulty holding my head in position relative to the camera?",
|
||||
"id.verification.camera.help.difficulty.question.id": "What if I have difficulty holding my ID in position relative to the camera?",
|
||||
"id.verification.camera.help.difficulty.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
|
||||
"id.verification.id.photo.unclear.question": "Is your ID image not clear or too blurry?",
|
||||
"id.verification.id.tips.title": "Helpful ID Tips",
|
||||
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid ID that includes your name. Please have your ID ready.",
|
||||
"id.verification.id.tips.list.well.lit": "Your ID is well-lit.",
|
||||
"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 ID",
|
||||
"id.verification.id.photo.title.upload": "Upload a Photo of Your 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.",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. (Supported formats: .jpg, .jpeg, .png)",
|
||||
"id.verification.id.photo.instructions.upload.error": "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.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.account.name.radio.label": "Does the name on your ID match the Account Name below?",
|
||||
"id.verification.account.name.radio.yes": "Yes",
|
||||
"id.verification.account.name.radio.no": "No",
|
||||
"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.settings": "Account Settings",
|
||||
"id.verification.account.name.label": "Account Name",
|
||||
"id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
|
||||
"id.verification.account.name.save": "Save and Next",
|
||||
"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.portrait.label": "Your Portrait",
|
||||
"id.verification.review.portrait.alt": "Photo of your face to be submitted.",
|
||||
"id.verification.review.portrait.retake": "Retake Portrait Photo",
|
||||
"id.verification.review.id.label": "Your Photo ID",
|
||||
"id.verification.review.id.alt": "Photo of your ID to be submitted.",
|
||||
"id.verification.review.id.retake": "Retake ID Photo",
|
||||
"id.verification.review.confirm": "Submit",
|
||||
"id.verification.review.error": "edX Support Page",
|
||||
"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.return.dashboard": "Return to Your Dashboard",
|
||||
"id.verification.return.course": "Return to Course",
|
||||
"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.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}.",
|
||||
"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,\n please go to {support_link} for help.\n ",
|
||||
"id.verification.account.name.edit": "Edit{sr}"
|
||||
"account.settings.message.duplicate.tpa.provider": "حساب {provider} الذي حددته موصول من قبل بحساب آخر على {siteName}.",
|
||||
"account.settings.message.managed.settings": "إعدادات ملفك الشخصي يديرها {ManagerTitle}. اتصل بمديرك أو ب{support} للحصول على المساعدة.",
|
||||
"account.settings.message.managed.settings.support": "الدعم",
|
||||
"account.settings.page.heading": "إعدادات الحساب",
|
||||
"account.settings.loading.message": "التحميل جارٍ...",
|
||||
"account.settings.loading.error": "خطأ: {error}",
|
||||
"account.settings.banner.beta.language": "لقد قمت بضبط لغتك على {beta_language}، وهي حاليا غير مترجمة بالكامل. يمكنك مساعدتنا في إتمام ترجمة هذه اللغة من خلال الانضمام إلى مجتمع Transifex وإضافة ترجمات من الإنجليزية للمتعلمين الذين يتحدثون {beta_language}.",
|
||||
"account.settings.banner.beta.language.action.switch.back": "العودة إلى {previous_language}",
|
||||
"account.settings.banner.beta.language.action.help.translate": "ساعد في الترجمة إلى {beta_language}",
|
||||
"account.settings.section.account.information": "معلومات الحساب",
|
||||
"account.settings.section.account.information.description": "تتضمن هذه الإعدادات معلومات أساسية عن حسابك.",
|
||||
"account.settings.section.profile.information": "معلومات الملف الشخصي",
|
||||
"account.settings.section.demographics.information": "معلومات اختيارية",
|
||||
"account.settings.section.site.preferences": "تفضيلات الموقع",
|
||||
"account.settings.section.linked.accounts": "الحسابات الموصولة",
|
||||
"account.settings.section.linked.accounts.description": "يمكنك وصل حسابات هويتك لتبسيط تسجيل دخولك إلى {siteName}.",
|
||||
"account.settings.field.username": "اسم المستخدم",
|
||||
"account.settings.field.username.help.text": "الاسم الذي يعرّفك على {siteName}. لا يمكنك تغيير اسم المستخدم الخاص بك.",
|
||||
"account.settings.field.full.name": "الاسم الكامل",
|
||||
"account.settings.field.full.name.empty": "إضافة الاسم",
|
||||
"account.settings.field.full.name.help.text": "الاسم المستعمل للتحقق من هويتك والذي يظهر في شهاداتك.",
|
||||
"account.settings.field.full.name.help.text.default": "الاسم الذي يظهر في ملفك الشخصي العامّ.",
|
||||
"account.settings.field.full.name.help.text.default.certificate": "سيظهر هذا الاسم في شهاداتك و سجلّاتك العامّة.",
|
||||
"account.settings.field.name.verified": "اسم متحقَّق منه",
|
||||
"account.settings.field.name.verified.help.text.verified": "تم التحقق من هذا الاسم عن طريق بطاقة تعريف ذات صورة.",
|
||||
"account.settings.field.name.verified.help.text.verified.proctored": "تم التحقق من هذا الاسم عن طريق المراقبة.",
|
||||
"account.settings.field.name.verified.help.text.verified.certificate": "تم التحقق من هذا الاسم عن طريق بطاقة هوية ذات صورة، وتم اختياره للظهور في الشهادات والسجلات العامة.",
|
||||
"account.settings.field.name.verified.help.text.verified.proctored.certificate": "تم التحقق من هذا الاسم من خلال المراقبة، وتم تحديده للظهور في الشهادات والسجلات العامة.",
|
||||
"account.settings.field.name.verified.help.text.submitted": "تم تسليم طلب التحقق. في العادة يستغرق هذا 48 ساعة أو أقل. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت.",
|
||||
"account.settings.field.name.verified.help.text.submitted.proctored": "تم تسليم امتحانك المراقَب. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت. رجاءً أعد التفقد بعد 2-5 أيام.",
|
||||
"account.settings.field.name.verified.help.text.submitted.certificate": "عند نجاح التحقق من الهوية، سيظهر هذا الاسم على شهاداتك و سجلاتك العامة. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت.",
|
||||
"account.settings.field.name.verified.help.text.submitted.proctored.certificate": "بمجرد اجتياز امتحانك المراقب للمراجعة، سيظهر هذا الاسم في شهادتك و سجلاتك العامة. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت.",
|
||||
"account.settings.field.name.verified.verification.help": "أدخل اسمك كما يظهر في بطاقة تعريف الطالب أو الموظف الخاصة بك، أو بطاقة تعريفك الصادرة عن الحكومة.",
|
||||
"account.settings.field.full.name.help.text.submitted": "تم تسليم طلب التحقق. يستغرق هذا عادةً 48 ساعة أو أقل. لا يمكن تغيير الاسم الكامل في هذا الوقت.",
|
||||
"account.settings.field.full.name.help.text.submitted.proctored": "تم تسليم امتحانك المراقب. لا يمكن تغيير الاسم الكامل في هذا الوقت. يرجى التحقق مرة أخرى خلال 2-5 أيام.",
|
||||
"account.settings.field.full.name.help.text.submitted.certificate": "عند نجاح التحقق من الهوية، سيظهر هذا الاسم على الشهادات والسجلات العامة. لا يمكن تغيير الاسم الكامل في هذا الوقت.",
|
||||
"account.settings.field.full.name.help.text.submitted.proctored.certificate": "بمجرد اجتياز امتحانك المراقب للمراجعة، سيظهر هذا الاسم في شهاداتك وسجلاتك العامة. لا يمكن تغيير الاسم الكامل في هذا الوقت.",
|
||||
"account.settings.field.name.verified.success.message": "اكتمل طلب التحقق من هويتك بنجاح. لديك الآن خيار تحديد الاسم الذي تفضل ظهوره على شهاداتك وسجلاتك العامة.",
|
||||
"account.settings.field.name.verified.success.message.header": "اكتمل طلب تغيير اسمك!",
|
||||
"account.settings.field.name.verified.failure.message": "لم تنجح آخر محاولاتك للتحقق من هويتك. تم إرجاع إعدادات الحساب ذات الصلة إلى وضعها السابق.",
|
||||
"account.settings.field.name.verified.failure.message.header": "لم نستطع التحقق من هويتك.",
|
||||
"account.settings.field.name.verified.failure.message.help.link": "اعرف المزيد عن التحقق من الهوية.",
|
||||
"account.settings.field.name.verified.submitted.message": "تم تسليم طلب التحقق من هويتك و عادة ما يستغرق إكماله ما بين 24 و 48 ساعة.",
|
||||
"account.settings.field.name.verified.submitted.message.certificate": "عند الموافقة على طلبك، سيظهر اسمك المحدَّث على جميع الشهادات السجلات العامة ذات الصلة.",
|
||||
"account.settings.field.name.verified.submitted.message.header": "طلب تغيير اسمك يكاد يكتمل!",
|
||||
"account.settings.field.email": "البريد الالكتروني (تسجيل الدخول)",
|
||||
"account.settings.field.email.empty": "إضافة عنوان البريد الإلكتروني",
|
||||
"account.settings.field.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر على الرابط في الرسالة لتحديث عنوان بريدك الإلكتروني.",
|
||||
"account.settings.field.email.help.text": "على هذا العنوان تصلك الرسائل من {siteName} و من فرق المساقات.",
|
||||
"account.settings.field.secondary.email": "عنوان بريد الاستعادة الإلكتروني.",
|
||||
"account.settings.field.secondary.email.empty": "إضافة عنوان بريد إلكتروني لاستعادة حسابك",
|
||||
"account.settings.field.secondary.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر على الرابط في الرسالة لتحديث عنوان بريد الاستعادة الإلكتروني الخاص بك.",
|
||||
"account.settings.email.field.confirmation.header": "تعليق عملية التأكيد",
|
||||
"account.settings.field.dob": "سنة الميلاد",
|
||||
"account.settings.field.dob.empty": "إضافة سنة الميلاد",
|
||||
"account.settings.field.year_of_birth.options.empty": "تحديد سنة الميلاد",
|
||||
"account.settings.field.dob.month": "الشهر",
|
||||
"account.settings.field.dob.year": "السنة",
|
||||
"account.settings.field.month.year.default": "Select month",
|
||||
"account.settings.field.dob.year.default": "Select year",
|
||||
"account.settings.field.dob.form.button": "يرجى تأكيد تاريخ ميلادك",
|
||||
"account.settings.field.dob.form.title": "Enter your birth month and year",
|
||||
"account.settings.field.dob.form.help.text": "We ask for birth month and year information to help us comply with our legal obligations.",
|
||||
"account.settings.field.dob.form.success": "Thank you for entering your information.",
|
||||
"account.settings.field.month_of_birth.options.empty": "تحديد شهر الميلاد",
|
||||
"account.settingsfield.dob.error.general": "حدث خطأ تقني. رجاءً حاول مجددا.",
|
||||
"account.settings.field.country": "البلد",
|
||||
"account.settings.field.country.empty": "إضافة البلد ",
|
||||
"account.settings.field.country.options.empty": "اختر البلد",
|
||||
"account.settings.field.state": "المنطقة / الولاية / المحافظة",
|
||||
"account.settings.field.state.empty": "إضافة المنطقة / الولاية / المحافظة",
|
||||
"account.settings.field.state.options.empty": "حدد المنطقة",
|
||||
"account.settings.field.site.language": "لغة الموقع",
|
||||
"account.settings.field.site.language.help.text": "اللغة المستخدمة في كافة أقسام هذا الموقع. يتوفّر هذا الموقع حاليًا بعدد محدود من اللغات.",
|
||||
"account.settings.field.education": "المستوى التعليمي",
|
||||
"account.settings.field.education.empty": "إضافة المستوى التعليمي",
|
||||
"account.settings.field.education.levels.empty": "اختر مستوى تعليميًّا",
|
||||
"account.settings.field.education.levels.p": "دكتوراه",
|
||||
"account.settings.field.education.levels.m": "ماجستير / ماستر أو شهادة مهنيّة",
|
||||
"account.settings.field.education.levels.b": "بكالوريوس / ليسانس",
|
||||
"account.settings.field.education.levels.a": "درجة الزمالة / دبلوم الدراسات الجامعية",
|
||||
"account.settings.field.education.levels.hs": "الثانوية العامة / البكالوريا",
|
||||
"account.settings.field.education.levels.jhs": "المدرسة الإعدادية / المتوسطة",
|
||||
"account.settings.field.education.levels.el": "المدرسة الابتدائية / الأساسية",
|
||||
"account.settings.field.education.levels.none": "دون تعليم رسمي",
|
||||
"account.settings.field.education.levels.o": "نوع آخر من التعليم",
|
||||
"account.settings.field.gender": "الجنس",
|
||||
"account.settings.field.gender.empty": "إضافة الجنس",
|
||||
"account.settings.field.gender.options.empty": "اختر جنسًا",
|
||||
"account.settings.field.gender.options.f": "أنثى",
|
||||
"account.settings.field.gender.options.m": "ذكر",
|
||||
"account.settings.field.gender.options.o": "آخر",
|
||||
"account.settings.field.language.proficiencies": "لغة التحدّث",
|
||||
"account.settings.field.language.proficiencies.empty": "إضافة لغة التحدث",
|
||||
"account.settings.field.language_proficiencies.options.empty": "اختر لغة",
|
||||
"account.settings.field.time.zone": "المنطقة الزمنية",
|
||||
"account.settings.field.time.zone.empty": "ضبط المنطقة الزمنية",
|
||||
"account.settings.field.time.zone.description": "حدد المنطقة الزمنية لعرض تواريخ المساقات. إذا لم تحدد منطقة زمنية، فسيتم عرض تواريخ المساق، بما في ذلك المواعيد النهائية للواجبات بالتوقيت المحلّي للمتصفح.",
|
||||
"account.settings.field.time.zone.default": "الافتراضي (المنطقة الزمنية المحلية)",
|
||||
"account.settings.field.time.zone.all": "جميع المناطق الزمنية",
|
||||
"account.settings.field.time.zone.country": "المناطق الزمنية للبلدان",
|
||||
"account.settings.section.social.media": "روابط التواصل الإجتماعي",
|
||||
"account.settings.section.social.media.description": "اختياريًا، اربط حساباتك الشخصية بأيقونات التواصل الاجتماعي في ملفك على {siteName}.",
|
||||
"account.settings.field.social.platform.name.linkedin": "لينكد إن",
|
||||
"account.settings.field.social.platform.name.linkedin.empty": "إضافة سيرة لينكد إن",
|
||||
"account.settings.jump.nav.delete.account": "حذف حسابي",
|
||||
"account.settings.field.social.platform.name.twitter": "تويتر",
|
||||
"account.settings.field.social.platform.name.twitter.empty": "إضافة صفحة تويتر",
|
||||
"account.settings.field.social.platform.name.facebook": "فيسبوك",
|
||||
"account.settings.field.social.platform.name.facebook.empty": "إضافة حساب فيسبوك",
|
||||
"account.settings.editable.field.action.save": "حفظ",
|
||||
"account.settings.editable.field.action.cancel": "إلغاء",
|
||||
"account.settings.editable.field.action.edit": "تعديل",
|
||||
"account.settings.static.field.empty": "لا قيمة محددة. رجاءً اتصل بمديرك في {enterprise} ليقوم بالتعديلات.",
|
||||
"account.settings.static.field.empty.no.admin": "لا قيمة محددة.",
|
||||
"account.settings.field.name.certificate.select": "في حال التأشير، سيظهر هذا الاسم في شهاداتك و سجلاتك العامة.",
|
||||
"account.settings.field.name.modal.certificate.title": "اختر اسمًا مفضلًا للشهادات والسجلات العامة",
|
||||
"account.settings.field.name.modal.certificate.select": "اختر اسمًا",
|
||||
"account.settings.field.name.modal.certificate.option.full": "الاسم الكامل",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "اسم متحقَّق منه",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "اختيار الاسم",
|
||||
"account.settings.coaching.consent.welcome.header": "لنبدأ.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "نحن هنا لأجلك من البداية حتى النهاية",
|
||||
"account.settings.coaching.consent.description": "تتضمن برامج MicroBachelors مرافقة تركز على مهنتك و تعليمك و كيفية تحقيقك للنتائج، و ذلك من خلال التواصل الفردي مع خبير متمرس. إن كنت مهتمًا، فيرجى تزويدنا بالمعلومات أدناه و النقر على \"إرسال\"، و سيتصل بك شريكنا في المرافقة عبر البريد الإلكتروني و/أو الرسائل النصية لمساعدتك على المضي قدمًا. تنطبق الشروط والأحكام.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* خدمات المرافقة مشمولة دون تكاليف إضافية للمتعلمين الذين لديهم أرقام هواتف أمريكية. تتضمن المرافقة رسائل نصية دورية. قد تنطبق أسعار على الرسائل والبيانات. أرسل STOP للانسحاب.",
|
||||
"account.settings.coaching.consent.accept-coaching": "سجّل للاستفادة من خدمات المرافقة",
|
||||
"account.settings.coaching.consent.decline-coaching": "أفضّل ألا يٌتصَل بي بخصوص خدمات المرافقة المجانية",
|
||||
"account.settings.coaching.consent.label.name": "رجاءً أكّد اسمك",
|
||||
"account.settings.coaching.consent.label.phone-number": "أدخل رقم هاتفك الجوّال",
|
||||
"account.settings.coaching.consent.success.header": "نجحت العملية!",
|
||||
"account.settings.coaching.consent.success.message": "أنت الآن مشترك في المرافقة. ترقّب رسالة عبر البريد الإلكتروني أو خدمة الرسائل القصيرة في في الأيام المقبلة.",
|
||||
"account.settings.coaching.consent.success.continue": "البدء في مساقي",
|
||||
"account.settings.coaching.managed.support": "الدعم",
|
||||
"account.settings.coaching.managed.alert": "اسمك يديره {ManagerTitle}. اتصل بمديرك للحصول على المساعدة.",
|
||||
"account.settings.field.phone_number": "رقم الهاتف",
|
||||
"account.settings.field.phone_number.empty": "إضافة رقم هاتف",
|
||||
"account.settings.field.coaching_consent": "الموافقة على المرافقة",
|
||||
"account.settings.field.coaching_consent.tooltip": "تتضمن برامج MicroBachelors مرافقة قائمة على الرسائل النصية، تساعدك على إقران تجاربك التعلّمية مع أهدافك المهنية من خلال النصائح الفردية. خدمات المرافقة مشمولة دون تكاليف إضافية، وهي متوفرة للمتعلمين الذين لديهم أرقام هواتف جوالة أمريكية. تنطبق أسعار المراسلة القياسية. أرسل 'STOP' في أي وقت للانسحاب من الرسائل.",
|
||||
"account.settings.field.coaching_consent.error": "مطلوب رقم هاتف أمريكي صحيح للتسجيل في المرافقة",
|
||||
"account.settings.delete.account.before.proceeding": "قبل المتابعة، يرجى {actionLink}.",
|
||||
"account.settings.delete.account.header": "حذف حسابي",
|
||||
"account.settings.delete.account.subheader": "نأسف لذهابك!",
|
||||
"account.settings.delete.account.text.1": "ترجى الملاحظة: إن حذف حسابك وبياناتك الشخصية له أثر دائم و لا يمكن التراجع عنه. لن يكون بمقدور {siteName} استعادة حسابك ولا البيانات التي يتم حذفها.",
|
||||
"account.settings.delete.account.text.2": "بمجرد حذف حسابك، فإنك لن تستطيع استخدامه لمتابعة المساقات على {siteName}.",
|
||||
"account.settings.delete.account.text.2.edX": "بمجرد حذف حسابك، فإنك لن تستطيع استخدامه لمتابعة المساقات على تطبيق edX ولا edx.org ولا أي موقع آخر تستضيفه edX. وهذا يشمل الوصول إلى edx.org من نظام صاحب العمل أو الجامعة و الوصول إلى المواقع الخاصة التي تقدمها MIT Open Learning و Wharton Executive Education و Harvard Medical School.",
|
||||
"account.settings.delete.account.text.3.link": "اتّبع هذه التعليمات لطباعة أو تحميل شهادة",
|
||||
"account.settings.delete.account.text.warning": "تحذير: حذف الحساب أثره دائم. يرجى قراءة ما ورد أعلاه بعناية قبل المتابعة. هذا إجراء غير رجعي، و لن تتمكن بعده من استخدام نفس البريد الإلكتروني على {siteName}.",
|
||||
"account.settings.delete.account.text.change.instead": "هل تريد بدلاً من ذلك تغيير بريدك الإلكتروني أو اسمك أو كلمة المرور الخاصة بك؟",
|
||||
"account.settings.delete.account.button": "حذف حسابي",
|
||||
"account.settings.delete.account.please.activate": "تفعيل حسابك",
|
||||
"account.settings.delete.account.please.confirm": "تأكيد حسابك",
|
||||
"account.settings.delete.account.please.unlink": "فصل جميع حسابات التواصل الاجتماعي",
|
||||
"account.settings.delete.account.modal.header": "هل أنت متأكد؟",
|
||||
"account.settings.delete.account.modal.text.1": "لقد اخترت \"حذف حسابي\". إن حذف حسابك وبياناتك الشخصية ذو أثر دائم لا يمكن التراجع عنه. لن يكون بمقدور {siteName} استعادة حسابك و لا البيانات التي حذفت.",
|
||||
"account.settings.delete.account.modal.text.2": "إن واصلت، فلن تستطيع استخدام هذا الحساب لمتابعة المساقات على {siteName}.",
|
||||
"account.settings.delete.account.modal.text.2.edX": "إن واصلت، فلن تستطيع استخدام هذا الحساب لمتابعة المساقات على على تطبيق edX و لا edx.org و لا أي موقع آخر تستضيفه edX. وهذا يشمل الوصول إلى edx.org من نظام صاحب العمل أو الجامعة و الوصول إلى المواقع الخاصة التي يقدمها MIT Open Learning و Wharton Executive Education و Harvard Medical School.",
|
||||
"account.settings.delete.account.modal.enter.password": "إن كنت لا تزال ترغب في المتابعة و حذف حسابك، فيرجى إدخال كلمة المرور:",
|
||||
"account.settings.delete.account.modal.confirm.delete": "تعم، احذف",
|
||||
"account.settings.delete.account.modal.confirm.cancel": "لا",
|
||||
"account.settings.delete.account.error.unable.to.delete": "لم نستطع حذف الحساب",
|
||||
"account.settings.delete.account.error.no.password": "كلمة المرور مطلوبة",
|
||||
"account.settings.delete.account.error.invalid.password": "كلمة المرور غير صحيحة",
|
||||
"account.settings.delete.account.error.unable.to.delete.details": "عذراً، حدث خطأ أثناء محاولة معالجة طلبك. رجاءً أعد المحاولة لاحقًا.",
|
||||
"account.settings.delete.account.modal.after.header": "نأسف لذهابك! سيُحذف حسابك في ظرف وجيز.",
|
||||
"account.settings.delete.account.modal.after.text": "حذف الحساب، بما في ذلك من إزالة من القوائم البريدية، إجراء قد يستغرق بضعة أسابيع حتى يكتمل عبر نظامنا. إن كنت تريد قبل ذلك الحين إيقاف تلقي البريد الإلكتروني، فيرجى إلغاء الاشتراك من تذييل أي بريد إلكتروني.",
|
||||
"account.settings.delete.account.modal.after.button": "إغلاق ",
|
||||
"account.settings.delete.account.text.3.edX": "قد تفقد كذلك إمكانية الوصول إلى الشهادات الموثقة و كذا مؤهلات البرامج الأخرى مثل شهادات MicroMasters. يمكنك الاحتفاظ بنسخة عنها لديك قبل المواصلة إلى الحذف. {actionLink}.",
|
||||
"account.settings.delete.account.text.3": "قد تفقد كذلك إمكانية الوصول إلى الشهادات الموثقة و كذا مؤهلات البرامج الأخرى. يمكنك الاحتفاظ بنسخة عنها لديك قبل المواصلة إلى الحذف.",
|
||||
"account.settings.message.demographics.service.issue": "حدث خطأ أثناء محاولة استخراج أو حفظ معلومات حسابك. رجاءً أعد المحاولة لاحقًا.",
|
||||
"account.settings.field.demographics.gender": "هوية الجنس",
|
||||
"account.settings.field.demographics.gender.empty": "إضافة هوية الجنس",
|
||||
"account.settings.field.demographics.gender.options.empty": "حدد هوية الجنس",
|
||||
"account.settings.field.demographics.gender_description": "وصف هوية الجنس",
|
||||
"account.settings.field.demographics.gender_description.empty": "أدخل الوصف",
|
||||
"account.settings.field.demographics.ethnicity": "هوية العرق/ الأصل",
|
||||
"account.settings.field.demographics.ethnicity.empty": "إضافة هوية العرق / الأصل",
|
||||
"account.settings.field.demographics.ethnicity.options.empty": "اختر كل ما ينطبق",
|
||||
"account.settings.field.demographics.income": "دخل الأسرة",
|
||||
"account.settings.field.demographics.income.empty": "إضافة دخل الأسرة",
|
||||
"account.settings.field.demographics.income.options.empty": "حدد نطاقًا لدخل الأسرة",
|
||||
"account.settings.field.demographics.military_history": "الوضعية إزاء الخدمة العسكرية في الولايات المتحدة",
|
||||
"account.settings.field.demographics.military_history.empty": "إضافة الوضعية إزاء الخدمة العسكرية",
|
||||
"account.settings.field.demographics.military_history.options.empty": "اختر الوضعية إزاء الخدمة العسكرية",
|
||||
"account.settings.field.demographics.learner_education_level": "مستواك التعليمي",
|
||||
"account.settings.field.demographics.learner_education_level.empty": "إضافة المستوى التعليمي",
|
||||
"account.settings.field.demographics.parent_education_level": "مستوى الوالدين/الأولياء التعليمي",
|
||||
"account.settings.field.demographics.parent_education_level.empty": "إضافة المستوى التعليمي",
|
||||
"account.settings.field.demographics.education_level.options.empty": "حدد المستوى التعليمي",
|
||||
"account.settings.field.demographics.work_status": "الحالة الوظيفية",
|
||||
"account.settings.field.demographics.work_status.empty": "إضافة الحالة الوظيفية",
|
||||
"account.settings.field.demographics.work_status.options.empty": "حدد الحالة الوظيفية",
|
||||
"account.settings.field.demographics.work_status_description": "وصف الحالة الوظيفية",
|
||||
"account.settings.field.demographics.work_status_description.empty": "أدخل الوصف",
|
||||
"account.settings.field.demographics.current_work_sector": "مجال العمل الحالي",
|
||||
"account.settings.field.demographics.current_work_sector.empty": "إضافة مجال العمل",
|
||||
"account.settings.field.demographics.future_work_sector": "مجال العمل المستقبلي",
|
||||
"account.settings.field.demographics.future_work_sector.empty": "إضافة مجال العمل",
|
||||
"account.settings.field.demographics.work_sector.options.empty": "حدد مجال العمل",
|
||||
"account.settings.section.demographics.why": "ما هي غاية {siteName} من جمع هذه المعلومات؟",
|
||||
"account.settings.name.change.title.id": "تغيير الاسم هذا يتطلب التحقق من الهوية",
|
||||
"account.settings.name.change.title.begin": "قبل أن نبدأ",
|
||||
"account.settings.name.change.warning.one": "تحذير: يقوم هذا الإجراء بتحديث الاسم الذي يظهر على جميع الشهادات التي تم الحصول عليها على هذا الحساب في الماضي و أي شهادات تحصل عليها حاليا أو مستقبلاً.",
|
||||
"account.settings.name.change.warning.two": "لا يمكن التراجع عن هذا الإجراء دون التحقق من هويتك.",
|
||||
"account.settings.name.change.id.name.label": "أدخل اسمك كما يظهر في بطاقة تعريف الطالب أو العمل أو بطاقة الهوية الصادرة عن الحكومة.",
|
||||
"account.settings.name.change.id.name.placeholder": "أدخل الاسم الموجود في بطاقة تعريفك ذات الصورة.",
|
||||
"account.settings.name.change.error.valid.name": "رجاءً أدخل اسما صحيحا.",
|
||||
"account.settings.name.change.error.general": "حدث خطأ تقني. رجاءً حاول مجددًا.",
|
||||
"account.settings.name.change.continue": "مواصلة",
|
||||
"account.settings.name.change.cancel": "إلغاء",
|
||||
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجددًا.",
|
||||
"account.settings.editable.field.password.reset.button.confirmation.support.link": "الدعم الفني",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "لقد أرسلنا رسالة إلى {email}. انقر على الرابط في الرسالة لإعادة ضبط كلمة المرور الخاصة بك. لم تصلك الرسالة؟ اتصل بـ{technicalSupportLink}.",
|
||||
"account.settings.editable.field.password.reset.button": "إعادة ضبط كلمة المرور",
|
||||
"account.settings.editable.field.password.reset.button.forbidden": "طلبك السابق في تقدّم، رجاءً حاول مجددًا بعد لحظات قليلة.",
|
||||
"account.settings.editable.field.password.reset.label": "كلمة المرور",
|
||||
"account.settings.sso.link.account": "تسجيل الدخول باستخدام {name}",
|
||||
"account.settings.sso.account.connected": "موصول",
|
||||
"account.settings.sso.account.disconnect.error": "حدثت مشكلة أثناء فصل هذا الحساب، اتصل بالدعم إن استمرت المشكلة.",
|
||||
"account.settings.sso.unlink.account": "فصل حساب {name}",
|
||||
"account.settings.sso.no.providers": "لا يمكن وصل أي حسابات في الوقت الراهن.",
|
||||
"account.page.title": "الحساب | {siteName}",
|
||||
"id.verification.access.blocked.denied": "لا يمكننا التحقق من هويتك في الوقت الراهن. إن لم تكن قد فعّلت حسابك بعد، فيرجى تفقد مجلد الرسائل غير المرغوب فيها بحثًا عن بريد التفعيل الإلكتروني من {email}.",
|
||||
"id.verification.next": "التالي",
|
||||
"id.verification.support": "الدعم",
|
||||
"id.verification.example.card.alt": "مثال بطاقة تعريف صحيحة بالاسم الكامل والصورة.",
|
||||
"id.verification.requirements.title": "متطلبات التحقق باستخدام الصورة",
|
||||
"id.verification.requirements.description": "لإكمال التحقق بالصورة، ستحتاج ما يلي:",
|
||||
"id.verification.requirements.card.device.title": "جهاز مزود بكاميرا",
|
||||
"id.verification.requirements.card.device.allow": "السماح",
|
||||
"id.verification.requirements.card.id.title": "بطاقة تعريف بها صورة",
|
||||
"id.verification.requirements.card.id.text": "تحتاج إلى بطاقة تعريف صالحة تحتوي اسمك الكامل وصورتك، كرخصة القيادة أو جواز السفر مثلاً.",
|
||||
"id.verification.privacy.title": "معلومات الخصوصية",
|
||||
"id.verification.privacy.need.photo.question": "ما حاجة {siteName} لصورتي؟",
|
||||
"id.verification.privacy.need.photo.answer": "نستخدم صور التحقق الخاصة بك لتأكيد هويتك ولضمان صحة شهادتك.",
|
||||
"id.verification.privacy.do.with.photo.question": "ماذا يُفعَل بهذه الصورة في {siteName}؟",
|
||||
"id.verification.privacy.do.with.photo.answer": "نقوم بتشفير صورتك بشكل آمن وإرسالها إلى خدمة الترخيص الخاصة بنا للمراجعة. صورتك و معلوماتك لا تُحفَظ و لا تظهر في أي مكان على {siteName} بعد اكتمال عملية التحقق.",
|
||||
"id.verification.access.blocked.title": "التحقق من الهوية",
|
||||
"id.verification.access.blocked.enrollment": "أنت حاليا غير ملتحق بأي مساق يتطلب التحقق من الهوية.",
|
||||
"id.verification.access.blocked.pending": "لقد سلّمت من قبل معلومات التحقق الخاصة بك. ستصلك رسالة على لوحة المعلومات الخاصة بك عند اكتمال عملية التحقق (عادةً في ظرف 5 أيام).",
|
||||
"id.verification.photo.take": "التقاط صورة ",
|
||||
"id.verification.photo.retake": "إعادة التقاط الصورة؟",
|
||||
"id.verification.photo.enable.detection": "تفعيل اكتشاف الوجوه",
|
||||
"id.verification.photo.enable.detection.portrait.help.text": "في حال التأشير، سيبرز مربع حول وجهك. يمكن رؤية وجهك بوضوح إن كان المربع المحيط به أزرق اللون. أما إن كان وجهك في وضع غير جيد أو غير قابل للاكتشاف، فسيكون المربع أحمر اللون.",
|
||||
"id.verification.photo.enable.detection.id.help.text": "في حال التأشير، سيظهر مربع حول صورة وجهك في بطاقة الهوية. يمكن رؤية وجهك بوضوح إن كان المربع المحيط به أزرق اللون. أما إن كان وجهك في وضع غير جيد أو غير قابل للاكتشاف، فسيكون المربع أحمر اللون.",
|
||||
"id.verification.photo.feedback.correct": "موضع الوجه جيد.",
|
||||
"id.verification.photo.feedback.two.faces": "تم اكتشاف أكثر من وجه واحد.",
|
||||
"id.verification.photo.feedback.no.faces": "لم يتم اكتشاف أي وجه.",
|
||||
"id.verification.photo.feedback.top.left": "الموضع خاطئ. أعلى اليسار.",
|
||||
"id.verification.photo.feedback.top.center": "الموضع خاطئ. أعلى الوسط.",
|
||||
"id.verification.photo.feedback.top.right": "الموضع خاطئ. أعلى اليمين.",
|
||||
"id.verification.photo.feedback.center.left": "الموضع خاطئ. وسط اليسار.",
|
||||
"id.verification.photo.feedback.center.center": "الموضع خاطئ. قريب جدًا من الكاميرا.",
|
||||
"id.verification.photo.feedback.center.right": "الموضع خاطئ. وسط اليمين.",
|
||||
"id.verification.photo.feedback.bottom.left": "الموضع خاطئ. أسفل اليسار.",
|
||||
"id.verification.photo.feedback.bottom.center": "الموضع خاطئ. أسفل الوسط.",
|
||||
"id.verification.photo.feedback.bottom.right": "الموضع خاطئ. أسفل اليمين.",
|
||||
"id.verification.camera.access.title": "صلاحيات الكاميرا",
|
||||
"id.verification.camera.access.title.success": "الوصول للكاميرا ممكن",
|
||||
"id.verification.camera.access.title.failed": "تعذّر الوصول للكاميرا.",
|
||||
"id.verification.camera.access.click.allow": "رجاءً تأكد من النقر على \"السماح\"",
|
||||
"id.verification.camera.access.enable": "تفعيل الكاميرا",
|
||||
"id.verification.camera.access.problems": "لديك مشاكل؟",
|
||||
"id.verification.camera.access.skip": "تَخَطَّ و قم برفع ملفات صور بدلاً من ذلك.",
|
||||
"id.verification.camera.access.success": "يبدو أن كاميرا جهازك جاهزة و تعمل.",
|
||||
"id.verification.camera.access.failure": "يبدو أننا غير قادرين على الوصول إلى كاميرا جهازك. ستحتاج لرفع ملف صورتك و صورة بطاقة هويتك.",
|
||||
"id.verification.camera.access.failure.temporary": "يبدو أننا غير قادرين على الوصول إلى الكاميرا. رجاءً تحقق من أن كاميرا جهازك متصلة ومن أنك قد سمحت للمتصفح بالوصول إليها.",
|
||||
"id.verification.camera.access.failure.temporary.chrome": "لتفعيل الوصول للكاميرا في متصفح كروم:",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step1": "افتح كروم.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2": "توجه إلى المزيد > الإعدادات.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "لنظام ويندوز: Alt+F أو Alt+E أو F10 متبوعاً بـ SPACEBAR",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "لنظام ماك: Command+,",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step3": "تحت علامة التبويب \"الخصوصية والأمان\"، اختر \"إعدادات الموقع\" ثم \"الكاميرا\".",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step4": "ضمن \"قائمة الحظر\"، ابحث عن \"edx.org\" وحدده.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step5": "في قسم \"الصلاحيات\"، قم بتحديث صلاحيات الكاميرا إلى \"السماح\".",
|
||||
"id.verification.camera.access.failure.temporary.ie11": "لتفعيل الوصول للكاميرا في متصفح انترنت إكسبلورر:",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step1": "افتح إدارة إعدادات Flash Player عن طريق الانتقال إلى إعدادات Windows > لوحة التحكم > Flash Player.",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step2": "حدد علامة التبويب \"Camera and Mic\" (الكاميرا والميكروفون)، ثم حدد الزر \"Camera and Microphone Settings by Site\" (إعدادات الكاميرا والميكروفون حسب الموقع).",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step3": "اختر \"edx.org\" من قائمة مواقع ويب وقم بتغيير الصلاحيات من خلال تحديد \"السماح\" في القائمة المنسدلة.",
|
||||
"id.verification.camera.access.failure.temporary.firefox": "لتمكين الوصول للكاميرا في متصفّح فايرفوكس: ",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step1": "فتح متصفّح فايرفوكس.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step2": "أدخل \"about:preferences\" في شريط عنوان الرابط.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step3": "حدد علامة التبويب \"الخصوصية والأمان\"، وانتقل إلى القسم \"الصلاحيات\".",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step4": "بجوار \"Camera\" (الكاميرا)، حدد \"Settings…\" (الإعدادات…) الزر.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step5": "في شريط البحث أدخل \"edx.org.\"",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step6": "في عمود الحالة لـ \"edx.org,\" حدد \"السماح\" من القائمة المنسدلة.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step7": "اختر \"حفظ التغييرات.\"",
|
||||
"id.verification.camera.access.failure.temporary.safari": "تمكين الوصول للكاميرا في متصفّح سفاري: ",
|
||||
"id.verification.camera.access.failure.temporary.safari.step1": "فتح متصفّح سفاري.",
|
||||
"id.verification.camera.access.failure.temporary.safari.step2": "انقر فوق قائمة تطبيق سفاري، ثم حدد \"Preferences\" (التفضيلات). يمكنك أيضاً استخدام الأمرCommand+, كاختصار للوحة المفاتيح",
|
||||
"id.verification.camera.access.failure.temporary.safari.step3": "حدد علامة التبويب \"مواقع الويب\" ثم حدد \"كاميرا\".",
|
||||
"id.verification.camera.access.failure.temporary.safari.step4": "حدد \"edx.org\" وقم بتغيير صلاحيات الكاميرا إلى \"السماح\".",
|
||||
"id.verification.camera.access.failure.unsupported": "يبدو أن متصفحك لا يدعم الوصول إلى الكاميرا.",
|
||||
"id.verification.camera.access.failure.unsupported.chrome.explanation": "لا يدعم متصفح Chrome حاليًا الوصول إلى الكاميرا على أجهزة iOS مثل أجهزة iPhone و iPad.",
|
||||
"id.verification.camera.access.failure.unsupported.instructions": "رجاءً استخدم متصفحًا آخر لإكمال التحقق من الهوية.",
|
||||
"id.verification.photo.tips.title": "نصائح مفيدة بخصوص الصورة",
|
||||
"id.verification.photo.tips.description": "بعد ذلك، سنحتاج منك التقاط صورة لوجهك. يرجى مراجعة النصائح المفيدة أدناه.",
|
||||
"id.verification.photo.tips.list.title": "نصائح بخصوص الصورة",
|
||||
"id.verification.photo.tips.list.description": "لالتقاط صورة ناجحة، يُرجى التأكّد ممّا يلي:",
|
||||
"id.verification.photo.tips.list.well.lit": "أنّ وجهك مُضاء جيدًا.",
|
||||
"id.verification.photo.tips.list.inside.frame": "أنّ وجهك كلَّه داخل إطار الصورة.",
|
||||
"id.verification.portrait.photo.title.camera": "التقط صورة لنفسك",
|
||||
"id.verification.portrait.photo.instructions.camera": "عندما يكون وجهك في موضعه، استخدم زر 'التقاط صورة' أدناه لالتقاط صورتك.",
|
||||
"id.verification.camera.help.sight.question": "ماذا إن لم أتمكن من رؤية صورة الكاميرا أو إن لم أتمكن من رؤية صورتي لتحديد أي جانب مرئي؟",
|
||||
"id.verification.camera.help.sight.answer.portrait": "قد تتمكن من إكمال إجراء التقاط الصور دون مساعدة، لكن قد يتطلب الأمر بضع محاولات لضبط موضع الكاميرا بشكل صحيح.يختلف موضع الكاميرا المثالي من حاسوب ﻵخر، لكن عمومًا يكون أفضل موضع لتصوير الرأس تقريبًا على بعد 12-18 بوصة (30-45 سنتمترًا) من الكاميرا، مع وضع رأسك في المنتصف بالنسبة إلى شاشة الحاسوب. إن تم رفض الصور التي ترسلها، فحاول تغيير اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة.",
|
||||
"id.verification.camera.help.sight.answer.id": "قد تتمكن من إكمال إجراء التقاط الصور دون مساعدة، لكن قد يتطلب الأمر بضع محاولات لضبط موضع الكاميرا بشكل صحيح.يختلف موضع الكاميرا المثالي من حاسوب ﻵخر، لكن عمومًا يكون أفضل موضع لتصوير بطاقة تعريف تقريبًا على بعد 8-12 بوصة (20-30 سنتمترًا) من الكاميرا، مع وضع البطاقة في المنتصف بالنسبة للكاميرا. إن تم رفض الصور التي ترسلها، فحاول تغيير اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة. إن أكثر سبب للرفض هو عدم القدرة على قراءة نص بطاقة التعريف.",
|
||||
"id.verification.camera.help.difficulty.question.portrait": "ماذا لو واجهت صعوبة في تثبيت رأسي في الموضع المناسب للكاميرا؟",
|
||||
"id.verification.camera.help.difficulty.question.id": "ماذا لو واجهت صعوبة في تثبيت بطاقة هويتي في الموضع المناسب للكاميرا؟",
|
||||
"id.verification.camera.help.difficulty.answer": "إن احتجت لمساعدة في التقاط صورة لإرسالها، فاتصل بدعم {siteName} للحصول على اقتراحات إضافية.",
|
||||
"id.verification.id.photo.unclear.question": "هل صورة بطاقة تعريفك غير واضحة أو ضبابية جدًا؟",
|
||||
"id.verification.id.tips.title": "نصائح مفيدة بخصوص بطاقة التعريف",
|
||||
"id.verification.id.tips.description": "بعد ذلك، عليك التقاط صورة لبطاقة تعريف صالحة تتضمن اسمك الكامل مع صورة، مثل رخصة القيادة أو جواز السفر.يرجى منك تجهيز بطاقة تعريفك.",
|
||||
"id.verification.id.tips.list.well.lit": "أن تكون بطاقة تعريفك مضاءة جيدًا.",
|
||||
"id.verification.id.tips.list.clear": "تأكد من قدرتك على رؤية صورتك وقراءة اسمك بوضوح.",
|
||||
"id.verification.id.photo.title.camera": "التقط صورة لبطاقة تعريفك",
|
||||
"id.verification.id.photo.title.upload": "ارفع صورة لبطاقة تعريفك",
|
||||
"id.verification.id.photo.preview.alt": "معاينة صورة الهوية.",
|
||||
"id.verification.id.photo.instructions.camera": "عندما تكون بطاقتك في موضعها، استخدم زر 'التقاط صورة' أدناه لالتقاط صورتك. يرجى استخدام جواز سفر أو رخصة قيادة أو بطاقة تعريف أخرى تتضمن اسمك الكامل وصورة لوجهك.",
|
||||
"id.verification.id.photo.instructions.upload": "يرجى رفع صورة لبطاقة تعريفك. تأكد من أن البطاقة كاملة داخل الإطار و أنها مضاءة جيدًا. يجب أن يكون حجم الملف أقل من 10 ميجابايت. الأنساق المدعومة:",
|
||||
"id.verification.id.photo.instructions.upload.error.invalidFileType": "الملف الذي حددته ليس ضمن أنواع الصورة المدعومة. اختر رجاءً من بين الأنساق التالية:",
|
||||
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "الملف الذي حددته كبير جداً. رجاءً أعد المحاولة باستخدام ملف ذي حجم أقل من 10 ميجابايت.",
|
||||
"id.verification.name.check.title": "تحقق مرة أخرى من اسمك",
|
||||
"id.verification.name.check.instructions": "هل الاسم أدناه يطابق الاسم الموجود في بطاقة تعريفك ذات الصورة. إن لم يكن كذلك، فقم بتحديث الاسم أدناه ليطابق بطاقة تعريفك.",
|
||||
"id.verification.name.check.mismatch.information": "إن كان الاسم أدناه لا يطابق بطاقة تعريفك، فسيتم رفض تأكيد هويتك.",
|
||||
"id.verification.name.error": "رجاءً أدخل اسمك كما يظهر في بطاقة تعريفك ذات الصورة.",
|
||||
"id.verification.account.name.warning.prefix": "تُرجى الملاحظة:",
|
||||
"id.verification.account.name.settings": "إعدادات الحساب",
|
||||
"id.verification.name.label": "الاسم",
|
||||
"id.verification.account.name.photo.alt": "صورة بطاقة هويتك التي ستُسلَّم.",
|
||||
"id.verification.review.title": "مراجعة صورك",
|
||||
"id.verification.review.description": "يُرجى التأكّد من أنّ الصور والمعلومات التي قدّمتها تمكّننا من التحقّق من هويّتك. ",
|
||||
"id.verification.review.portrait.label": "صورتك الشخصية",
|
||||
"id.verification.review.portrait.alt": "صورة وجهك التي ستُسلَّم.",
|
||||
"id.verification.review.portrait.retake": "إعادة التقاط الصورة الشخصية",
|
||||
"id.verification.review.id.label": "بطاقة تعريفك",
|
||||
"id.verification.review.id.alt": "صورة بطاقة تعريفك التي ستُسلَّم.",
|
||||
"id.verification.review.id.retake": "إعادة التقاط صورة بطاقة الهوية",
|
||||
"id.verification.review.confirm": "تسليم",
|
||||
"id.verification.submission.alert.error.face": "مطلوبة صورة لوجهك. رجاءً أعد التقاط صورتك الشخصية.",
|
||||
"id.verification.submission.alert.error.id": "مطلوبة صورة لبطاقة تعريفك. رجاءً أعد التقاط صورة لبطاقة تعريفك.",
|
||||
"id.verification.submission.alert.error.name": "مطلوب اسم حساب صحيح. يرجى تحديث اسم حسابك لمطابقة الاسم على بطاقة تعريفك.",
|
||||
"id.verification.submission.alert.error.unsupported": "واحد أو أكثر من الملفات التي قمت برفعها في نَسقٍ غير مدعوم. رجاءً اختر مما يلي:",
|
||||
"id.verification.review.error": "{siteName} صفحة دعم",
|
||||
"id.verification.submitted.title": "التحقق من الهوية جارٍ",
|
||||
"id.verification.submitted.text": "لقد تلقينا معلوماتك و نحن الآن نتحقق من هويتك. سيتم إخطارك عند اكتمال عملية التحقق (عادةً في ظرف 5 أيام). إلى ذلكم الحين، لا يزال يمكنك الوصول لجميع محتويات المساق المتاحة.",
|
||||
"id.verification.return.dashboard": "العودة للوحة المعلومات",
|
||||
"id.verification.return.course": "العودة للمساق",
|
||||
"id.verification.return.generic": "العودة",
|
||||
"id.verification.photo.upload.help.title": "قم يدلا من هذا برفع صورة",
|
||||
"id.verification.photo.camera.help.title": "استخدم الكاميرا بدلاً من هذا",
|
||||
"id.verification.photo.upload.help.text": "إن واجهتك مشكلة في استخدام مُلتقط الصور أعلاه، فقد ترغب بدﻷ من ذلك في رفع صورة. لرفع صورة، انقر على الزر أدناه.",
|
||||
"id.verification.photo.camera.help.text": "إن واجهتك مشكلة في رفع صورة أعلاه، فقد ترغب بدلا من ذلك في استخدام كاميرا جهازك. لاستخدام الكاميرا، انقر على الزر أدناه.",
|
||||
"id.verification.upload.help.button": "انتقل إلى وضع الرفع",
|
||||
"id.verification.camera.help.button": "انتقل إلى وضع الكاميرا",
|
||||
"id.verification.request.camera.access.instructions": "حتى تلتقط صورة باستخدام الكاميرا، قد تتلقى طلبًا من المتصفح للوصول إلى الكاميرا. {clickAllow}",
|
||||
"id.verification.requirements.account.managed.alert": "إعدادات حسابك يديرها {managerTitle}. إن لم يكن الاسم في بطاقة هويتك ذات الصورة. مطابقًا للاسم الذي في حسابك، فيرجى الاتصال بالمسؤول {profileDataManager} أو ب{support} للحصول على المساعدة قبل إتمام عملية التحقق من الصورة..",
|
||||
"id.verification.requirements.card.device.text": "أنت بحاجة إلى جهاز مزود بكاميرا. إذا تلقيت طلبًا من المتصفح للوصول إلى كاميرا جهازك، فتأكد رجاءً من النقر على {السماح}.",
|
||||
"id.verification.account.name.summary.alert": "إعدادات حسابك يديرها {managerTitle}. إن لم يكن الاسم في بطاقة هويتك ذات الصورة. مطابقًا للاسم الذي في حسابك، فيرجى الاتصال بالمسؤول {profileDataManager} أو ب{support} للحصول على المساعدة قبل إتمام عملية التحقق من الصورة..",
|
||||
"idv.submission.alert.error": "\nواجهنا خطأ فني أثناء محاولة رفع طلب التحقق من الهوية\nقد تكون هذه مشكلة مؤقتة، لذا يرجى المحاولة مجددًا بعد بضع دقائق\nإن استمرت المشكلة، فيرجى الذهاب إلى {support_link} للحصول على المساعدة.",
|
||||
"id.verification.account.name.edit": "تعديل {sr}"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"siteheader.links.courses": "Courses",
|
||||
"siteheader.links.programs": "Programs & Degrees",
|
||||
"siteheader.links.schools": "Schools & Partners",
|
||||
"siteheader.user.menu.dashboard": "Dashboard",
|
||||
"siteheader.user.menu.profile": "Profile",
|
||||
"siteheader.user.menu.account.settings": "Account",
|
||||
"siteheader.user.menu.logout": "Logout",
|
||||
"siteheader.user.menu.login": "Login",
|
||||
"siteheader.user.menu.register": "Sign Up"
|
||||
}
|
||||
361
src/i18n/messages/de.json
Normal file
361
src/i18n/messages/de.json
Normal file
@@ -0,0 +1,361 @@
|
||||
{
|
||||
"account.settings.message.duplicate.tpa.provider": "The {provider} account you selected is already linked to another {siteName} 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.support": "support",
|
||||
"account.settings.page.heading": "Account Settings",
|
||||
"account.settings.loading.message": "Loading...",
|
||||
"account.settings.loading.error": "Error: {error}",
|
||||
"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": "Switch Back to {previous_language}",
|
||||
"account.settings.banner.beta.language.action.help.translate": "Help Translate into {beta_language}",
|
||||
"account.settings.section.account.information": "Account Information",
|
||||
"account.settings.section.account.information.description": "These settings include basic information about your account.",
|
||||
"account.settings.section.profile.information": "Profile Information",
|
||||
"account.settings.section.demographics.information": "Optional Information",
|
||||
"account.settings.section.site.preferences": "Site Preferences",
|
||||
"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.field.username": "Username",
|
||||
"account.settings.field.username.help.text": "The name that identifies you on {siteName}. You cannot change your username.",
|
||||
"account.settings.field.full.name": "Full 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.default": "The name that appears on your public profile.",
|
||||
"account.settings.field.full.name.help.text.default.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 photo ID.",
|
||||
"account.settings.field.name.verified.help.text.verified.proctored": "This name has been verified by proctoring.",
|
||||
"account.settings.field.name.verified.help.text.verified.certificate": "This name has been verified by photo ID, and is selected to appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.verified.help.text.verified.proctored.certificate": "This name has been verified by proctoring, and is 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.proctored": "Your proctored exam has been submitted. Verified name cannot be changed at this time. Please check back in 2-5 days.",
|
||||
"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.help.text.submitted.proctored.certificate": "Once your proctored exam passes review, this name will appear on your certificate and public-facing records. Verified Name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.verification.help": "Enter your name as it appears on your unexpired student, work, or government-issued identification card.",
|
||||
"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.proctored": "Your proctored exam has been submitted. Full name cannot be changed at this time. Please check back in 2-5 days.",
|
||||
"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.full.name.help.text.submitted.proctored.certificate": "Once your proctored exam passes review, 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.empty": "Add email address",
|
||||
"account.settings.field.email.confirmation": "We’ve 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.secondary.email": "Recovery email address",
|
||||
"account.settings.field.secondary.email.empty": "Add a recovery email address",
|
||||
"account.settings.field.secondary.email.confirmation": "We’ve sent a confirmation message to {value}. Click the link in the message to update your recovery email address.",
|
||||
"account.settings.email.field.confirmation.header": "Pending confirmation",
|
||||
"account.settings.field.dob": "Year of birth",
|
||||
"account.settings.field.dob.empty": "Add year of birth",
|
||||
"account.settings.field.year_of_birth.options.empty": "Select a year of birth",
|
||||
"account.settings.field.dob.month": "Month",
|
||||
"account.settings.field.dob.year": "Year",
|
||||
"account.settings.field.month.year.default": "Select month",
|
||||
"account.settings.field.dob.year.default": "Select year",
|
||||
"account.settings.field.dob.form.button": "Please confirm your date of birth",
|
||||
"account.settings.field.dob.form.title": "Enter your birth month and year",
|
||||
"account.settings.field.dob.form.help.text": "We ask for birth month and year information to help us comply with our legal obligations.",
|
||||
"account.settings.field.dob.form.success": "Thank you for entering your information.",
|
||||
"account.settings.field.month_of_birth.options.empty": "Select a month of birth",
|
||||
"account.settingsfield.dob.error.general": "A technical error occurred. Please try again.",
|
||||
"account.settings.field.country": "Country",
|
||||
"account.settings.field.country.empty": "Add country",
|
||||
"account.settings.field.country.options.empty": "Select a Country",
|
||||
"account.settings.field.state": "State",
|
||||
"account.settings.field.state.empty": "Add state",
|
||||
"account.settings.field.state.options.empty": "Select a State",
|
||||
"account.settings.field.site.language": "Site language",
|
||||
"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.education": "Education",
|
||||
"account.settings.field.education.empty": "Add level of education",
|
||||
"account.settings.field.education.levels.empty": "Select a level of education",
|
||||
"account.settings.field.education.levels.p": "Doctorate",
|
||||
"account.settings.field.education.levels.m": "Master's or professional degree",
|
||||
"account.settings.field.education.levels.b": "Bachelor's Degree",
|
||||
"account.settings.field.education.levels.a": "Associate's degree",
|
||||
"account.settings.field.education.levels.hs": "Secondary/high school",
|
||||
"account.settings.field.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"account.settings.field.education.levels.el": "Elementary/primary school",
|
||||
"account.settings.field.education.levels.none": "No formal education",
|
||||
"account.settings.field.education.levels.o": "Other education",
|
||||
"account.settings.field.gender": "Gender",
|
||||
"account.settings.field.gender.empty": "Add gender",
|
||||
"account.settings.field.gender.options.empty": "Select a gender",
|
||||
"account.settings.field.gender.options.f": "Female",
|
||||
"account.settings.field.gender.options.m": "Male",
|
||||
"account.settings.field.gender.options.o": "Other",
|
||||
"account.settings.field.language.proficiencies": "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.time.zone": "Time zone",
|
||||
"account.settings.field.time.zone.empty": "Set time zone",
|
||||
"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 browser’s local time zone.",
|
||||
"account.settings.field.time.zone.default": "Default (Local Time Zone)",
|
||||
"account.settings.field.time.zone.all": "All time zones",
|
||||
"account.settings.field.time.zone.country": "Country time zones",
|
||||
"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.field.social.platform.name.linkedin": "LinkedIn",
|
||||
"account.settings.field.social.platform.name.linkedin.empty": "Add LinkedIn profile",
|
||||
"account.settings.jump.nav.delete.account": "Delete My Account",
|
||||
"account.settings.field.social.platform.name.twitter": "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.empty": "Add Facebook profile",
|
||||
"account.settings.editable.field.action.save": "Save",
|
||||
"account.settings.editable.field.action.cancel": "Cancel",
|
||||
"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.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": "Let’s get started.",
|
||||
"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 you’re 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.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.coaching.consent.accept-coaching": "Sign up for coaching",
|
||||
"account.settings.coaching.consent.decline-coaching": "I prefer not to be contacted with free coaching services",
|
||||
"account.settings.coaching.consent.label.name": "Please confirm your name",
|
||||
"account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
|
||||
"account.settings.coaching.consent.success.header": "Success!",
|
||||
"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": "Start my course",
|
||||
"account.settings.coaching.managed.support": "support",
|
||||
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
|
||||
"account.settings.field.phone_number": "Phone Number",
|
||||
"account.settings.field.phone_number.empty": "Add a phone number",
|
||||
"account.settings.field.coaching_consent": "Coaching consent",
|
||||
"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": "A valid US phone number is required to opt into coaching",
|
||||
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
|
||||
"account.settings.delete.account.header": "Delete My Account",
|
||||
"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.2": "Once your account is deleted, you cannot use it to take courses on {siteName}.",
|
||||
"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 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.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 {siteName}.",
|
||||
"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.please.activate": "activate your account",
|
||||
"account.settings.delete.account.please.confirm": "confirm your account",
|
||||
"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.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.2": "If you proceed, you will be unable to use this account to take courses on {siteName}.",
|
||||
"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.confirm.delete": "Yes, Delete",
|
||||
"account.settings.delete.account.modal.confirm.cancel": "Cancel",
|
||||
"account.settings.delete.account.error.unable.to.delete": "Unable to delete account",
|
||||
"account.settings.delete.account.error.no.password": "A password is required",
|
||||
"account.settings.delete.account.error.invalid.password": "Password is incorrect",
|
||||
"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.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.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. 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.empty": "Add gender identity",
|
||||
"account.settings.field.demographics.gender.options.empty": "Select a gender identity",
|
||||
"account.settings.field.demographics.gender_description": "Gender identity description",
|
||||
"account.settings.field.demographics.gender_description.empty": "Enter description",
|
||||
"account.settings.field.demographics.ethnicity": "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.income": "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.military_history": "U.S. 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.learner_education_level": "Your 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.empty": "Add education level",
|
||||
"account.settings.field.demographics.education_level.options.empty": "Select education level",
|
||||
"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_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.section.demographics.why": "Why does {siteName} collect this information?",
|
||||
"account.settings.name.change.title.id": "This name change requires identity verification",
|
||||
"account.settings.name.change.title.begin": "Before we begin",
|
||||
"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.name.change.warning.two": "This action cannot be undone without verifying your identity.",
|
||||
"account.settings.name.change.id.name.label": "Enter your name as it appears on your unexpired student, work, or government-issued identification card.",
|
||||
"account.settings.name.change.id.name.placeholder": "Enter the name on your photo ID",
|
||||
"account.settings.name.change.error.valid.name": "Please enter a valid name.",
|
||||
"account.settings.name.change.error.general": "A technical error occurred. Please try again.",
|
||||
"account.settings.name.change.continue": "Continue",
|
||||
"account.settings.name.change.cancel": "Cancel",
|
||||
"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": "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": "Reset Password",
|
||||
"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": "Password",
|
||||
"account.settings.sso.link.account": "Sign in with {name}",
|
||||
"account.settings.sso.account.connected": "Linked",
|
||||
"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.no.providers": "No accounts can be linked at this time.",
|
||||
"account.page.title": "Account | {siteName}",
|
||||
"id.verification.access.blocked.denied": "We 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.support": "support",
|
||||
"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.description": "In order to complete Photo Verification, you will need the following:",
|
||||
"id.verification.requirements.card.device.title": "Device with Camera",
|
||||
"id.verification.requirements.card.device.allow": "Allow",
|
||||
"id.verification.requirements.card.id.title": "Photo Identification Card",
|
||||
"id.verification.requirements.card.id.text": "You need a valid identification card that contains your full name and photo, such as a driver’s license or passport.",
|
||||
"id.verification.privacy.title": "Privacy Information",
|
||||
"id.verification.privacy.need.photo.question": "Why does {siteName} 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.do.with.photo.question": "What does {siteName} 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.access.blocked.title": "Identity Verification",
|
||||
"id.verification.access.blocked.enrollment": "You are not currently enrolled in a course that requires identity verification.",
|
||||
"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.photo.take": "Take 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.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.enable": "Enable Camera",
|
||||
"id.verification.camera.access.problems": "Having problems?",
|
||||
"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.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.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.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.inside.frame": "Your entire face fits inside the frame.",
|
||||
"id.verification.portrait.photo.title.camera": "Take a Photo of Yourself",
|
||||
"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.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.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.difficulty.question.portrait": "What if I have difficulty holding my head in position relative to the camera?",
|
||||
"id.verification.camera.help.difficulty.question.id": "What if I have difficulty holding my ID in position relative to the camera?",
|
||||
"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.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 driver’s 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.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": "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, driver’s 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.name.check.title": "Double-Check Your Name",
|
||||
"id.verification.name.check.instructions": "Does the name below match the name on your photo ID? If not, update the name below to match your photo ID.",
|
||||
"id.verification.name.check.mismatch.information": "If the name below does not match your photo ID, your identity verification will be denied.",
|
||||
"id.verification.name.error": "Please enter your name as it appears on your photo ID.",
|
||||
"id.verification.account.name.warning.prefix": "Please Note:",
|
||||
"id.verification.account.name.settings": "Account Settings",
|
||||
"id.verification.name.label": "Name",
|
||||
"id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
|
||||
"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.portrait.label": "Your Portrait",
|
||||
"id.verification.review.portrait.alt": "Photo of your face to be submitted.",
|
||||
"id.verification.review.portrait.retake": "Retake Portrait Photo",
|
||||
"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": "Retake ID Photo",
|
||||
"id.verification.review.confirm": "Submit",
|
||||
"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.text": "We have received your information and are verifying your identity. You will be notified when the verification process is complete (usually within 5 days). In the meantime, you can still access all available course content.",
|
||||
"id.verification.return.dashboard": "Return to Your Dashboard",
|
||||
"id.verification.return.course": "Return to Course",
|
||||
"id.verification.return.generic": "Return",
|
||||
"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.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.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}"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user