Compare commits
577 Commits
djoy/updat
...
sarina/upd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b12c08d0a | ||
|
|
87fdb315b1 | ||
|
|
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 | ||
|
|
8602561e55 | ||
|
|
b310574f18 | ||
|
|
73badaf916 | ||
|
|
f02cc43078 | ||
|
|
bd5c2343be | ||
|
|
6349d487e4 | ||
|
|
9c6137e668 | ||
|
|
2cf01270d7 | ||
|
|
403df8926d | ||
|
|
4bd96c70af | ||
|
|
d2a835f560 | ||
|
|
d0b5d54d0a | ||
|
|
26299eed65 | ||
|
|
648bea8d84 | ||
|
|
7409f02056 | ||
|
|
f5dd409816 | ||
|
|
6ddac11dc0 | ||
|
|
7019aea4fb | ||
|
|
5424434599 | ||
|
|
8ca9dc78a9 | ||
|
|
30e25b96bb | ||
|
|
1d01abc7da | ||
|
|
917152df22 | ||
|
|
961c0feb78 | ||
|
|
7d57d86729 | ||
|
|
34c5de1340 | ||
|
|
81d604d046 | ||
|
|
e6f7e83cf5 | ||
|
|
a970e17070 | ||
|
|
f471ae0aa7 | ||
|
|
b9efe6faee | ||
|
|
2dbccec1f1 | ||
|
|
9f38b975d9 | ||
|
|
ae355cefcf | ||
|
|
d63dfc929f | ||
|
|
64be9edeac | ||
|
|
5f4f82eae1 | ||
|
|
c8c7352549 | ||
|
|
88206e4282 | ||
|
|
d8e23b1a02 | ||
|
|
5db21d2483 | ||
|
|
526d6114f2 | ||
|
|
0cc38e2dc6 | ||
|
|
380ca7c816 | ||
|
|
bcb20234ab | ||
|
|
87ff50ace8 | ||
|
|
b409aff6b4 | ||
|
|
2b67d037bf | ||
|
|
4137996a91 | ||
|
|
1319bd6377 | ||
|
|
a579e86e98 | ||
|
|
74bb2fb45f | ||
|
|
11dbbad20b | ||
|
|
a0227f1dbc | ||
|
|
f6a7a6063c | ||
|
|
0aa02687e6 | ||
|
|
303f6a5d3f | ||
|
|
b262f42c8d | ||
|
|
b92794b72c | ||
|
|
3f754fa114 | ||
|
|
bf274e5186 | ||
|
|
be9cf70c5c | ||
|
|
c00ea15920 | ||
|
|
5a8bd309e7 | ||
|
|
d83ea54272 | ||
|
|
eae18d9c63 | ||
|
|
a18da61cec | ||
|
|
80d5fd2a34 | ||
|
|
d4e9ba0420 | ||
|
|
5add376c31 | ||
|
|
559c9aa1a9 | ||
|
|
ea8a6d29d0 | ||
|
|
4689482137 | ||
|
|
d48be79e53 | ||
|
|
ed94cc68e3 | ||
|
|
05740b37ff | ||
|
|
b2c8164cd1 | ||
|
|
47b369f797 | ||
|
|
88239f2700 | ||
|
|
602c4b484c | ||
|
|
45ec573ff9 | ||
|
|
e0befb8b60 | ||
|
|
9f4d944670 | ||
|
|
60d26649dd | ||
|
|
55f25c73ca | ||
|
|
29dbdf6ad0 | ||
|
|
5bfa834563 | ||
|
|
5e3af50e3b | ||
|
|
e30a20b185 | ||
|
|
2743e05890 | ||
|
|
71c0563e3a | ||
|
|
5d8d327a48 | ||
|
|
15ba7c087e | ||
|
|
6df7ad243b | ||
|
|
594d3ff9f1 | ||
|
|
7bb9d09dae | ||
|
|
0e6233b693 | ||
|
|
22087d2d2c | ||
|
|
64798cdc80 | ||
|
|
f53ba967e5 | ||
|
|
2deb47d542 | ||
|
|
1b9dd3bdf5 | ||
|
|
fd3c8ede6d | ||
|
|
c1fdace72d | ||
|
|
4ca4d55796 | ||
|
|
2da606bf6f | ||
|
|
997a3c0b98 | ||
|
|
f627257a1c | ||
|
|
9aa2a816b4 | ||
|
|
fc54dd528f | ||
|
|
53fc1b325c | ||
|
|
48b02cd2de | ||
|
|
490274b2ed | ||
|
|
10dc9aabde | ||
|
|
74ec75781e | ||
|
|
c428d3044f | ||
|
|
d9777fe48e | ||
|
|
6b7ab05dd5 | ||
|
|
ce79cd7f5a | ||
|
|
b8ab0a2150 | ||
|
|
56569c717c | ||
|
|
45511860c2 | ||
|
|
11f7d56e75 | ||
|
|
f19380ebac | ||
|
|
5eb43871c7 | ||
|
|
a2388bffc2 | ||
|
|
ebe6af0913 | ||
|
|
fd6ba7847a | ||
|
|
f051905da1 | ||
|
|
5eddb35e0b | ||
|
|
0196245c13 | ||
|
|
0c53a29094 | ||
|
|
60643f6215 | ||
|
|
b0aa91fc98 | ||
|
|
09813fa689 | ||
|
|
b05a40d0aa | ||
|
|
1a76468587 | ||
|
|
93ef8d2b04 | ||
|
|
98efe2649a | ||
|
|
80cc8f156f | ||
|
|
1364ca5711 | ||
|
|
c337e03a4d | ||
|
|
c6aedbea29 | ||
|
|
6e099457db | ||
|
|
54fa5b970a | ||
|
|
38371d1a46 | ||
|
|
f805480e21 | ||
|
|
cc3bf06a6f | ||
|
|
0e8d7622c6 | ||
|
|
9faf28203b | ||
|
|
e94d05d9e2 | ||
|
|
8b4f7818fc | ||
|
|
f18c71d13a | ||
|
|
a1aeb7035e | ||
|
|
ef85448e27 | ||
|
|
741cfb6aac | ||
|
|
c808c8c0a1 | ||
|
|
4eb96e64c7 | ||
|
|
f1ec989054 | ||
|
|
54032e6ec5 | ||
|
|
ef5c303fbc | ||
|
|
caa06a08b0 | ||
|
|
5e4278ea5a | ||
|
|
96dc3f7e3f | ||
|
|
5726be2805 | ||
|
|
73c66d5d18 | ||
|
|
19ef66cf42 | ||
|
|
c83d76e1a9 | ||
|
|
2f2abd54ff | ||
|
|
0b00fa21a2 | ||
|
|
1aa6a22c1c | ||
|
|
d3d0bf97b6 | ||
|
|
7361674019 | ||
|
|
10a3f1fb35 | ||
|
|
06d018fc62 | ||
|
|
3e5bf2b19a | ||
|
|
724a7f9201 | ||
|
|
66b27a01d0 | ||
|
|
9c9725c86c | ||
|
|
bc8d41cd66 | ||
|
|
3a49fb3296 | ||
|
|
c1d1af4943 | ||
|
|
a9d3463619 | ||
|
|
d53c9c11a9 | ||
|
|
d326eb5892 | ||
|
|
945b14fa4b | ||
|
|
92b8998b96 | ||
|
|
a158f8c708 | ||
|
|
637375e890 | ||
|
|
abe5af2870 | ||
|
|
0495c7f6ba | ||
|
|
56362695dd | ||
|
|
1d56ea026f | ||
|
|
edb5998617 | ||
|
|
b8cf476d01 | ||
|
|
7f5e840538 | ||
|
|
e5e950937b | ||
|
|
95cb4c9138 | ||
|
|
aac7244aec | ||
|
|
69f9c8faf5 | ||
|
|
c40ba138ce | ||
|
|
13d71b3257 | ||
|
|
2f459362ad | ||
|
|
bc706c9fe6 | ||
|
|
db7955b3e6 | ||
|
|
b59a39c4b3 | ||
|
|
382a68ef92 | ||
|
|
0e722e1906 | ||
|
|
5fb8e05e6b | ||
|
|
1766de1145 | ||
|
|
6edf9becb5 | ||
|
|
a1c74bd9b8 | ||
|
|
0139d2db75 | ||
|
|
38de20b454 | ||
|
|
8daddeadde | ||
|
|
74e5f2bf76 | ||
|
|
172f9bce4d | ||
|
|
fbbadfedd1 | ||
|
|
adf031e264 | ||
|
|
da5c6b592a | ||
|
|
39b57ead67 | ||
|
|
7c24f47560 | ||
|
|
4276442bfa | ||
|
|
bdad621102 | ||
|
|
c0be21fc3f | ||
|
|
75c1354fae | ||
|
|
39eb2bb310 | ||
|
|
ec0f816b0e | ||
|
|
c863e53855 | ||
|
|
f4811efe66 | ||
|
|
ea37cf01dd | ||
|
|
15d8836a8c | ||
|
|
05ea18f70d | ||
|
|
cca4af7cc1 | ||
|
|
b1a5f98541 | ||
|
|
8020ff58b8 | ||
|
|
569099e88a | ||
|
|
90260ec263 | ||
|
|
5ca2b801c7 | ||
|
|
149cce731d | ||
|
|
274af6c2e5 | ||
|
|
9cdf40f1bb | ||
|
|
22e855702c | ||
|
|
c566b157d9 | ||
|
|
d124a91688 | ||
|
|
5c578af96c | ||
|
|
3076227249 | ||
|
|
5cb1947a69 | ||
|
|
39cd052a81 | ||
|
|
bfbcdf7ea0 | ||
|
|
68928291bc | ||
|
|
917f7cb486 | ||
|
|
d2ff8e1eec | ||
|
|
29269fcdbb | ||
|
|
ea8b435c35 | ||
|
|
23e6ca6bc4 | ||
|
|
23f54949e8 | ||
|
|
f43bc5597c | ||
|
|
481e38c308 | ||
|
|
0a7d449c09 | ||
|
|
2acc15eaa6 | ||
|
|
92fd7c4baa | ||
|
|
0d61d09ddd | ||
|
|
223ea212bc | ||
|
|
58519e5d7f | ||
|
|
4132963bfb | ||
|
|
bbe2a6ab49 | ||
|
|
defd969a80 | ||
|
|
d33cb658e2 | ||
|
|
2ac911945c | ||
|
|
a7530f84aa | ||
|
|
15708b1154 | ||
|
|
c3749e9c7f | ||
|
|
30914e6d6f | ||
|
|
78b5991c89 | ||
|
|
0ea87767af | ||
|
|
3e50222528 | ||
|
|
c912f0e5d5 | ||
|
|
19a88de031 | ||
|
|
8c76c3657d | ||
|
|
28c5be9897 | ||
|
|
386d2ab1f3 | ||
|
|
e8369ff5b7 | ||
|
|
d9d14202c5 | ||
|
|
e1e7344533 | ||
|
|
029813646b | ||
|
|
87ea759e0e | ||
|
|
b67083b2e7 | ||
|
|
14ded7cc23 | ||
|
|
543e6ccae0 | ||
|
|
e6246f3233 | ||
|
|
c04024ef00 | ||
|
|
81e9858be1 | ||
|
|
4374c60dc6 | ||
|
|
70e9bb31a5 | ||
|
|
cee06f4e25 | ||
|
|
8070ec1acf | ||
|
|
abf0c65be7 | ||
|
|
a6c671e824 | ||
|
|
ed43fe3b37 | ||
|
|
a026f09b63 | ||
|
|
e322ae18aa | ||
|
|
71f007b9df | ||
|
|
747fe550c7 | ||
|
|
a84516daef | ||
|
|
3d3e2a2e38 | ||
|
|
27ba7f4a65 | ||
|
|
c3f66ab92c | ||
|
|
590903bcd8 | ||
|
|
a1b1d0ea60 | ||
|
|
8d099c0048 | ||
|
|
b81f7ae8b9 | ||
|
|
27cc4fe692 | ||
|
|
3471e81988 | ||
|
|
8f1231192b | ||
|
|
994dbb6904 | ||
|
|
f251ac7b7e | ||
|
|
f14206bdf4 | ||
|
|
cae761a00d | ||
|
|
879c9a3b33 | ||
|
|
faeed0ec13 | ||
|
|
a65efe3bb4 | ||
|
|
08d079ce40 | ||
|
|
feae2f635a | ||
|
|
da3acc278d | ||
|
|
a705e22d62 | ||
|
|
1db817e1c2 | ||
|
|
e60c24e476 | ||
|
|
8c3a3c284b | ||
|
|
b90deae9f6 | ||
|
|
ea55ceff05 | ||
|
|
6f9e94885b | ||
|
|
882a13fa76 | ||
|
|
bc3085b141 | ||
|
|
f1f9c86b15 | ||
|
|
a437f9c91a | ||
|
|
91d7b98e08 | ||
|
|
a93dc4e3b1 | ||
|
|
4ba3414bd8 | ||
|
|
d4d10287d4 | ||
|
|
0dfd8d8558 | ||
|
|
c60c048c24 | ||
|
|
c57213714b | ||
|
|
777193f816 | ||
|
|
8c564b3d96 | ||
|
|
364684b0c8 | ||
|
|
8abb8d4e5d | ||
|
|
932d47550e | ||
|
|
ab62699148 | ||
|
|
24819bc5df | ||
|
|
48ba152fb7 | ||
|
|
741a9632f8 | ||
|
|
125113a154 | ||
|
|
b1a84572fc | ||
|
|
ae8c9e2893 | ||
|
|
945ef15b97 | ||
|
|
9837b85dce | ||
|
|
2e83d33de2 | ||
|
|
c8d84fc5ab | ||
|
|
bdd87dfaa7 | ||
|
|
563f760e16 | ||
|
|
bc41541848 | ||
|
|
e28f17d061 | ||
|
|
58c375b83a | ||
|
|
0cd9f08539 | ||
|
|
e3db2b72e6 | ||
|
|
d353d8ecf0 | ||
|
|
cfe1be3361 | ||
|
|
1a17ac4934 | ||
|
|
efb99185e3 | ||
|
|
f3693c156c | ||
|
|
19c12118b3 | ||
|
|
13046c717a | ||
|
|
c72fd2aceb |
26
.babelrc
26
.babelrc
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"browsers": ["last 2 versions", "ie 11"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"babel-preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread",
|
||||
"transform-class-properties"
|
||||
],
|
||||
"env": {
|
||||
"i18n": {
|
||||
"plugins": [
|
||||
["react-intl", {
|
||||
"messagesDir": "./temp"
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
25
.env
Normal file
25
.env
Normal file
@@ -0,0 +1,25 @@
|
||||
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=''
|
||||
SUPPORT_URL=null
|
||||
USER_INFO_COOKIE_NAME=null
|
||||
LOGO_URL=''
|
||||
LOGO_TRADEMARK_URL=''
|
||||
LOGO_WHITE_URL=''
|
||||
FAVICON_URL=''
|
||||
PUBLISHER_BASE_URL=
|
||||
STUDIO_BASE_URL=
|
||||
DISCOVERY_API_BASE_URL=
|
||||
28
.env.development
Normal file
28
.env.development
Normal file
@@ -0,0 +1,28 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:1997'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
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'
|
||||
PORT=1997
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME=localhost
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
COACHING_ENABLED=false
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=false
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
PUBLISHER_BASE_URL=
|
||||
STUDIO_BASE_URL=
|
||||
DISCOVERY_API_BASE_URL=
|
||||
27
.env.test
Normal file
27
.env.test
Normal file
@@ -0,0 +1,27 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:1997'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
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'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME=localhost
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
COACHING_ENABLED=''
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
PUBLISHER_BASE_URL=
|
||||
STUDIO_BASE_URL=
|
||||
DISCOVERY_API_BASE_URL=
|
||||
@@ -2,3 +2,4 @@ coverage/*
|
||||
dist/
|
||||
node_modules/
|
||||
__mocks__/
|
||||
__snapshots__/
|
||||
|
||||
34
.eslintrc
34
.eslintrc
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"extends": "eslint-config-edx",
|
||||
"parser": "babel-eslint",
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{
|
||||
"devDependencies": [
|
||||
"webpack/*.js",
|
||||
"**/*.test.jsx",
|
||||
"**/*.test.js"
|
||||
]
|
||||
}
|
||||
],
|
||||
// https://github.com/evcohen/eslint-plugin-jsx-a11y/issues/340#issuecomment-338424908
|
||||
"jsx-a11y/anchor-is-valid": [ "error", {
|
||||
"components": [ "Link" ],
|
||||
"specialLink": [ "to" ]
|
||||
}],
|
||||
"jsx-a11y/label-has-for": [ 2, {
|
||||
"components": [ "label" ],
|
||||
"required": {
|
||||
"some": [ "nesting", "id" ]
|
||||
},
|
||||
"allowChildren": false
|
||||
}]
|
||||
},
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"globals": {
|
||||
"newrelic": false
|
||||
}
|
||||
}
|
||||
3
.eslintrc.js
Executable file
3
.eslintrc.js
Executable file
@@ -0,0 +1,3 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint');
|
||||
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @edx/community-engineering
|
||||
32
.github/workflows/ci.yml
vendored
Normal file
32
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version:
|
||||
- 12
|
||||
npm-test:
|
||||
- i18n_extract
|
||||
- is-es5
|
||||
- lint
|
||||
- test
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install -g npm@6
|
||||
- run: make requirements
|
||||
- run: make test NPM_TESTS=build
|
||||
- run: make test NPM_TESTS=${{ matrix.npm-test }}
|
||||
- name: upload coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
fail_ci_if_error: false
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,6 +6,8 @@ npm-debug.log
|
||||
coverage
|
||||
|
||||
dist/
|
||||
src/i18n/transifex_input.json
|
||||
temp/babel-plugin-react-intl
|
||||
|
||||
### pyenv ###
|
||||
.python-version
|
||||
@@ -14,3 +16,4 @@ dist/
|
||||
*~
|
||||
/temp
|
||||
/.vscode
|
||||
/module.config.js
|
||||
|
||||
22
.npmignore
22
.npmignore
@@ -1,13 +1,15 @@
|
||||
.eslintignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
.travis.yml
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
Makefile
|
||||
npm-debug.log
|
||||
|
||||
webpack
|
||||
.tx
|
||||
coverage
|
||||
dist
|
||||
node_modules
|
||||
public
|
||||
src
|
||||
.dockerignore
|
||||
.eslintignore
|
||||
.eslintrc
|
||||
.gitignore
|
||||
.releaserc
|
||||
.travis.yml
|
||||
babel.config.js
|
||||
Makefile
|
||||
renovate.json
|
||||
|
||||
22
.travis.yml
22
.travis.yml
@@ -1,22 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- lts/*
|
||||
cache:
|
||||
directories:
|
||||
- "~/.npm"
|
||||
before_install:
|
||||
- npm install -g npm@latest
|
||||
- npm install -g greenkeeper-lockfile@1.14.0
|
||||
install:
|
||||
- npm ci
|
||||
before_script: greenkeeper-lockfile-update
|
||||
after_script: greenkeeper-lockfile-upload
|
||||
script:
|
||||
- make validate-no-uncommitted-package-lock-changes
|
||||
- npm run i18n_extract
|
||||
- npm run lint
|
||||
- npm run test
|
||||
- npm run build
|
||||
after_success:
|
||||
- npm run coveralls
|
||||
- codecov
|
||||
149
LICENSE
149
LICENSE
@@ -1,23 +1,21 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
69
Makefile
69
Makefile
@@ -1,36 +1,61 @@
|
||||
extract_translations: ## no prerequisites so we can control order of operations
|
||||
echo "We have to define this target due to tooling assumptions"
|
||||
echo "Also we have to npm install using this hook b/c there's no other place for it in the current setup"
|
||||
npm install
|
||||
npm run-script i18n_extract
|
||||
transifex_resource = frontend-app-account
|
||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
|
||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test is-es5
|
||||
|
||||
.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...
|
||||
# Pulling display strings from .jsx files into .json files...
|
||||
rm -rf $(transifex_temp)
|
||||
npm run-script i18n_extract
|
||||
|
||||
i18n.concat:
|
||||
# Gathering JSON messages into one file...
|
||||
./src/i18n/i18n-concat.js ./temp/src ./src/i18n/transifex_input.json
|
||||
# Gathering JSON messages into one file...
|
||||
$(transifex_utils) $(transifex_temp) $(transifex_input)
|
||||
|
||||
extract_translations: | requirements i18n.extract i18n.concat
|
||||
|
||||
# Despite the name, we actually need this target to detect changes in the incoming translated message files as well.
|
||||
detect_changed_source_translations:
|
||||
git diff --exit-code ./src/i18n/transifex_input.json
|
||||
# Checking for changed translations...
|
||||
git diff --exit-code $(i18n)
|
||||
|
||||
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/frontend-app-account/translation/en/strings/
|
||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/frontend-app-account/source/
|
||||
|
||||
# push translations to Transifex, doing magic so we can include the translator comments
|
||||
push_translations: | i18n.extract
|
||||
# Adding translator comments...
|
||||
# Fetching strings from Transifex...
|
||||
# Pushes translations to Transifex. You must run make extract_translations first.
|
||||
push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
|
||||
# Writing out comments to file...
|
||||
./src/i18n/i18n-concat.js ./temp/src --comments
|
||||
# Adding comments to Transifex...
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
|
||||
|
||||
# pull translations from Transifex
|
||||
pull_translations: ## must be exactly this name for edx tooling support, see ecommerce-scripts/transifex/pull.py
|
||||
tx pull -f --mode reviewed --language="ar,fr,es_419,zh_CN"
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -f --mode reviewed --language=$(transifex_langs)
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
# Checking for package-lock.json changes...
|
||||
git diff --exit-code package-lock.json
|
||||
|
||||
111
README.rst
111
README.rst
@@ -1,22 +1,117 @@
|
||||
|Build Status| |Coveralls| |npm_version| |npm_downloads| |license|
|
||||
|ci-badge| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
|
||||
|
||||
frontend-app-account
|
||||
=========================
|
||||
====================
|
||||
|
||||
Please tag **@edx/arch-team** on any PRs or issues.
|
||||
Please tag **@edx/community-engineering** on any PRs or issues. Thanks!
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
React app for account settings.
|
||||
This is a micro-frontend application responsible for the display and updating of a user's account information.
|
||||
|
||||
.. |Build Status| image:: https://api.travis-ci.org/edx/frontend-app-account.svg?branch=master
|
||||
:target: https://travis-ci.org/edx/frontend-app-account
|
||||
.. |Coveralls| image:: https://img.shields.io/coveralls/edx/frontend-app-account.svg?branch=master
|
||||
:target: https://coveralls.io/github/edx/frontend-app-account
|
||||
What is the domain of this MFE?
|
||||
|
||||
In this MFE: Private user settings UIs. Public facing profile is in a `separate MFE (Profile) <https://github.com/edx/frontend-app-profile>`_
|
||||
|
||||
- Account settings page
|
||||
|
||||
- Demographics collection
|
||||
|
||||
- IDV (Identity Verification)
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
This MFE is bundled with `Devstack <https://github.com/edx/devstack>`_, see the `Getting Started <https://github.com/edx/devstack#getting-started>`_ section for setup instructions.
|
||||
|
||||
1. Install Devstack using the `Getting Started <https://github.com/edx/devstack#getting-started>`_ instructions.
|
||||
|
||||
2. Start up Devstack, if it's not already started.
|
||||
|
||||
3. Log in to Devstack (http://localhost:18000/login )
|
||||
|
||||
4. Within this project, install requirements and start the development server:
|
||||
|
||||
.. code-block::
|
||||
|
||||
npm install
|
||||
npm start # The server will run on port 1997
|
||||
|
||||
5. Once the dev server is up, visit http://localhost:1997 to access the MFE
|
||||
|
||||
.. image:: ./docs/images/localhost_preview.png
|
||||
|
||||
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://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
|
||||
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/edx/edx-developer-docs/actions/workflows/ci.yml/badge.svg
|
||||
:target: https://github.com/edx/edx-developer-docs/actions/workflows/ci.yml
|
||||
:alt: Continuous Integration
|
||||
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-app-account
|
||||
:target: https://codecov.io/gh/edx/frontend-app-account
|
||||
.. |npm_version| image:: https://img.shields.io/npm/v/@edx/frontend-app-account.svg
|
||||
:target: @edx/frontend-app-account
|
||||
.. |npm_downloads| image:: https://img.shields.io/npm/dt/@edx/frontend-app-account.svg
|
||||
:target: @edx/frontend-app-account
|
||||
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-account.svg
|
||||
:target: @edx/frontend-app-account
|
||||
.. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
|
||||
:target: https://github.com/semantic-release/semantic-release
|
||||
|
||||
30
docs/decisions/0002-coaching-addition.rst
Normal file
30
docs/decisions/0002-coaching-addition.rst
Normal file
@@ -0,0 +1,30 @@
|
||||
1. Add Coaching Consent
|
||||
--------------------------------
|
||||
|
||||
Status
|
||||
------
|
||||
|
||||
Accepted
|
||||
|
||||
Context
|
||||
-------
|
||||
|
||||
We need to provide users who are eligible for coaching with both an always available
|
||||
coaching toggle and a one-time form they can view to signup for coaching.
|
||||
|
||||
Decision
|
||||
--------
|
||||
|
||||
While the coaching functionality is currently both limited, closed source, and the form
|
||||
exists outside of the standard design of this MFE, it was decided to add it here as a
|
||||
temporary measure due to it being at it's core, an account setting.
|
||||
|
||||
The longer term solutions include either:
|
||||
- using the frontend plugins feature when they become available to inject our coaching
|
||||
work into the account MFE
|
||||
- roll it into it's own MFE if enough additional coaching frontend work is required
|
||||
|
||||
Consequences
|
||||
------------
|
||||
|
||||
Code will exist inside this Open edX MFE that integrates with a closed source app.
|
||||
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 |
7
jest.config.js
Normal file
7
jest.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFiles: [
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
});
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
nick: acct
|
||||
oeps: {}
|
||||
owner: edx/arch-team
|
||||
openedx-release: {ref: master}
|
||||
|
||||
29080
package-lock.json
generated
29080
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
198
package.json
Executable file → Normal file
198
package.json
Executable file → Normal file
@@ -1,142 +1,94 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-account",
|
||||
"version": "0.1.0",
|
||||
"description": "User account React app",
|
||||
"version": "1.0.0-semantically-released",
|
||||
"description": "User account micro-frontend for Open edX",
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-account.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production BABEL_ENV=production webpack --config=webpack/webpack.prod.config.js",
|
||||
"coveralls": "cat ./coverage/lcov.info | coveralls",
|
||||
"i18n_extract": "BABEL_ENV=i18n babel src --quiet > /dev/null",
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"lint": "eslint --ext .js --ext .jsx .",
|
||||
"precommit": "npm run lint",
|
||||
"start": "NODE_ENV=development BABEL_ENV=development webpack-dev-server --config=webpack/webpack.dev.config.js --progress",
|
||||
"test": "jest --coverage --passWithNoTests",
|
||||
"travis-deploy-once": "travis-deploy-once"
|
||||
"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"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/edx/frontend-app-account/issues"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "https://github.com/edx/frontend-app-account#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
"ie 11"
|
||||
],
|
||||
"dependencies": {
|
||||
"@cospired/i18n-iso-languages": "^2.0.2",
|
||||
"@edx/edx-bootstrap": "^2.0.1",
|
||||
"@edx/frontend-analytics": "^1.0.0",
|
||||
"@edx/frontend-auth": "^5.0.0",
|
||||
"@edx/frontend-component-footer": "^2.0.3",
|
||||
"@edx/frontend-component-site-header": "^2.1.4",
|
||||
"@edx/frontend-logging": "^2.0.0",
|
||||
"@edx/paragon": "^4.1.3",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.17",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.7.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.8.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"classnames": "^2.2.6",
|
||||
"connected-react-router": "^5.0.1",
|
||||
"email-prop-type": "^1.1.5",
|
||||
"font-awesome": "^4.7.0",
|
||||
"form-urlencoded": "^3.0.0",
|
||||
"glob": "^7.1.3",
|
||||
"history": "^4.7.2",
|
||||
"i18n-iso-countries": "^3.7.8",
|
||||
"iso-countries-languages": "^0.2.1",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.pick": "^4.4.0",
|
||||
"lodash.snakecase": "^4.1.1",
|
||||
"newrelic": "^5.5.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.8.3",
|
||||
"react-dom": "^16.8.3",
|
||||
"react-intl": "^2.8.0",
|
||||
"react-redux": "^5.1.1",
|
||||
"react-router": "^4.2.0",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-transition-group": "^2.5.3",
|
||||
"redux": "^4.0.1",
|
||||
"redux-devtools-extension": "^2.13.2",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-saga": "^1.0.1",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"reselect": "^4.0.0",
|
||||
"universal-cookie": "^3.1.0",
|
||||
"webpack-rtl-plugin": "^2.0.0"
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "10.1.6",
|
||||
"@edx/frontend-component-header": "2.3.0",
|
||||
"@edx/frontend-platform": "1.12.6",
|
||||
"@edx/paragon": "16.1.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.1.15",
|
||||
"@tensorflow-models/blazeface": "0.0.7",
|
||||
"@tensorflow/tfjs-converter": "1.7.4",
|
||||
"@tensorflow/tfjs-core": "1.7.4",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"bowser": "2.11.0",
|
||||
"classnames": "2.3.1",
|
||||
"font-awesome": "4.7.0",
|
||||
"form-urlencoded": "4.0.1",
|
||||
"formdata-polyfill": "4.0.7",
|
||||
"history": "4.10.1",
|
||||
"jslib-html5-camera-photo": "3.1.8",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.findindex": "4.6.0",
|
||||
"lodash.get": "4.4.2",
|
||||
"lodash.isempty": "4.4.0",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.omit": "4.5.0",
|
||||
"lodash.pick": "4.4.0",
|
||||
"lodash.pickby": "4.6.0",
|
||||
"memoize-one": "5.2.1",
|
||||
"newrelic": "5.13.1",
|
||||
"prop-types": "15.7.2",
|
||||
"qs": "6.10.1",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-redux": "7.2.5",
|
||||
"react-router": "5.2.1",
|
||||
"react-router-dom": "5.3.0",
|
||||
"react-router-hash-link": "1.2.2",
|
||||
"react-scrollspy": "3.4.3",
|
||||
"react-transition-group": "4.4.2",
|
||||
"redux": "4.1.1",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-saga": "1.1.3",
|
||||
"redux-thunk": "2.3.0",
|
||||
"reselect": "4.0.0",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^4.2.0",
|
||||
"autoprefixer": "^9.4.2",
|
||||
"axios-mock-adapter": "^1.15.0",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-eslint": "^8.2.2",
|
||||
"babel-jest": "^22.4.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-react-intl": "^3.0.1",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"codecov": "^3.0.0",
|
||||
"copy-webpack-plugin": "^4.6.0",
|
||||
"css-loader": "^0.28.9",
|
||||
"enzyme": "^3.3.0",
|
||||
"enzyme-adapter-react-16": "^1.1.1",
|
||||
"es-check": "^2.0.2",
|
||||
"eslint-config-edx": "^4.0.3",
|
||||
"fetch-mock": "^6.3.0",
|
||||
"file-loader": "^1.1.9",
|
||||
"html-webpack-harddisk-plugin": "^0.2.0",
|
||||
"html-webpack-new-relic-plugin": "^1.1.0",
|
||||
"html-webpack-plugin": "^3.0.3",
|
||||
"husky": "^0.14.3",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"image-webpack-loader": "^4.2.0",
|
||||
"jest": "^22.4.0",
|
||||
"mini-css-extract-plugin": "^0.4.0",
|
||||
"node-sass": "^4.7.2",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"react-dev-utils": "^5.0.0",
|
||||
"react-test-renderer": "^16.8.6",
|
||||
"@edx/frontend-build": "8.0.4",
|
||||
"@testing-library/jest-dom": "5.14.1",
|
||||
"@testing-library/react": "12.0.0",
|
||||
"codecov": "3.8.3",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.6",
|
||||
"es-check": "6.0.0",
|
||||
"husky": "7.0.2",
|
||||
"react-test-renderer": "16.14.0",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "^1.5.3",
|
||||
"sass-loader": "^6.0.6",
|
||||
"style-loader": "^0.20.2",
|
||||
"travis-deploy-once": "^5.0.9",
|
||||
"url-loader": "^1.1.2",
|
||||
"webpack": "^4.25.1",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.1.0",
|
||||
"webpack-merge": "^4.1.1"
|
||||
},
|
||||
"jest": {
|
||||
"testURL": "http://localhost/",
|
||||
"setupFiles": [
|
||||
"./src/setupTest.js"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.svg": "<rootDir>/__mocks__/svgrMock.js",
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
|
||||
"\\.(css|scss)$": "identity-obj-proxy",
|
||||
"@edx/frontend-i18n(.*)$": "<rootDir>/src/i18n$1"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{js,jsx}"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"/node_modules/",
|
||||
"src/setupTest.js",
|
||||
"src/index.js",
|
||||
"/tests/"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"/node_modules/(?!(@edx/paragon)/).*/"
|
||||
]
|
||||
"redux-mock-store": "1.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +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" />
|
||||
<% 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>
|
||||
|
||||
28
renovate.json
Normal file
28
renovate.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base",
|
||||
"schedule:weekly",
|
||||
":automergeLinters",
|
||||
":automergeMinor",
|
||||
":automergeTesters",
|
||||
":enableVulnerabilityAlerts",
|
||||
":rebaseStalePrs",
|
||||
":semanticCommits",
|
||||
":updateNotScheduled"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchDepTypes": [
|
||||
"devDependencies"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"lockFileMaintenance",
|
||||
"minor",
|
||||
"patch",
|
||||
"pin"
|
||||
],
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"timezone": "America/New_York"
|
||||
}
|
||||
@@ -1,131 +1,661 @@
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { getConfig, history, getQueryParameters } from '@edx/frontend-platform';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import memoize from 'memoize-one';
|
||||
import findIndex from 'lodash.findindex';
|
||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
injectIntl,
|
||||
intlShape,
|
||||
getLocale,
|
||||
FormattedMessage,
|
||||
getCountryList,
|
||||
getLanguageList,
|
||||
} from '@edx/frontend-i18n'; // eslint-disable-line
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Hyperlink, Icon, Alert,
|
||||
} from '@edx/paragon';
|
||||
import { CheckCircle, Error, WarningFilled } from '@edx/paragon/icons';
|
||||
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
|
||||
import { fetchAccount, fetchThirdPartyAuthProviders } from './actions';
|
||||
import { accountSettingsSelector } from './selectors';
|
||||
|
||||
import { PageLoading } from '../common';
|
||||
import EditableField from './components/EditableField';
|
||||
import PasswordReset from './components/PasswordReset';
|
||||
import ThirdPartyAuth from './components/ThirdPartyAuth';
|
||||
import EmailField from './components/EmailField';
|
||||
|
||||
import { fetchSettings, saveSettings, updateDraft } from './data/actions';
|
||||
import { accountSettingsPageSelector } from './data/selectors';
|
||||
import PageLoading from './PageLoading';
|
||||
import JumpNav from './JumpNav';
|
||||
import DeleteAccount from './delete-account';
|
||||
import EditableField from './EditableField';
|
||||
import ResetPassword from './reset-password';
|
||||
import ThirdPartyAuth from './third-party-auth';
|
||||
import BetaLanguageBanner from './BetaLanguageBanner';
|
||||
import EmailField from './EmailField';
|
||||
import OneTimeDismissibleAlert from './OneTimeDismissibleAlert';
|
||||
import {
|
||||
YEAR_OF_BIRTH_OPTIONS,
|
||||
EDUCATION_LEVELS,
|
||||
GENDER_OPTIONS,
|
||||
} from './constants';
|
||||
|
||||
COUNTRY_WITH_STATES,
|
||||
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) {
|
||||
super(props);
|
||||
this.countryOptions = getCountryList(getLocale())
|
||||
.map(({ code, name }) => ({ value: code, label: name }));
|
||||
this.languageProficiencyOptions = getLanguageList(getLocale())
|
||||
.map(({ code, name }) => ({ value: code, label: name }));
|
||||
this.educationLevels = EDUCATION_LEVELS.map(key => ({
|
||||
value: key,
|
||||
label: props.intl.formatMessage(messages[`account.settings.field.education.levels.${key}`]),
|
||||
}));
|
||||
this.genderOptions = GENDER_OPTIONS.map(key => ({
|
||||
value: key,
|
||||
label: props.intl.formatMessage(messages[`account.settings.field.gender.options.${key}`]),
|
||||
}));
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
// If there is a "duplicate_provider" query parameter, that's the backend's
|
||||
// way of telling us that the provider account the user tried to link is already linked
|
||||
// to another user account on the platform. We use this to display a message to that effect,
|
||||
// and remove the parameter from the URL.
|
||||
const duplicateTpaProvider = getQueryParameters().duplicate_provider;
|
||||
if (duplicateTpaProvider !== undefined) {
|
||||
history.replace(history.location.pathname);
|
||||
}
|
||||
this.state = {
|
||||
duplicateTpaProvider,
|
||||
};
|
||||
|
||||
this.navLinkRefs = {
|
||||
'#basic-information': React.createRef(),
|
||||
'#profile-information': React.createRef(),
|
||||
'#demographics-information': React.createRef(),
|
||||
'#social-media': React.createRef(),
|
||||
'#site-preferences': React.createRef(),
|
||||
'#linked-accounts': React.createRef(),
|
||||
'#delete-account': React.createRef(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchAccount();
|
||||
this.props.fetchThirdPartyAuthProviders();
|
||||
this.props.fetchSettings();
|
||||
this.props.fetchSiteLanguages();
|
||||
sendTrackingLogEvent('edx.user.settings.viewed', {
|
||||
page: 'account',
|
||||
visibility: null,
|
||||
user_id: this.context.authenticatedUser.userId,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.loading && !prevProps.loaded && this.props.loaded) {
|
||||
const locationHash = global.location.hash;
|
||||
// Check for the locationHash in the URL and then scroll to it if it is in the
|
||||
// NavLinks list
|
||||
if (typeof locationHash !== 'string') {
|
||||
return;
|
||||
}
|
||||
if (Object.keys(this.navLinkRefs).includes(locationHash) && this.navLinkRefs[locationHash].current) {
|
||||
window.scrollTo(0, this.navLinkRefs[locationHash].current.offsetTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: We need 'locale' for the memoization in getLocalizedTimeZoneOptions. Don't remove it!
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
getLocalizedTimeZoneOptions = memoize((timeZoneOptions, countryTimeZoneOptions, locale) => {
|
||||
const concatTimeZoneOptions = [{
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.time.zone.default']),
|
||||
value: '',
|
||||
}];
|
||||
if (countryTimeZoneOptions.length) {
|
||||
concatTimeZoneOptions.push({
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.time.zone.country']),
|
||||
group: countryTimeZoneOptions,
|
||||
});
|
||||
}
|
||||
concatTimeZoneOptions.push({
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.time.zone.all']),
|
||||
group: timeZoneOptions,
|
||||
});
|
||||
return concatTimeZoneOptions;
|
||||
});
|
||||
|
||||
getLocalizedOptions = memoize((locale, country) => ({
|
||||
countryOptions: [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.country.options.empty']),
|
||||
}].concat(getCountryList(locale).map(({ code, name }) => ({ value: code, label: name }))),
|
||||
stateOptions: [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.state.options.empty']),
|
||||
}].concat(getStatesList(country)),
|
||||
languageProficiencyOptions: [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.language_proficiencies.options.empty']),
|
||||
}].concat(getLanguageList(locale).map(({ code, name }) => ({ value: code, label: name }))),
|
||||
yearOfBirthOptions: [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.year_of_birth.options.empty']),
|
||||
}].concat(YEAR_OF_BIRTH_OPTIONS),
|
||||
educationLevelOptions: EDUCATION_LEVELS.map(key => ({
|
||||
value: key,
|
||||
label: this.props.intl.formatMessage(messages[`account.settings.field.education.levels.${key || 'empty'}`]),
|
||||
})),
|
||||
genderOptions: GENDER_OPTIONS.map(key => ({
|
||||
value: key,
|
||||
label: this.props.intl.formatMessage(messages[`account.settings.field.gender.options.${key || 'empty'}`]),
|
||||
})),
|
||||
}));
|
||||
|
||||
sortDates = (a, b) => {
|
||||
const aTimeSinceEpoch = new Date(a).getTime();
|
||||
const bTimeSinceEpoch = new Date(b).getTime();
|
||||
|
||||
return bTimeSinceEpoch - aTimeSinceEpoch;
|
||||
}
|
||||
|
||||
sortVerifiedNameRecords = verifiedNameHistory => {
|
||||
if (Array.isArray(verifiedNameHistory)) {
|
||||
return [...verifiedNameHistory].sort(this.sortDates);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
handleEditableFieldChange = (name, value) => {
|
||||
this.props.updateDraft(name, value);
|
||||
}
|
||||
|
||||
handleSubmit = (formId, values) => {
|
||||
this.props.saveSettings(formId, values);
|
||||
}
|
||||
|
||||
isEditable(fieldName) {
|
||||
return !this.props.staticFields.includes(fieldName);
|
||||
}
|
||||
|
||||
isManagedProfile() {
|
||||
// Enterprise customer profiles are managed by their organizations. We determine whether
|
||||
// a profile is managed or not by the presence of the profileDataManager prop.
|
||||
return Boolean(this.props.profileDataManager);
|
||||
}
|
||||
|
||||
renderDuplicateTpaProviderMessage() {
|
||||
if (!this.state.duplicateTpaProvider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Alert variant="danger">
|
||||
<FormattedMessage
|
||||
id="account.settings.message.duplicate.tpa.provider"
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderManagedProfileMessage() {
|
||||
if (!this.isManagedProfile()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<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."
|
||||
description="alert message informing the user their account data is managed by a third party"
|
||||
values={{
|
||||
managerTitle: <b>{this.props.profileDataManager}</b>,
|
||||
support: (
|
||||
<Hyperlink destination={getConfig().SUPPORT_URL} target="_blank">
|
||||
<FormattedMessage
|
||||
id="account.settings.message.managed.settings.support"
|
||||
defaultMessage="support"
|
||||
description="website support"
|
||||
/>
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderVerifiedNameSuccessMessage = () => (
|
||||
<OneTimeDismissibleAlert
|
||||
id="dismissedVerifiedNameSuccessMessage"
|
||||
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'], {
|
||||
verifiedName,
|
||||
},
|
||||
)}
|
||||
</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 = () => (
|
||||
<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'])}
|
||||
</p>
|
||||
</Alert>
|
||||
)
|
||||
|
||||
renderVerifiedNameMessage = verifiedNameRecord => {
|
||||
const { created, status, verified_name: verifiedName } = verifiedNameRecord;
|
||||
|
||||
switch (status) {
|
||||
case 'approved':
|
||||
return this.renderVerifiedNameSuccessMessage();
|
||||
case 'denied':
|
||||
return this.renderVerifiedNameFailureMessage(verifiedName, created);
|
||||
case 'submitted':
|
||||
return this.renderVerifiedNameSubmittedMessage();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
renderVerifiedNameIcon = (status) => {
|
||||
switch (status) {
|
||||
case 'approved':
|
||||
return (<Icon src={CheckCircle} className="ml-1" style={{ height: '18px', width: '18px', color: 'green' }} />);
|
||||
case 'submitted':
|
||||
return (<Icon src={WarningFilled} className="ml-1" style={{ height: '18px', width: '18px', color: 'yellow' }} />);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
renderVerifiedNameHelpText = (status) => {
|
||||
switch (status) {
|
||||
case 'approved':
|
||||
return (this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.verified']));
|
||||
case 'submitted':
|
||||
return (this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.submitted']));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
renderFullNameHelpText = (status) => {
|
||||
switch (status) {
|
||||
case 'submitted':
|
||||
return (this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.submitted']));
|
||||
default:
|
||||
return (this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text']));
|
||||
}
|
||||
}
|
||||
|
||||
renderEmptyStaticFieldMessage() {
|
||||
if (this.isManagedProfile()) {
|
||||
return this.props.intl.formatMessage(messages['account.settings.static.field.empty'], {
|
||||
enterprise: this.props.profileDataManager,
|
||||
});
|
||||
}
|
||||
return this.props.intl.formatMessage(messages['account.settings.static.field.empty.no.admin']);
|
||||
}
|
||||
|
||||
renderSecondaryEmailField(editableFieldProps) {
|
||||
if (!this.props.formValues.secondary_email_enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EmailField
|
||||
name="secondary_email"
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.secondary.email'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.secondary.email.empty'])}
|
||||
value={this.props.formValues.secondary_email}
|
||||
confirmationMessageDefinition={messages['account.settings.field.secondary.email.confirmation']}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderDemographicsSection() {
|
||||
// check the result of an LMS API call to determine if we should render the DemographicsSection component
|
||||
if (this.props.formValues.shouldDisplayDemographicsSection) {
|
||||
return (
|
||||
<DemographicsSection forwardRef={this.navLinkRefs['#demographics-information']} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const editableFieldProps = {
|
||||
onChange: this.handleEditableFieldChange,
|
||||
onSubmit: this.handleSubmit,
|
||||
};
|
||||
|
||||
// Memoized options lists
|
||||
const {
|
||||
countryOptions,
|
||||
stateOptions,
|
||||
languageProficiencyOptions,
|
||||
yearOfBirthOptions,
|
||||
educationLevelOptions,
|
||||
genderOptions,
|
||||
} = 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 { verifiedName } = this.props.formValues;
|
||||
const verifiedNameEnabled = this.props.formValues.verifiedNameHistory.verified_name_enabled;
|
||||
|
||||
const timeZoneOptions = this.getLocalizedTimeZoneOptions(
|
||||
this.props.timeZoneOptions,
|
||||
this.props.countryTimeZoneOptions,
|
||||
this.context.locale,
|
||||
);
|
||||
|
||||
const hasLinkedTPA = findIndex(this.props.tpaProviders, provider => provider.connected) >= 0;
|
||||
return (
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col-md-8 col-lg-6">
|
||||
<h2>{this.props.intl.formatMessage(messages['account.settings.section.account.information'])}</h2>
|
||||
<p>{this.props.intl.formatMessage(messages['account.settings.section.account.information.description'])}</p>
|
||||
<>
|
||||
<div className="account-section" id="basic-information" ref={this.navLinkRefs['#basic-information']}>
|
||||
{verifiedNameEnabled && this.renderVerifiedNameMessage(this.props.formValues.mostRecentVerifiedName)}
|
||||
|
||||
<EditableField
|
||||
name="username"
|
||||
type="text"
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.username'])}
|
||||
isEditable={false}
|
||||
/>
|
||||
<EditableField
|
||||
name="name"
|
||||
type="text"
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])}
|
||||
/>
|
||||
<EmailField
|
||||
name="email"
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.email'])}
|
||||
confirmationMessageDefinition={messages['account.settings.field.email.confirmation']}
|
||||
/>
|
||||
<EditableField
|
||||
name="year_of_birth"
|
||||
type="select"
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.dob'])}
|
||||
options={YEAR_OF_BIRTH_OPTIONS}
|
||||
/>
|
||||
<PasswordReset />
|
||||
<EditableField
|
||||
name="country"
|
||||
type="select"
|
||||
options={this.countryOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.country'])}
|
||||
/>
|
||||
<EditableField
|
||||
name="level_of_education"
|
||||
type="select"
|
||||
options={this.educationLevels}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.education'])}
|
||||
/>
|
||||
<EditableField
|
||||
name="gender"
|
||||
type="select"
|
||||
options={this.genderOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.gender'])}
|
||||
/>
|
||||
<EditableField
|
||||
name="language_proficiencies"
|
||||
type="select"
|
||||
options={this.languageProficiencyOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies'])}
|
||||
/>
|
||||
<ThirdPartyAuth />
|
||||
<h2 className="section-heading">
|
||||
{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()}
|
||||
|
||||
<h2>{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}</h2>
|
||||
<p>{this.props.intl.formatMessage(messages['account.settings.section.social.media.description'])}</p>
|
||||
<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'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
isEditable={false}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="name"
|
||||
type="text"
|
||||
value={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()
|
||||
}
|
||||
helpText={
|
||||
verifiedNameEnabled && verifiedName
|
||||
? this.renderFullNameHelpText(verifiedName.status)
|
||||
: this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])
|
||||
}
|
||||
isEditable={
|
||||
verifiedNameEnabled && verifiedName
|
||||
? this.isEditable('verifiedName') && this.isEditable('name')
|
||||
: this.isEditable('name')
|
||||
}
|
||||
isGrayedOut={
|
||||
verifiedNameEnabled && verifiedName && !this.isEditable('verifiedName')
|
||||
}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
{verifiedNameEnabled && verifiedName
|
||||
&& (
|
||||
<EditableField
|
||||
name="social_link_linkedIn"
|
||||
name="verifiedName"
|
||||
type="text"
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.linkedin'])}
|
||||
value={this.props.formValues.verifiedName.verified_name}
|
||||
label={
|
||||
(
|
||||
<div className="d-flex">
|
||||
{this.props.intl.formatMessage(messages['account.settings.field.name.verified'])}
|
||||
{
|
||||
this.renderVerifiedNameIcon(verifiedName.status)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
helpText={this.renderVerifiedNameHelpText(verifiedName.status)}
|
||||
isEditable={this.isEditable('verifiedName')}
|
||||
isGrayedOut={!this.isEditable('verifiedName')}
|
||||
{...(this.isEditable('verifiedName') && editableFieldProps)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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()
|
||||
}
|
||||
value={this.props.formValues.email}
|
||||
confirmationMessageDefinition={messages['account.settings.field.email.confirmation']}
|
||||
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
|
||||
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()
|
||||
}
|
||||
isEditable={this.isEditable('country')}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
{showState
|
||||
&& (
|
||||
<EditableField
|
||||
name="social_link_facebook"
|
||||
type="text"
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.facebook'])}
|
||||
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()
|
||||
}
|
||||
isEditable={this.isEditable('state')}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="social_link_twitter"
|
||||
type="text"
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.twitter'])}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="account-section" id="profile-information" ref={this.navLinkRefs['#profile-information']}>
|
||||
<h2 className="section-heading">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.profile.information'])}
|
||||
</h2>
|
||||
|
||||
<EditableField
|
||||
name="level_of_education"
|
||||
type="select"
|
||||
value={this.props.formValues.level_of_education}
|
||||
options={educationLevelOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.education'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.education.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="gender"
|
||||
type="select"
|
||||
value={this.props.formValues.gender}
|
||||
options={genderOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.gender'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.gender.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="language_proficiencies"
|
||||
type="select"
|
||||
value={this.props.formValues.language_proficiencies}
|
||||
options={languageProficiencyOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
{getConfig().COACHING_ENABLED
|
||||
&& this.props.formValues.coaching.eligible_for_coaching
|
||||
&& (
|
||||
<CoachingToggle
|
||||
name="coaching"
|
||||
phone_number={this.props.formValues.phone_number}
|
||||
coaching={this.props.formValues.coaching}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && this.renderDemographicsSection()}
|
||||
<div className="account-section" id="social-media">
|
||||
<h2 className="section-heading">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}
|
||||
</h2>
|
||||
<p>
|
||||
{this.props.intl.formatMessage(
|
||||
messages['account.settings.section.social.media.description'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
|
||||
<EditableField
|
||||
name="social_link_linkedin"
|
||||
type="text"
|
||||
value={this.props.formValues.social_link_linkedin}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.linkedin'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.linkedin.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="social_link_facebook"
|
||||
type="text"
|
||||
value={this.props.formValues.social_link_facebook}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.facebook'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.facebook.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="social_link_twitter"
|
||||
type="text"
|
||||
value={this.props.formValues.social_link_twitter}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.twitter'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.twitter.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="account-section" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}>
|
||||
<h2 className="section-heading">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.site.preferences'])}
|
||||
</h2>
|
||||
|
||||
<BetaLanguageBanner />
|
||||
<EditableField
|
||||
name="siteLanguage"
|
||||
type="select"
|
||||
options={this.props.siteLanguageOptions}
|
||||
value={this.props.siteLanguage.draft !== undefined ? this.props.siteLanguage.draft : this.context.locale}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.site.language'])}
|
||||
helpText={this.props.intl.formatMessage(messages['account.settings.field.site.language.help.text'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="time_zone"
|
||||
type="select"
|
||||
value={this.props.formValues.time_zone}
|
||||
options={timeZoneOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.time.zone'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.time.zone.empty'])}
|
||||
helpText={this.props.intl.formatMessage(messages['account.settings.field.time.zone.description'])}
|
||||
{...editableFieldProps}
|
||||
onSubmit={(formId, value) => {
|
||||
// the endpoint will not accept an empty string. it must be null
|
||||
this.handleSubmit(formId, value || null);
|
||||
}}
|
||||
/>
|
||||
</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'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<ThirdPartyAuth />
|
||||
</div>
|
||||
|
||||
<div className="account-section" id="delete-account" ref={this.navLinkRefs['#delete-account']}>
|
||||
<DeleteAccount
|
||||
isVerifiedAccount={this.props.isActive}
|
||||
hasLinkedTPA={hasLinkedTPA}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,34 +684,126 @@ class AccountSettingsPage extends React.Component {
|
||||
|
||||
return (
|
||||
<div className="page__account-settings container-fluid py-5">
|
||||
<h1>
|
||||
{this.renderDuplicateTpaProviderMessage()}
|
||||
<h1 className="mb-4">
|
||||
{this.props.intl.formatMessage(messages['account.settings.page.heading'])}
|
||||
</h1>
|
||||
{loading ? this.renderLoading() : null}
|
||||
{loaded ? this.renderContent() : null}
|
||||
{loadingError ? this.renderError() : null}
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<JumpNav
|
||||
displayDemographicsLink={this.props.formValues.shouldDisplayDemographicsSection}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-9">
|
||||
{loading ? this.renderLoading() : null}
|
||||
{loaded ? this.renderContent() : null}
|
||||
{loadingError ? this.renderError() : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AccountSettingsPage.contextType = AppContext;
|
||||
|
||||
AccountSettingsPage.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
loading: PropTypes.bool,
|
||||
loaded: PropTypes.bool,
|
||||
loadingError: PropTypes.string,
|
||||
fetchAccount: PropTypes.func.isRequired,
|
||||
fetchThirdPartyAuthProviders: PropTypes.func.isRequired,
|
||||
|
||||
// Form data
|
||||
formValues: PropTypes.shape({
|
||||
username: PropTypes.string,
|
||||
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,
|
||||
phone_number: PropTypes.string,
|
||||
social_link_linkedin: PropTypes.string,
|
||||
social_link_facebook: PropTypes.string,
|
||||
social_link_twitter: PropTypes.string,
|
||||
time_zone: PropTypes.string,
|
||||
coaching: PropTypes.shape({
|
||||
coaching_consent: PropTypes.bool.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
}),
|
||||
state: PropTypes.string,
|
||||
shouldDisplayDemographicsSection: PropTypes.bool,
|
||||
verifiedNameHistory: PropTypes.shape({
|
||||
verified_name_enabled: PropTypes.bool,
|
||||
use_verified_name_for_certs: PropTypes.bool,
|
||||
results: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
verified_name: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
verifiedName: PropTypes.shape({
|
||||
verified_name: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
}),
|
||||
mostRecentVerifiedName: PropTypes.shape({
|
||||
verified_name: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
}),
|
||||
}).isRequired,
|
||||
siteLanguage: PropTypes.shape({
|
||||
previousValue: PropTypes.string,
|
||||
draft: PropTypes.string,
|
||||
}),
|
||||
siteLanguageOptions: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
})),
|
||||
profileDataManager: PropTypes.string,
|
||||
staticFields: PropTypes.arrayOf(PropTypes.string),
|
||||
isActive: PropTypes.bool,
|
||||
secondary_email_enabled: PropTypes.bool,
|
||||
|
||||
timeZoneOptions: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
})),
|
||||
countryTimeZoneOptions: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
})),
|
||||
fetchSiteLanguages: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
fetchSettings: PropTypes.func.isRequired,
|
||||
tpaProviders: PropTypes.arrayOf(PropTypes.object),
|
||||
};
|
||||
|
||||
AccountSettingsPage.defaultProps = {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
loadingError: null,
|
||||
siteLanguage: null,
|
||||
siteLanguageOptions: [],
|
||||
timeZoneOptions: [],
|
||||
countryTimeZoneOptions: [],
|
||||
profileDataManager: null,
|
||||
staticFields: [],
|
||||
tpaProviders: [],
|
||||
isActive: true,
|
||||
secondary_email_enabled: false,
|
||||
};
|
||||
|
||||
|
||||
export default connect(accountSettingsSelector, {
|
||||
fetchAccount,
|
||||
fetchThirdPartyAuthProviders,
|
||||
export default connect(accountSettingsPageSelector, {
|
||||
fetchSettings,
|
||||
saveSettings,
|
||||
updateDraft,
|
||||
fetchSiteLanguages,
|
||||
})(injectIntl(AccountSettingsPage));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from 'react-intl';
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.page.heading': {
|
||||
@@ -8,7 +8,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'account.settings.loading.message': {
|
||||
id: 'account.settings.loading.message',
|
||||
defaultMessage: 'Loading',
|
||||
defaultMessage: 'Loading...',
|
||||
description: 'Message when data is being loaded',
|
||||
},
|
||||
'account.settings.loading.error': {
|
||||
@@ -16,6 +16,21 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Error: {error}',
|
||||
description: 'Message when data failed to load',
|
||||
},
|
||||
'account.settings.banner.beta.language': {
|
||||
id: 'account.settings.banner.beta.language',
|
||||
defaultMessage: '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}.',
|
||||
description: 'Message when the user selects a beta language this is not yet fully translated.',
|
||||
},
|
||||
'account.settings.banner.beta.language.action.switch.back': {
|
||||
id: 'account.settings.banner.beta.language.action.switch.back',
|
||||
defaultMessage: 'Switch Back to {previous_language}',
|
||||
description: 'Button on the beta language message to switch back to the previous language.',
|
||||
},
|
||||
'account.settings.banner.beta.language.action.help.translate': {
|
||||
id: 'account.settings.banner.beta.language.action.help.translate',
|
||||
defaultMessage: 'Help Translate into {beta_language}',
|
||||
description: 'Button on the beta language message to help translate the beta language.',
|
||||
},
|
||||
'account.settings.section.account.information': {
|
||||
id: 'account.settings.section.account.information',
|
||||
defaultMessage: 'Account Information',
|
||||
@@ -26,26 +41,146 @@ const messages = defineMessages({
|
||||
defaultMessage: 'These settings include basic information about your account.',
|
||||
description: 'The basic account information section heading description.',
|
||||
},
|
||||
'account.settings.section.profile.information': {
|
||||
id: 'account.settings.section.profile.information',
|
||||
defaultMessage: 'Profile Information',
|
||||
description: 'The profile information section heading.',
|
||||
},
|
||||
'account.settings.section.demographics.information': {
|
||||
id: 'account.settings.section.demographics.information',
|
||||
defaultMessage: 'Optional Information',
|
||||
description: 'The optional information section heading.',
|
||||
},
|
||||
'account.settings.section.site.preferences': {
|
||||
id: 'account.settings.section.site.preferences',
|
||||
defaultMessage: 'Site Preferences',
|
||||
description: 'The site preferences section heading.',
|
||||
},
|
||||
'account.settings.section.linked.accounts': {
|
||||
id: 'account.settings.section.linked.accounts',
|
||||
defaultMessage: 'Linked Accounts',
|
||||
description: 'The linked accounts section heading.',
|
||||
},
|
||||
'account.settings.section.linked.accounts.description': {
|
||||
id: 'account.settings.section.linked.accounts.description',
|
||||
defaultMessage: 'You can link your identity accounts to simplify signing in to {siteName}.',
|
||||
description: 'The linked accounts section heading description.',
|
||||
},
|
||||
'account.settings.field.username': {
|
||||
id: 'account.settings.field.username',
|
||||
defaultMessage: 'Username',
|
||||
description: 'Label for account settings username field.',
|
||||
},
|
||||
'account.settings.field.username.help.text': {
|
||||
id: 'account.settings.field.username.help.text',
|
||||
defaultMessage: 'The name that identifies you on {siteName}. You cannot change your username.',
|
||||
description: 'Help text for the account settings username field.',
|
||||
},
|
||||
'account.settings.field.full.name': {
|
||||
id: 'account.settings.field.full.name',
|
||||
defaultMessage: 'Full name',
|
||||
description: 'Label for account settings name field.',
|
||||
},
|
||||
'account.settings.field.full.name.empty': {
|
||||
id: 'account.settings.field.full.name.empty',
|
||||
defaultMessage: 'Add name',
|
||||
description: 'Placeholder for empty account settings name field.',
|
||||
},
|
||||
'account.settings.field.full.name.help.text': {
|
||||
id: 'account.settings.field.full.name.help.text',
|
||||
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.name.verified': {
|
||||
id: 'account.settings.field.name.verified',
|
||||
defaultMessage: 'Verified name',
|
||||
description: 'Label for account settings verified name field.',
|
||||
},
|
||||
'account.settings.field.name.verified.help.text.verified': {
|
||||
id: 'account.settings.field.name.verified.help.text.verified',
|
||||
defaultMessage: 'This name has been verified by government ID.',
|
||||
description: 'Help text for the account settings verified name field when the name is verified.',
|
||||
},
|
||||
'account.settings.field.name.verified.help.text.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.full.name.help.text.submitted': {
|
||||
id: 'account.settings.field.full.name.help.text.submitted',
|
||||
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 verified name has been submitted.',
|
||||
},
|
||||
'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 Verified name change attempt, “{verifiedName}”, did not pass ID verification. Your previous Verified name 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. 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 has been submitted for verification',
|
||||
},
|
||||
'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)',
|
||||
description: 'Label for account settings email field.',
|
||||
},
|
||||
'account.settings.field.email.empty': {
|
||||
id: 'account.settings.field.email.empty',
|
||||
defaultMessage: 'Add email address',
|
||||
description: 'Placeholder for empty account settings email field.',
|
||||
},
|
||||
'account.settings.field.email.confirmation': {
|
||||
id: 'account.settings.field.email.confirmation',
|
||||
defaultMessage: 'We’ve sent a confirmation message to {value}. Click the link in the message to update your email address.',
|
||||
description: 'Confirmation message for saving the account settings email field.',
|
||||
},
|
||||
'account.settings.field.email.help.text': {
|
||||
id: 'account.settings.field.email.help.text',
|
||||
defaultMessage: 'You receive messages from {siteName} and course teams at this address.',
|
||||
description: 'Help text for the account settings email field.',
|
||||
},
|
||||
'account.settings.field.secondary.email': {
|
||||
id: 'account.settings.field.secondary.email',
|
||||
defaultMessage: 'Recovery email address',
|
||||
description: 'Label for account settings recovery email field.',
|
||||
},
|
||||
'account.settings.field.secondary.email.empty': {
|
||||
id: 'account.settings.field.secondary.email.empty',
|
||||
defaultMessage: 'Add a recovery email address',
|
||||
description: 'Placeholder for empty account settings recovery email field.',
|
||||
},
|
||||
'account.settings.field.secondary.email.confirmation': {
|
||||
id: 'account.settings.field.secondary.email.confirmation',
|
||||
defaultMessage: 'We’ve sent a confirmation message to {value}. Click the link in the message to update your recovery email address.',
|
||||
description: 'Confirmation message for saving the account settings recovery email field.',
|
||||
},
|
||||
'account.settings.email.field.confirmation.header': {
|
||||
id: 'account.settings.email.field.confirmation.header',
|
||||
defaultMessage: 'One more step!',
|
||||
@@ -56,19 +191,68 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Year of birth',
|
||||
description: 'Label for account settings year of birth field.',
|
||||
},
|
||||
'account.settings.field.dob.empty': {
|
||||
id: 'account.settings.field.dob.empty',
|
||||
defaultMessage: 'Add year of birth',
|
||||
description: 'Placeholder for empty account settings year of birth field.',
|
||||
},
|
||||
'account.settings.field.year_of_birth.options.empty': {
|
||||
id: 'account.settings.field.year_of_birth.options.empty',
|
||||
defaultMessage: 'Select a year of birth',
|
||||
description: 'Option for empty value on account settings year of birth field.',
|
||||
},
|
||||
'account.settings.field.country': {
|
||||
id: 'account.settings.field.country',
|
||||
defaultMessage: 'Country',
|
||||
description: 'Label for account settings country field.',
|
||||
},
|
||||
|
||||
'account.settings.field.country.empty': {
|
||||
id: 'account.settings.field.country.empty',
|
||||
defaultMessage: 'Add country',
|
||||
description: 'Placeholder for empty account settings country field.',
|
||||
},
|
||||
'account.settings.field.country.options.empty': {
|
||||
id: 'account.settings.field.country.options.empty',
|
||||
defaultMessage: 'Select a Country',
|
||||
description: 'Option for empty value on account settings country field.',
|
||||
},
|
||||
'account.settings.field.state': {
|
||||
id: 'account.settings.field.state',
|
||||
defaultMessage: 'State',
|
||||
description: 'Label for account settings state field.',
|
||||
},
|
||||
'account.settings.field.state.empty': {
|
||||
id: 'account.settings.field.state.empty',
|
||||
defaultMessage: 'Add state',
|
||||
description: 'Placeholder for empty account settings state field.',
|
||||
},
|
||||
'account.settings.field.state.options.empty': {
|
||||
id: 'account.settings.field.state.options.empty',
|
||||
defaultMessage: 'Select a State',
|
||||
description: 'Option for empty value on account settings state field.',
|
||||
},
|
||||
'account.settings.field.site.language': {
|
||||
id: 'account.settings.field.site.language',
|
||||
defaultMessage: 'Site language',
|
||||
description: 'Label for account settings site language field.',
|
||||
},
|
||||
'account.settings.field.site.language.help.text': {
|
||||
id: 'account.settings.field.site.language.help.text',
|
||||
defaultMessage: 'The language used throughout this site. This site is currently available in a limited number of languages.',
|
||||
description: 'Help text for the site language field.',
|
||||
},
|
||||
'account.settings.field.education': {
|
||||
id: 'account.settings.field.education',
|
||||
defaultMessage: 'Education',
|
||||
description: 'Label for account settings education field.',
|
||||
},
|
||||
'account.settings.field.education.levels.null': {
|
||||
id: 'account.settings.field.education.levels.null',
|
||||
'account.settings.field.education.empty': {
|
||||
id: 'account.settings.field.education.empty',
|
||||
defaultMessage: 'Add level of education',
|
||||
description: 'Placeholder for empty account settings education field.',
|
||||
},
|
||||
'account.settings.field.education.levels.empty': {
|
||||
id: 'account.settings.field.education.levels.empty',
|
||||
defaultMessage: 'Select a level of education',
|
||||
description: 'Placeholder for the education levels dropdown.',
|
||||
},
|
||||
@@ -123,8 +307,13 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Gender',
|
||||
description: 'Label for account settings gender field.',
|
||||
},
|
||||
'account.settings.field.gender.options.null': {
|
||||
id: 'account.settings.field.gender.options.null',
|
||||
'account.settings.field.gender.empty': {
|
||||
id: 'account.settings.field.gender.empty',
|
||||
defaultMessage: 'Add gender',
|
||||
description: 'Placeholder for empty account settings gender field.',
|
||||
},
|
||||
'account.settings.field.gender.options.empty': {
|
||||
id: 'account.settings.field.gender.options.empty',
|
||||
defaultMessage: 'Select a gender',
|
||||
description: 'Placeholder for the gender options dropdown.',
|
||||
},
|
||||
@@ -143,11 +332,51 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Other',
|
||||
description: 'The label for catch-all gender option.',
|
||||
},
|
||||
|
||||
'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 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 language field.',
|
||||
},
|
||||
|
||||
'account.settings.field.time.zone': {
|
||||
id: 'account.settings.field.time.zone',
|
||||
defaultMessage: 'Time zone',
|
||||
description: 'Label for time zone settings field.',
|
||||
},
|
||||
'account.settings.field.time.zone.empty': {
|
||||
id: 'account.settings.field.time.zone.empty',
|
||||
defaultMessage: 'Set time zone',
|
||||
description: 'Placeholder for empty for time zone settings field.',
|
||||
},
|
||||
'account.settings.field.time.zone.description': {
|
||||
id: 'account.settings.field.time.zone.description',
|
||||
defaultMessage: '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.',
|
||||
description: 'Description for time zone settings field.',
|
||||
},
|
||||
'account.settings.field.time.zone.default': {
|
||||
id: 'account.settings.field.time.zone.default',
|
||||
defaultMessage: 'Default (Local Time Zone)',
|
||||
description: 'The default option for a time zone.',
|
||||
},
|
||||
'account.settings.field.time.zone.all': {
|
||||
id: 'account.settings.field.time.zone.all',
|
||||
defaultMessage: 'All time zones',
|
||||
description: 'The label for the group of options for all time zones.',
|
||||
},
|
||||
'account.settings.field.time.zone.country': {
|
||||
id: 'account.settings.field.time.zone.country',
|
||||
defaultMessage: 'Country time zones',
|
||||
description: 'The group of time zone options for a country.',
|
||||
},
|
||||
|
||||
'account.settings.section.social.media': {
|
||||
@@ -157,7 +386,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': {
|
||||
@@ -165,21 +394,36 @@ const messages = defineMessages({
|
||||
defaultMessage: 'LinkedIn',
|
||||
description: 'Label for LinkedIn',
|
||||
},
|
||||
'account.settings.field.social.platform.name.linkedin.empty': {
|
||||
id: 'account.settings.field.social.platform.name.linkedin.empty',
|
||||
defaultMessage: 'Add LinkedIn profile',
|
||||
description: 'Placeholder for an empty LinkedIn field',
|
||||
},
|
||||
'account.settings.jump.nav.delete.account': {
|
||||
id: 'account.settings.jump.nav.delete.account',
|
||||
defaultMessage: 'Delete My Account',
|
||||
description: 'Header for the user account deletion area',
|
||||
},
|
||||
'account.settings.field.social.platform.name.twitter': {
|
||||
id: 'account.settings.field.social.platform.name.twitter',
|
||||
defaultMessage: 'Twitter',
|
||||
description: 'Label for Twitter',
|
||||
},
|
||||
'account.settings.field.social.platform.name.twitter.empty': {
|
||||
id: 'account.settings.field.social.platform.name.twitter.empty',
|
||||
defaultMessage: 'Add Twitter profile',
|
||||
description: 'Placeholder for an empty Twitter field',
|
||||
},
|
||||
|
||||
'account.settings.field.social.platform.name.facebook': {
|
||||
id: 'account.settings.field.social.platform.name.facebook',
|
||||
defaultMessage: 'Facebook',
|
||||
description: 'Label for Facebook',
|
||||
},
|
||||
|
||||
'account.settings.editable.field.password.reset.button': {
|
||||
id: 'account.settings.editable.field.password.reset.button',
|
||||
defaultMessage: 'Reset Password',
|
||||
description: 'The password reset button in account settings',
|
||||
'account.settings.field.social.platform.name.facebook.empty': {
|
||||
id: 'account.settings.field.social.platform.name.facebook.empty',
|
||||
defaultMessage: 'Add Facebook profile',
|
||||
description: 'Placeholder for an empty Facebook field',
|
||||
},
|
||||
'account.settings.editable.field.action.save': {
|
||||
id: 'account.settings.editable.field.action.save',
|
||||
@@ -196,6 +440,16 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Edit',
|
||||
description: 'The edit button on an editable field',
|
||||
},
|
||||
'account.settings.static.field.empty': {
|
||||
id: 'account.settings.static.field.empty',
|
||||
defaultMessage: 'No value set. Contact your {enterprise} administrator to make changes.',
|
||||
description: 'The placeholder for an empty but uneditable field',
|
||||
},
|
||||
'account.settings.static.field.empty.no.admin': {
|
||||
id: 'account.settings.static.field.empty.no.admin',
|
||||
defaultMessage: 'No value set.',
|
||||
description: 'The placeholder for an empty but uneditable field when there is no administrator',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -2,7 +2,6 @@ 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)}>
|
||||
@@ -16,7 +15,6 @@ function Alert(props) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Alert.propTypes = {
|
||||
className: PropTypes.string,
|
||||
icon: PropTypes.node,
|
||||
@@ -29,5 +27,4 @@ Alert.defaultProps = {
|
||||
children: undefined,
|
||||
};
|
||||
|
||||
|
||||
export default Alert;
|
||||
116
src/account-settings/BetaLanguageBanner.jsx
Normal file
116
src/account-settings/BetaLanguageBanner.jsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, Hyperlink } from '@edx/paragon';
|
||||
|
||||
import { betaLanguageBannerSelector } from './data/selectors';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
import { saveSettings } from './data/actions';
|
||||
import { TRANSIFEX_LANGUAGE_BASE_URL } from './data/constants';
|
||||
import Alert from './Alert';
|
||||
|
||||
class BetaLanguageBanner extends React.Component {
|
||||
getSiteLanguageEntry(languageCode) {
|
||||
return this.props.siteLanguageList.filter(l => l.code === languageCode)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a link to the Transifex URL where contributors can provide translations.
|
||||
* This code is tightly coupled to how Transifex chooses to design its URLs.
|
||||
*/
|
||||
getTransifexLink(languageCode) {
|
||||
return TRANSIFEX_LANGUAGE_BASE_URL + this.getTransifexURLPath(languageCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL path that Transifex chooses to use for its language sub-pages.
|
||||
*
|
||||
* For extended language codes, it returns the 2nd half capitalized, replacing
|
||||
* hyphen (-) with underscore (_).
|
||||
* example: pt-br -> pt_BR
|
||||
*
|
||||
* For short language codes, it returns the code as is.
|
||||
* example: fr -> fr
|
||||
*/
|
||||
getTransifexURLPath(languageCode) {
|
||||
const tokenizedCode = languageCode.split('-');
|
||||
if (tokenizedCode.length > 1) {
|
||||
return `${tokenizedCode[0]}_${tokenizedCode[1].toUpperCase()}`;
|
||||
}
|
||||
return tokenizedCode[0];
|
||||
}
|
||||
|
||||
handleRevertLanguage = () => {
|
||||
const previousSiteLanguage = this.props.siteLanguage.previousValue;
|
||||
this.props.saveSettings('siteLanguage', previousSiteLanguage);
|
||||
};
|
||||
|
||||
render() {
|
||||
const savedLanguage = this.getSiteLanguageEntry(this.context.locale);
|
||||
const isSavedLanguageReleased = savedLanguage.released === true;
|
||||
const noPreviousLanguageSet = this.props.siteLanguage.previousValue === null;
|
||||
if (isSavedLanguageReleased || noPreviousLanguageSet) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previousLanguage = this.getSiteLanguageEntry(this.props.siteLanguage.previousValue);
|
||||
return (
|
||||
<div>
|
||||
<Alert className="beta_language_alert alert alert-warning" role="alert">
|
||||
<p>
|
||||
{this.props.intl.formatMessage(messages['account.settings.banner.beta.language'], {
|
||||
beta_language: savedLanguage.name,
|
||||
})}
|
||||
</p>
|
||||
<div>
|
||||
<Button onClick={this.handleRevertLanguage} className="mr-2">
|
||||
{this.props.intl.formatMessage(
|
||||
messages['account.settings.banner.beta.language.action.switch.back'],
|
||||
{ previous_language: previousLanguage.name },
|
||||
)}
|
||||
</Button>
|
||||
<Hyperlink
|
||||
destination={this.getTransifexLink(savedLanguage.code)}
|
||||
className="btn btn-outline-secondary"
|
||||
target="_blank"
|
||||
>
|
||||
{this.props.intl.formatMessage(
|
||||
messages['account.settings.banner.beta.language.action.help.translate'],
|
||||
{ beta_language: savedLanguage.name },
|
||||
)}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BetaLanguageBanner.contextType = AppContext;
|
||||
|
||||
BetaLanguageBanner.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
siteLanguage: PropTypes.shape({
|
||||
previousValue: PropTypes.string,
|
||||
draft: PropTypes.string,
|
||||
}),
|
||||
siteLanguageList: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
code: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
released: PropTypes.bool,
|
||||
})).isRequired,
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
BetaLanguageBanner.defaultProps = {
|
||||
siteLanguage: null,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
betaLanguageBannerSelector,
|
||||
{
|
||||
saveSettings,
|
||||
},
|
||||
)(injectIntl(BetaLanguageBanner));
|
||||
@@ -1,29 +1,30 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
|
||||
import { Button, StatefulButton } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Input, StatefulButton, ValidationFormGroup,
|
||||
} from '@edx/paragon';
|
||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Input from './temp/Input';
|
||||
import ValidationFormGroup from './temp/ValidationFormGroup';
|
||||
import SwitchContent from './temp/SwitchContent';
|
||||
import messages from '../AccountSettingsPage.messages';
|
||||
import SwitchContent from './SwitchContent';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
|
||||
import {
|
||||
openForm,
|
||||
closeForm,
|
||||
updateDraft,
|
||||
saveAccount,
|
||||
} from '../actions';
|
||||
import { editableFieldSelector } from '../selectors';
|
||||
|
||||
} from './data/actions';
|
||||
import { editableFieldSelector } from './data/selectors';
|
||||
|
||||
function EditableField(props) {
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
emptyLabel,
|
||||
type,
|
||||
value,
|
||||
userSuppliedValue,
|
||||
options,
|
||||
saveState,
|
||||
error,
|
||||
@@ -36,21 +37,12 @@ function EditableField(props) {
|
||||
onChange,
|
||||
isEditing,
|
||||
isEditable,
|
||||
isGrayedOut,
|
||||
intl,
|
||||
...others
|
||||
} = props;
|
||||
const id = `field-${name}`;
|
||||
|
||||
const getValue = (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) return selectedOption.label;
|
||||
}
|
||||
return rawValue;
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onSubmit(name, new FormData(e.target).get(name));
|
||||
@@ -68,8 +60,39 @@ function EditableField(props) {
|
||||
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;
|
||||
if (!confirmationMessageDefinition || !confirmationValue) {
|
||||
return null;
|
||||
}
|
||||
return intl.formatMessage(confirmationMessageDefinition, {
|
||||
value: confirmationValue,
|
||||
});
|
||||
@@ -89,6 +112,7 @@ function EditableField(props) {
|
||||
>
|
||||
<label className="h6 d-block" htmlFor={id}>{label}</label>
|
||||
<Input
|
||||
data-hj-suppress
|
||||
name={name}
|
||||
id={id}
|
||||
type={type}
|
||||
@@ -97,11 +121,12 @@ function EditableField(props) {
|
||||
options={options}
|
||||
{...others}
|
||||
/>
|
||||
<>{others.children}</>
|
||||
</ValidationFormGroup>
|
||||
<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']),
|
||||
@@ -114,13 +139,13 @@ function EditableField(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>
|
||||
@@ -129,15 +154,15 @@ function EditableField(props) {
|
||||
),
|
||||
default: (
|
||||
<div className="form-group">
|
||||
<div className="d-flex justify-content-between align-items-start">
|
||||
<h6>{label}</h6>
|
||||
<div className="d-flex align-items-start">
|
||||
<h6 aria-level="3">{label}</h6>
|
||||
{isEditable ? (
|
||||
<Button onClick={handleEdit} className="btn-link">
|
||||
{intl.formatMessage(messages['account.settings.editable.field.action.edit'])}
|
||||
<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>{getValue(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>
|
||||
),
|
||||
@@ -146,12 +171,13 @@ function EditableField(props) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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]),
|
||||
@@ -171,6 +197,7 @@ EditableField.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isEditing: PropTypes.bool,
|
||||
isEditable: PropTypes.bool,
|
||||
isGrayedOut: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
@@ -179,18 +206,18 @@ EditableField.defaultProps = {
|
||||
options: undefined,
|
||||
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,
|
||||
onChange: updateDraft,
|
||||
onSubmit: saveAccount,
|
||||
})(injectIntl(EditableField));
|
||||
@@ -1,31 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
|
||||
import { Button, StatefulButton } from '@edx/paragon';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, StatefulButton, Input, ValidationFormGroup,
|
||||
} from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faExclamationTriangle, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import Input from './temp/Input';
|
||||
import ValidationFormGroup from './temp/ValidationFormGroup';
|
||||
import SwitchContent from './temp/SwitchContent';
|
||||
import Alert from './Alert';
|
||||
import messages from '../AccountSettingsPage.messages';
|
||||
import SwitchContent from './SwitchContent';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
|
||||
import {
|
||||
openForm,
|
||||
closeForm,
|
||||
updateDraft,
|
||||
saveAccount,
|
||||
} from '../actions';
|
||||
import { editableFieldSelector } from '../selectors';
|
||||
|
||||
} from './data/actions';
|
||||
import { editableFieldSelector } from './data/selectors';
|
||||
|
||||
function EmailField(props) {
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
emptyLabel,
|
||||
value,
|
||||
saveState,
|
||||
error,
|
||||
@@ -60,13 +57,15 @@ function EmailField(props) {
|
||||
};
|
||||
|
||||
const renderConfirmationMessage = () => {
|
||||
if (!confirmationMessageDefinition || !confirmationValue) return null;
|
||||
if (!confirmationMessageDefinition || !confirmationValue) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Alert
|
||||
className="alert-warning mt-n2"
|
||||
icon={<FontAwesomeIcon className="mr-2 h6" icon={faExclamationTriangle} />}
|
||||
>
|
||||
<h6>
|
||||
<h6 aria-level="3">
|
||||
{intl.formatMessage(messages['account.settings.email.field.confirmation.header'])}
|
||||
</h6>
|
||||
{intl.formatMessage(confirmationMessageDefinition, { value: confirmationValue })}
|
||||
@@ -87,6 +86,20 @@ function EmailField(props) {
|
||||
</span>
|
||||
);
|
||||
|
||||
const renderEmptyLabel = () => {
|
||||
if (isEditable) {
|
||||
return <Button variant="link" onClick={handleEdit} className="p-0">{emptyLabel}</Button>;
|
||||
}
|
||||
return <span className="text-muted">{emptyLabel}</span>;
|
||||
};
|
||||
|
||||
const renderValue = () => {
|
||||
if (confirmationValue) {
|
||||
return renderConfirmationValue();
|
||||
}
|
||||
return value || renderEmptyLabel();
|
||||
};
|
||||
|
||||
return (
|
||||
<SwitchContent
|
||||
expression={isEditing ? 'editing' : 'default'}
|
||||
@@ -101,6 +114,7 @@ function EmailField(props) {
|
||||
>
|
||||
<label className="h6 d-block" htmlFor={id}>{label}</label>
|
||||
<Input
|
||||
data-hj-suppress
|
||||
name={name}
|
||||
id={id}
|
||||
type="email"
|
||||
@@ -111,7 +125,7 @@ function EmailField(props) {
|
||||
<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']),
|
||||
@@ -124,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>
|
||||
@@ -139,15 +153,16 @@ function EmailField(props) {
|
||||
),
|
||||
default: (
|
||||
<div className="form-group">
|
||||
<div className="d-flex justify-content-between align-items-start">
|
||||
<h6>{label}</h6>
|
||||
<div className="d-flex align-items-start">
|
||||
<h6 aria-level="3">{label}</h6>
|
||||
{isEditable ? (
|
||||
<Button onClick={handleEdit} className="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>{confirmationValue ? renderConfirmationValue() : value}</p>
|
||||
<p data-hj-suppress>{renderValue()}</p>
|
||||
{renderConfirmationMessage() || <p className="small text-muted mt-n2">{helpText}</p>}
|
||||
</div>
|
||||
),
|
||||
@@ -156,10 +171,10 @@ function EmailField(props) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
EmailField.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
emptyLabel: PropTypes.node,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
|
||||
error: PropTypes.string,
|
||||
@@ -183,6 +198,7 @@ EmailField.defaultProps = {
|
||||
value: undefined,
|
||||
saveState: undefined,
|
||||
label: undefined,
|
||||
emptyLabel: undefined,
|
||||
error: undefined,
|
||||
confirmationMessageDefinition: undefined,
|
||||
confirmationValue: undefined,
|
||||
@@ -191,10 +207,7 @@ EmailField.defaultProps = {
|
||||
isEditable: true,
|
||||
};
|
||||
|
||||
|
||||
export default connect(editableFieldSelector, {
|
||||
onEdit: openForm,
|
||||
onCancel: closeForm,
|
||||
onChange: updateDraft,
|
||||
onSubmit: saveAccount,
|
||||
})(injectIntl(EmailField));
|
||||
74
src/account-settings/JumpNav.jsx
Normal file
74
src/account-settings/JumpNav.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { NavHashLink } from 'react-router-hash-link';
|
||||
import Scrollspy from 'react-scrollspy';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import PropTypes from 'prop-types';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
|
||||
function JumpNav({ intl, displayDemographicsLink }) {
|
||||
return (
|
||||
<div className="jump-nav">
|
||||
<Scrollspy
|
||||
items={[
|
||||
'basic-information',
|
||||
'profile-information',
|
||||
'demographics-information',
|
||||
'social-media',
|
||||
'site-preferences',
|
||||
'linked-accounts',
|
||||
'delete-account',
|
||||
]}
|
||||
className="list-unstyled"
|
||||
currentClassName="font-weight-bold"
|
||||
>
|
||||
<li>
|
||||
<NavHashLink to="#basic-information">
|
||||
{intl.formatMessage(messages['account.settings.section.account.information'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavHashLink to="#profile-information">
|
||||
{intl.formatMessage(messages['account.settings.section.profile.information'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && displayDemographicsLink
|
||||
&& (
|
||||
<li>
|
||||
<NavHashLink to="#demographics-information">
|
||||
{intl.formatMessage(messages['account.settings.section.demographics.information'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<NavHashLink to="#social-media">
|
||||
{intl.formatMessage(messages['account.settings.section.social.media'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavHashLink to="#site-preferences">
|
||||
{intl.formatMessage(messages['account.settings.section.site.preferences'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavHashLink to="#linked-accounts">
|
||||
{intl.formatMessage(messages['account.settings.section.linked.accounts'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavHashLink to="#delete-account">
|
||||
{intl.formatMessage(messages['account.settings.jump.nav.delete.account'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
</Scrollspy>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
JumpNav.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
displayDemographicsLink: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(JumpNav);
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
43
src/account-settings/OneTimeDismissibleAlert.jsx
Normal file
43
src/account-settings/OneTimeDismissibleAlert.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
export default function OneTimeDismissibleAlert(props) {
|
||||
const [dismissed, setDismissed] = useState(localStorage.getItem(props.id) !== 'true');
|
||||
|
||||
const onClose = () => {
|
||||
localStorage.setItem(props.id, 'true');
|
||||
setDismissed(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
variant={props.variant}
|
||||
dismissible
|
||||
icon={props.icon}
|
||||
onClose={onClose}
|
||||
show={dismissed}
|
||||
>
|
||||
<Alert.Heading>{props.header}</Alert.Heading>
|
||||
<p>
|
||||
{props.body}
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
OneTimeDismissibleAlert.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
variant: PropTypes.string,
|
||||
icon: PropTypes.func,
|
||||
header: PropTypes.string,
|
||||
body: PropTypes.string,
|
||||
};
|
||||
|
||||
OneTimeDismissibleAlert.defaultProps = {
|
||||
variant: 'success',
|
||||
icon: undefined,
|
||||
header: undefined,
|
||||
body: undefined,
|
||||
};
|
||||
@@ -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,7 +22,6 @@ const onChildExit = (htmlNode) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function SwitchContent({ expression, cases, className }) {
|
||||
const getContent = (caseKey) => {
|
||||
if (cases[caseKey]) {
|
||||
@@ -29,7 +29,8 @@ function SwitchContent({ expression, cases, className }) {
|
||||
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);
|
||||
}
|
||||
@@ -49,7 +50,6 @@ function SwitchContent({ expression, cases, className }) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
SwitchContent.propTypes = {
|
||||
expression: PropTypes.string,
|
||||
cases: PropTypes.objectOf(PropTypes.node).isRequired,
|
||||
@@ -61,5 +61,4 @@ SwitchContent.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
|
||||
|
||||
export default SwitchContent;
|
||||
@@ -2,13 +2,56 @@
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h6, .h6 {
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
line-height: 1.2;
|
||||
border: none;
|
||||
padding: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.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 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import { utils } from '../common';
|
||||
|
||||
const { AsyncActionType } = utils;
|
||||
|
||||
export const FETCH_ACCOUNT = new AsyncActionType('ACCOUNT_SETTINGS', 'FETCH_ACCOUNT');
|
||||
export const SAVE_ACCOUNT = new AsyncActionType('ACCOUNT_SETTINGS', 'SAVE_ACCOUNT');
|
||||
export const FETCH_THIRD_PARTY_AUTH_PROVIDERS = new AsyncActionType('ACCOUNT_SETTINGS', 'FETCH_THIRD_PARTY_AUTH_PROVIDERS');
|
||||
export const RESET_PASSWORD = new AsyncActionType('ACCOUNT_SETTINGS', 'RESET_PASSWORD');
|
||||
export const OPEN_FORM = 'OPEN_FORM';
|
||||
export const CLOSE_FORM = 'CLOSE_FORM';
|
||||
export const UPDATE_DRAFT = 'UPDATE_DRAFT';
|
||||
export const RESET_DRAFTS = 'RESET_DRAFTS';
|
||||
|
||||
// FETCH EXAMPLE ACTIONS
|
||||
|
||||
export const fetchAccount = () => ({
|
||||
type: FETCH_ACCOUNT.BASE,
|
||||
});
|
||||
|
||||
export const fetchAccountBegin = () => ({
|
||||
type: FETCH_ACCOUNT.BEGIN,
|
||||
});
|
||||
|
||||
export const fetchAccountSuccess = values => ({
|
||||
type: FETCH_ACCOUNT.SUCCESS,
|
||||
payload: { values },
|
||||
});
|
||||
|
||||
export const fetchAccountFailure = error => ({
|
||||
type: FETCH_ACCOUNT.FAILURE,
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
export const fetchAccountReset = () => ({
|
||||
type: FETCH_ACCOUNT.RESET,
|
||||
});
|
||||
|
||||
export const openForm = formId => ({
|
||||
type: OPEN_FORM,
|
||||
payload: { formId },
|
||||
});
|
||||
|
||||
export const closeForm = formId => ({
|
||||
type: CLOSE_FORM,
|
||||
payload: { formId },
|
||||
});
|
||||
|
||||
|
||||
// FORM STATE ACTIONS
|
||||
|
||||
export const updateDraft = (name, value) => ({
|
||||
type: UPDATE_DRAFT,
|
||||
payload: {
|
||||
name,
|
||||
value,
|
||||
},
|
||||
});
|
||||
|
||||
export const resetDrafts = () => ({
|
||||
type: RESET_DRAFTS,
|
||||
});
|
||||
|
||||
// SAVE PROFILE ACTIONS
|
||||
|
||||
export const saveAccount = (formId, commitValues) => ({
|
||||
type: SAVE_ACCOUNT.BASE,
|
||||
payload: { formId, commitValues },
|
||||
});
|
||||
|
||||
export const saveAccountBegin = () => ({
|
||||
type: SAVE_ACCOUNT.BEGIN,
|
||||
});
|
||||
|
||||
export const saveAccountSuccess = (values, confirmationValues) => ({
|
||||
type: SAVE_ACCOUNT.SUCCESS,
|
||||
payload: { values, confirmationValues },
|
||||
});
|
||||
|
||||
export const saveAccountReset = () => ({
|
||||
type: SAVE_ACCOUNT.RESET,
|
||||
});
|
||||
|
||||
export const saveAccountFailure = ({ fieldErrors, message }) => ({
|
||||
type: SAVE_ACCOUNT.FAILURE,
|
||||
payload: { errors: fieldErrors, message },
|
||||
});
|
||||
|
||||
|
||||
// SAVE PROFILE ACTIONS
|
||||
|
||||
export const resetPassword = () => ({
|
||||
type: RESET_PASSWORD.BASE,
|
||||
});
|
||||
|
||||
export const resetPasswordBegin = () => ({
|
||||
type: RESET_PASSWORD.BEGIN,
|
||||
});
|
||||
|
||||
export const resetPasswordSuccess = () => ({
|
||||
type: RESET_PASSWORD.SUCCESS,
|
||||
});
|
||||
|
||||
export const resetPasswordReset = () => ({
|
||||
type: RESET_PASSWORD.RESET,
|
||||
});
|
||||
|
||||
|
||||
// fetch third party auth providers
|
||||
|
||||
export const fetchThirdPartyAuthProviders = () => ({
|
||||
type: FETCH_THIRD_PARTY_AUTH_PROVIDERS.BASE,
|
||||
});
|
||||
export const fetchThirdPartyAuthProvidersBegin = () => ({
|
||||
type: FETCH_THIRD_PARTY_AUTH_PROVIDERS.BEGIN,
|
||||
});
|
||||
export const fetchThirdPartyAuthProvidersSuccess = providers => ({
|
||||
type: FETCH_THIRD_PARTY_AUTH_PROVIDERS.SUCCESS, payload: { providers },
|
||||
});
|
||||
export const fetchThirdPartyAuthProvidersFailure = error => ({
|
||||
type: FETCH_THIRD_PARTY_AUTH_PROVIDERS.FAILURE, payload: { error },
|
||||
});
|
||||
|
||||
269
src/account-settings/coaching/CoachingConsent.jsx
Normal file
269
src/account-settings/coaching/CoachingConsent.jsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig, getQueryParameters } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import get from 'lodash.get';
|
||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import PageLoading from '../PageLoading';
|
||||
import CoachingConsentForm from './CoachingConsentForm';
|
||||
import messages from './CoachingConsent.messages';
|
||||
import LogoSVG from '../../logo.svg';
|
||||
import { fetchSettings } from '../data/actions';
|
||||
import { coachingConsentPageSelector } from '../data/selectors';
|
||||
|
||||
const Logo = ({ src, alt, ...attributes }) => (
|
||||
<>
|
||||
<img src={src} alt={alt} {...attributes} />
|
||||
</>
|
||||
);
|
||||
|
||||
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>
|
||||
<div>{props.message}</div>
|
||||
<Hyperlink destination={props.continueUrl} className="d-block p-2 my-3 text-center text-white bg-primary rounded">
|
||||
{props.continue}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
);
|
||||
|
||||
const AutoRedirect = (props) => {
|
||||
window.location.href = props.redirectUrl;
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const VIEWS = {
|
||||
NOT_LOADED: 'NOT_LOADED',
|
||||
LOADED: 'LOADED',
|
||||
SUCCESS: 'SUCCESS',
|
||||
SUCCESS_PENDING: 'SUCCESS_PENDING',
|
||||
DECLINED: 'DECLINED',
|
||||
DECLINE_PENDING: 'DECLINE_PENDING',
|
||||
};
|
||||
|
||||
class CoachingConsent extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
// Used to redirect back to the courseware.
|
||||
const nextUrl = this.sanitizeForwardingUrl(getQueryParameters().next);
|
||||
this.state = {
|
||||
redirectUrl: nextUrl || `${getConfig().LMS_BASE_URL}/dashboard/`,
|
||||
formErrors: {},
|
||||
formSubmitted: false,
|
||||
declineSubmitted: false,
|
||||
submissionSuccess: false,
|
||||
};
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.declineCoaching = this.declineCoaching.bind(this);
|
||||
this.patchUsingCoachingConsentForm = this.patchUsingCoachingConsentForm.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchSettings();
|
||||
}
|
||||
|
||||
sanitizeForwardingUrl(url) {
|
||||
// Redirect to root of MFE if invalid next param is sent
|
||||
return url && url.startsWith(getConfig().LMS_BASE_URL) ? url : `${getConfig().LMS_BASE_URL}/dashboard/`;
|
||||
}
|
||||
|
||||
async patchUsingCoachingConsentForm(body) {
|
||||
const { userId } = getAuthenticatedUser();
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/coaching/v1/coaching_consent/${userId}/`;
|
||||
let formErrors = {};
|
||||
const data = await getAuthenticatedHttpClient()
|
||||
.patch(requestUrl, body)
|
||||
.catch((error) => {
|
||||
if (get(error, 'customAttributes.httpErrorResponseData')) {
|
||||
formErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
|
||||
} else {
|
||||
formErrors = { full_name: 'Something went wrong. Please try again.' };
|
||||
}
|
||||
this.setState({
|
||||
submissionSuccess: false,
|
||||
formErrors,
|
||||
formSubmitted: false,
|
||||
});
|
||||
});
|
||||
if (get(data, 'status') === 200) {
|
||||
this.setState({ submissionSuccess: true });
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
e.preventDefault();
|
||||
const body = {
|
||||
coaching_consent: false,
|
||||
consent_form_seen: true,
|
||||
};
|
||||
this.setState({
|
||||
formErrors: {},
|
||||
formSubmitted: false,
|
||||
declineSubmitted: true,
|
||||
}, () => this.patchUsingCoachingConsentForm(body));
|
||||
}
|
||||
|
||||
renderView(currentView) {
|
||||
switch (currentView) {
|
||||
case VIEWS.NOT_LOADED:
|
||||
return <PageLoading srMessage="" />;
|
||||
case VIEWS.LOADED:
|
||||
return (
|
||||
<CoachingConsentForm
|
||||
onSubmit={this.handleSubmit}
|
||||
declineCoaching={this.declineCoaching}
|
||||
formErrors={this.state.formErrors}
|
||||
formValues={this.props.formValues}
|
||||
redirectUrl={this.state.redirectUrl}
|
||||
profileDataManager={this.props.profileDataManager}
|
||||
/>
|
||||
);
|
||||
case VIEWS.SUCCESS_PENDING:
|
||||
return <PageLoading srMessage="Submitting..." />;
|
||||
case VIEWS.SUCCESS:
|
||||
return (
|
||||
<SuccessMessage
|
||||
continueUrl={this.state.redirectUrl}
|
||||
header={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.header'])}
|
||||
message={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.message'])}
|
||||
continue={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.continue'])}
|
||||
/>
|
||||
);
|
||||
case VIEWS.DECLINE_PENDING:
|
||||
return <PageLoading srMessage="Redirecting..." />;
|
||||
case VIEWS.DECLINED:
|
||||
return <AutoRedirect redirectUrl={this.state.redirectUrl} />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loaded } = this.props;
|
||||
const formHasErrors = Object.keys(this.state.formErrors).length > 0;
|
||||
let currentView = null;
|
||||
// This amount of logic was making the template very hard to read, so I broke it out into views.
|
||||
if (!loaded) {
|
||||
currentView = VIEWS.NOT_LOADED;
|
||||
} else if (this.state.formSubmitted && !formHasErrors) {
|
||||
if (this.state.submissionSuccess) {
|
||||
currentView = VIEWS.SUCCESS;
|
||||
} else {
|
||||
currentView = VIEWS.SUCCESS_PENDING;
|
||||
}
|
||||
} else if (this.state.declineSubmitted && !formHasErrors) {
|
||||
if (this.state.submissionSuccess) {
|
||||
currentView = VIEWS.DECLINED;
|
||||
} else {
|
||||
currentView = VIEWS.DECLINE_PENDING;
|
||||
}
|
||||
} else {
|
||||
currentView = VIEWS.LOADED;
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="w-100 d-flex justify-content-center align-items-center shadow coaching-header">
|
||||
<Logo
|
||||
className="logo"
|
||||
src={LogoSVG}
|
||||
alt="Logo"
|
||||
/>
|
||||
</div>
|
||||
{this.renderView(currentView)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Logo.defaultProps = {
|
||||
src: '',
|
||||
alt: '',
|
||||
};
|
||||
|
||||
Logo.propTypes = {
|
||||
src: PropTypes.string,
|
||||
alt: PropTypes.string,
|
||||
};
|
||||
|
||||
SuccessMessage.defaultProps = {
|
||||
header: '',
|
||||
message: '',
|
||||
continueUrl: '',
|
||||
continue: '',
|
||||
};
|
||||
|
||||
SuccessMessage.propTypes = {
|
||||
header: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
continueUrl: PropTypes.string,
|
||||
continue: PropTypes.string,
|
||||
};
|
||||
|
||||
AutoRedirect.defaultProps = {
|
||||
redirectUrl: '',
|
||||
};
|
||||
|
||||
AutoRedirect.propTypes = {
|
||||
redirectUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
CoachingConsent.defaultProps = {
|
||||
loaded: false,
|
||||
profileDataManager: null,
|
||||
};
|
||||
|
||||
CoachingConsent.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
loaded: PropTypes.bool,
|
||||
formValues: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
coaching: PropTypes.shape({
|
||||
coaching_consent: PropTypes.bool.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
consent_form_seen: PropTypes.bool.isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
formErrors: PropTypes.shape({
|
||||
coaching: PropTypes.object,
|
||||
}).isRequired,
|
||||
confirmationValues: PropTypes.shape({
|
||||
coaching: PropTypes.object,
|
||||
name: PropTypes.object,
|
||||
phone_number: PropTypes.object,
|
||||
}).isRequired,
|
||||
fetchSettings: PropTypes.func.isRequired,
|
||||
profileDataManager: PropTypes.string,
|
||||
};
|
||||
|
||||
export default connect(coachingConsentPageSelector, {
|
||||
fetchSettings,
|
||||
})(injectIntl(CoachingConsent));
|
||||
66
src/account-settings/coaching/CoachingConsent.messages.js
Normal file
66
src/account-settings/coaching/CoachingConsent.messages.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.coaching.consent.welcome.header': {
|
||||
id: 'account.settings.coaching.consent.welcome.header',
|
||||
defaultMessage: 'Let’s get started.',
|
||||
description: 'The welcome header for consent form.',
|
||||
},
|
||||
'account.settings.coaching.consent.welcome.subheader': {
|
||||
id: 'account.settings.coaching.consent.welcome.subheader',
|
||||
defaultMessage: "We're here for you from start to finish",
|
||||
description: 'The welcome subheader for consent form.',
|
||||
},
|
||||
'account.settings.coaching.consent.description': {
|
||||
id: 'account.settings.coaching.consent.description',
|
||||
defaultMessage: "MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If 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.*",
|
||||
description: 'Text describing what Coaching is.',
|
||||
},
|
||||
'account.settings.coaching.consent.text-messaging.disclaimer': {
|
||||
id: 'account.settings.coaching.consent.text-messaging.disclaimer',
|
||||
defaultMessage: '* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.',
|
||||
description: 'Text describing what Coaching is.',
|
||||
},
|
||||
'account.settings.coaching.consent.accept-coaching': {
|
||||
id: 'account.settings.coaching.consent.accept-coaching',
|
||||
defaultMessage: 'Sign up for coaching',
|
||||
description: 'Text to confirm coaching enablement',
|
||||
},
|
||||
'account.settings.coaching.consent.decline-coaching': {
|
||||
id: 'account.settings.coaching.consent.decline-coaching',
|
||||
defaultMessage: 'I prefer not to be contacted with free coaching services',
|
||||
description: 'Text to decline coaching enablement',
|
||||
},
|
||||
'account.settings.coaching.consent.label.name': {
|
||||
id: 'account.settings.coaching.consent.label.name',
|
||||
defaultMessage: 'Please confirm your name',
|
||||
description: 'Label for name input',
|
||||
},
|
||||
'account.settings.coaching.consent.label.phone-number': {
|
||||
id: 'account.settings.coaching.consent.label.phone-number',
|
||||
defaultMessage: 'Enter your mobile number',
|
||||
description: 'Label for mobile phone number input',
|
||||
},
|
||||
'account.settings.coaching.consent.success.header': {
|
||||
id: 'account.settings.coaching.consent.success.header',
|
||||
defaultMessage: 'Success!',
|
||||
description: 'Heading announcing that submission succeeded',
|
||||
},
|
||||
'account.settings.coaching.consent.success.message': {
|
||||
id: 'account.settings.coaching.consent.success.message',
|
||||
defaultMessage: "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
|
||||
description: 'Text announcing that you have signed up and will receive texts',
|
||||
},
|
||||
'account.settings.coaching.consent.success.continue': {
|
||||
id: 'account.settings.coaching.consent.success.continue',
|
||||
defaultMessage: 'Start my course',
|
||||
description: 'Text that the user will be sent back to the courseware',
|
||||
},
|
||||
'account.settings.coaching.managed.support': {
|
||||
id: 'account.settings.coaching.managed.support',
|
||||
defaultMessage: 'support',
|
||||
description: 'website support',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
131
src/account-settings/coaching/CoachingConsentForm.jsx
Normal file
131
src/account-settings/coaching/CoachingConsentForm.jsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Input, Button, Hyperlink } from '@edx/paragon';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Alert from '../Alert';
|
||||
import messages from './CoachingConsent.messages';
|
||||
|
||||
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"
|
||||
values={{
|
||||
managerTitle: <b>{profileDataManager}</b>,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
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'])}
|
||||
</h2>
|
||||
<p>{props.intl.formatMessage(messages['account.settings.coaching.consent.description'])}</p>
|
||||
<div>
|
||||
<form onSubmit={props.onSubmit}>
|
||||
<div className="py-3">
|
||||
{!!props.profileDataManager && (
|
||||
<ManagedProfileAlert profileDataManager={props.profileDataManager} />
|
||||
)}
|
||||
<ErrorMessage message={props.formErrors.full_name} />
|
||||
<label className="h6" htmlFor="fullName">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.name'])}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
name="full-name"
|
||||
id="fullName"
|
||||
disabled={!!props.profileDataManager}
|
||||
defaultValue={props.formValues.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="py-3">
|
||||
<ErrorMessage message={props.formErrors.phone_number} />
|
||||
<label className="h6" htmlFor="phoneNumber">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.phone-number'])}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
name="phone_number"
|
||||
id="phoneNumber"
|
||||
defaultValue={props.formValues.phone_number}
|
||||
/>
|
||||
</div>
|
||||
<div className=" py-3">
|
||||
<p className="small font-italic">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.text-messaging.disclaimer'])}
|
||||
</p>
|
||||
</div>
|
||||
<ErrorMessage message={props.formErrors.coaching} />
|
||||
<div className="d-flex flex-column align-items-center">
|
||||
<Button variant="outline-primary" className="w-100" type="submit">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.accept-coaching'])}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Hyperlink
|
||||
className="mt-3 text-dark btn-link small"
|
||||
destination={props.redirectUrl}
|
||||
onClick={props.declineCoaching}
|
||||
>
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.decline-coaching'])}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
CoachingForm.defaultProps = {
|
||||
formErrors: {
|
||||
coaching: '',
|
||||
name: '',
|
||||
phone_number: '',
|
||||
},
|
||||
};
|
||||
|
||||
CoachingForm.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
declineCoaching: PropTypes.func.isRequired,
|
||||
formValues: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
coaching: PropTypes.shape({
|
||||
coaching_consent: PropTypes.bool.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
consent_form_seen: PropTypes.bool.isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
formErrors: PropTypes.shape({
|
||||
coaching: PropTypes.string,
|
||||
full_name: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
}),
|
||||
redirectUrl: PropTypes.string.isRequired,
|
||||
profileDataManager: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
ErrorMessage.defaultProps = {
|
||||
message: '',
|
||||
};
|
||||
|
||||
ErrorMessage.propTypes = {
|
||||
message: PropTypes.string,
|
||||
};
|
||||
|
||||
ManagedProfileAlert.propTypes = {
|
||||
profileDataManager: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CoachingForm);
|
||||
98
src/account-settings/coaching/CoachingToggle.jsx
Normal file
98
src/account-settings/coaching/CoachingToggle.jsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { ValidationFormGroup, Input } from '@edx/paragon';
|
||||
import messages from './CoachingToggle.messages';
|
||||
import { editableFieldSelector } from '../data/selectors';
|
||||
import { saveSettings, updateDraft, saveMultipleSettings } from '../data/actions';
|
||||
import EditableField from '../EditableField';
|
||||
|
||||
const CoachingToggle = props => (
|
||||
<>
|
||||
<EditableField
|
||||
name="phone_number"
|
||||
type="text"
|
||||
value={props.phone_number}
|
||||
label={props.intl.formatMessage(messages['account.settings.field.phone_number'])}
|
||||
emptyLabel={props.intl.formatMessage(messages['account.settings.field.phone_number.empty'])}
|
||||
onChange={props.updateDraft}
|
||||
onSubmit={() => {
|
||||
const { coaching } = props;
|
||||
if (coaching.coaching_consent === true) {
|
||||
return props.saveMultipleSettings([
|
||||
{
|
||||
formId: 'coaching',
|
||||
commitValues: {
|
||||
...coaching,
|
||||
phone_number: props.phone_number,
|
||||
},
|
||||
},
|
||||
{
|
||||
formId: 'phone_number',
|
||||
commitValues: props.phone_number,
|
||||
},
|
||||
], 'phone_number');
|
||||
}
|
||||
return props.saveSettings('phone_number', props.phone_number);
|
||||
}}
|
||||
/>
|
||||
<ValidationFormGroup
|
||||
for="coachingConsent"
|
||||
helpText={props.intl.formatMessage(messages['account.settings.field.coaching_consent.tooltip'])}
|
||||
invalid={!!props.error}
|
||||
invalidMessage={props.intl.formatMessage(messages['account.settings.field.coaching_consent.error'])}
|
||||
className="custom-control custom-switch"
|
||||
>
|
||||
<Input
|
||||
name={props.name}
|
||||
className="custom-control-input"
|
||||
disabled={props.saveState === 'pending'}
|
||||
type="checkbox"
|
||||
id="coachingConsent"
|
||||
checked={props.coaching.coaching_consent}
|
||||
value={props.coaching.coaching_consent}
|
||||
onChange={async (e) => {
|
||||
const { name } = e.target;
|
||||
// eslint-disable-next-line camelcase
|
||||
const { user, eligible_for_coaching } = props.coaching;
|
||||
const value = {
|
||||
user,
|
||||
eligible_for_coaching,
|
||||
coaching_consent: e.target.checked,
|
||||
};
|
||||
props.saveSettings(name, value);
|
||||
}}
|
||||
/>
|
||||
<label className="custom-control-label" htmlFor="coachingConsent">{props.intl.formatMessage(messages['account.settings.field.coaching_consent'])}</label>
|
||||
</ValidationFormGroup>
|
||||
</>
|
||||
);
|
||||
|
||||
CoachingToggle.defaultProps = {
|
||||
phone_number: '',
|
||||
error: '',
|
||||
saveState: undefined,
|
||||
};
|
||||
|
||||
CoachingToggle.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
error: PropTypes.string,
|
||||
coaching: PropTypes.shape({
|
||||
coaching_consent: PropTypes.bool.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
saveMultipleSettings: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
phone_number: PropTypes.string,
|
||||
};
|
||||
|
||||
export default connect(editableFieldSelector, {
|
||||
saveSettings,
|
||||
updateDraft,
|
||||
saveMultipleSettings,
|
||||
})(injectIntl(CoachingToggle));
|
||||
31
src/account-settings/coaching/CoachingToggle.messages.js
Normal file
31
src/account-settings/coaching/CoachingToggle.messages.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.field.phone_number': {
|
||||
id: 'account.settings.field.phone_number',
|
||||
defaultMessage: 'Phone Number',
|
||||
description: 'The label for a phone numbers setting in the user profile',
|
||||
},
|
||||
'account.settings.field.phone_number.empty': {
|
||||
id: 'account.settings.field.phone_number.empty',
|
||||
defaultMessage: 'Add a phone number',
|
||||
description: 'placeholder for a profiles empty phone number field',
|
||||
},
|
||||
'account.settings.field.coaching_consent': {
|
||||
id: 'account.settings.field.coaching_consent',
|
||||
defaultMessage: 'Coaching consent',
|
||||
description: 'The label for the coaching consent setting in the user profile',
|
||||
},
|
||||
'account.settings.field.coaching_consent.tooltip': {
|
||||
id: 'account.settings.field.coaching_consent.tooltip',
|
||||
defaultMessage: 'MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text ‘STOP’ at anytime to opt-out of messages.',
|
||||
description: 'A tooltip explaining what coaching is and who it is for',
|
||||
},
|
||||
'account.settings.field.coaching_consent.error': {
|
||||
id: 'account.settings.field.coaching_consent.error',
|
||||
defaultMessage: 'A valid US phone number is required to opt into coaching',
|
||||
description: 'An error message that displays when a user attempts to consent to coaching without first providing a phone number in their profile',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
51
src/account-settings/coaching/data/service.js
Normal file
51
src/account-settings/coaching/data/service.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import get from 'lodash.get';
|
||||
|
||||
/**
|
||||
* get all settings related to the coaching plugin. Settings used
|
||||
* by Microbachelors students.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
*/
|
||||
export async function getCoachingPreferences(userId) {
|
||||
let data = {};
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`));
|
||||
} catch (error) {
|
||||
// If a user isn't active the API call will fail with a lack of credentials.
|
||||
data = {
|
||||
coaching_consent: false,
|
||||
user: userId,
|
||||
eligible_for_coaching: false,
|
||||
consent_form_seen: false,
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* patch all of the settings related to coaching.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
* @param {Object} commitValues { coaching }
|
||||
*/
|
||||
export async function patchCoachingPreferences(userId, commitValues) {
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`;
|
||||
const { coaching } = commitValues;
|
||||
coaching.user = userId;
|
||||
|
||||
await getAuthenticatedHttpClient()
|
||||
.patch(requestUrl, coaching)
|
||||
.catch((error) => {
|
||||
const apiError = Object.create(error);
|
||||
apiError.fieldErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
|
||||
if (get(apiError, 'fieldErrors.phone_number')) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
apiError.fieldErrors.coaching = apiError.fieldErrors.phone_number[0];
|
||||
delete apiError.fieldErrors.phone_number;
|
||||
}
|
||||
throw apiError;
|
||||
});
|
||||
return commitValues;
|
||||
}
|
||||
102
src/account-settings/coaching/test/CoachingConsent.test.jsx
Normal file
102
src/account-settings/coaching/test/CoachingConsent.test.jsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
|
||||
import CoachingConsent from '../CoachingConsent';
|
||||
import * as selectors from '../../data/selectors';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
const IntlCoachingConsent = injectIntl(CoachingConsent);
|
||||
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ coachingConsentPageSelector: () => ({}) })));
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('CoachingConsent', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
selectors.mockClear();
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
fetchSettings: jest.fn(),
|
||||
loaded: true,
|
||||
saveState: undefined,
|
||||
formValues: {
|
||||
name: 'edx edx',
|
||||
phone_number: '1234567890',
|
||||
coaching: {
|
||||
coaching_consent: true,
|
||||
consent_form_seen: false,
|
||||
eligible_for_coaching: true,
|
||||
user: 1,
|
||||
},
|
||||
},
|
||||
formErrors: {},
|
||||
confirmationValues: {},
|
||||
profileDataManager: '',
|
||||
intl: {},
|
||||
};
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
patch: async () => ({
|
||||
data: { status: 200 },
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3 }));
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlCoachingConsent {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('disables name field on enterprise user', () => {
|
||||
props = {
|
||||
...props,
|
||||
profileDataManager: 'test person',
|
||||
};
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlCoachingConsent {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('display completed box when successfully submitted', async () => {
|
||||
const fakeEvent = {
|
||||
preventDefault: () => {},
|
||||
target: {
|
||||
fullName: { value: 'edx edx' },
|
||||
phoneNumber: { value: '9783028731' },
|
||||
},
|
||||
};
|
||||
const wrapper = renderer.create(
|
||||
reduxWrapper(<IntlCoachingConsent {...props} />),
|
||||
{
|
||||
// bypass the forward-ref. we don't care about focus for this one test
|
||||
createNodeMock: (element) => {
|
||||
if (element.type === 'button') {
|
||||
// mock a focus function
|
||||
return {
|
||||
focus: async () => wrapper.root.findByType('form').props.onSubmit(fakeEvent),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
);
|
||||
const form = wrapper.root.findByType('form');
|
||||
await act(async () => { await form.props.onSubmit(fakeEvent); });
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,278 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CoachingConsent disables name field on enterprise user 1`] = `
|
||||
<main>
|
||||
<div
|
||||
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
className="logo"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg"
|
||||
>
|
||||
<h2
|
||||
className="h2"
|
||||
>
|
||||
Let’s get started.
|
||||
</h2>
|
||||
<p>
|
||||
MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If 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.*
|
||||
</p>
|
||||
<div>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert alert-primary"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<span>
|
||||
Your name is managed by
|
||||
<b>
|
||||
test person
|
||||
</b>
|
||||
. Contact your administrator for help.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="fullName"
|
||||
>
|
||||
Please confirm your name
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
defaultValue="edx edx"
|
||||
disabled={true}
|
||||
id="fullName"
|
||||
name="full-name"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="phoneNumber"
|
||||
>
|
||||
Enter your mobile number
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
defaultValue="1234567890"
|
||||
id="phoneNumber"
|
||||
name="phone_number"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=" py-3"
|
||||
>
|
||||
<p
|
||||
className="small font-italic"
|
||||
>
|
||||
* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="d-flex flex-column align-items-center"
|
||||
>
|
||||
<button
|
||||
className="w-100 btn btn-outline-primary"
|
||||
disabled={false}
|
||||
type="submit"
|
||||
>
|
||||
Sign up for coaching
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="mt-3"
|
||||
>
|
||||
<a
|
||||
className="mt-3 text-dark btn-link small"
|
||||
href="http://localhost:18000/dashboard/"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
I prefer not to be contacted with free coaching services
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
exports[`CoachingConsent display completed box when successfully submitted 1`] = `
|
||||
<main>
|
||||
<div
|
||||
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
className="logo"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center flex-column"
|
||||
style={
|
||||
Object {
|
||||
"height": "50vh",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="spinner-border text-primary"
|
||||
role="status"
|
||||
>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
Submitting...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
exports[`CoachingConsent should render 1`] = `
|
||||
<main>
|
||||
<div
|
||||
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
className="logo"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg"
|
||||
>
|
||||
<h2
|
||||
className="h2"
|
||||
>
|
||||
Let’s get started.
|
||||
</h2>
|
||||
<p>
|
||||
MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If 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.*
|
||||
</p>
|
||||
<div>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="fullName"
|
||||
>
|
||||
Please confirm your name
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
defaultValue="edx edx"
|
||||
disabled={false}
|
||||
id="fullName"
|
||||
name="full-name"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="phoneNumber"
|
||||
>
|
||||
Enter your mobile number
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
defaultValue="1234567890"
|
||||
id="phoneNumber"
|
||||
name="phone_number"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=" py-3"
|
||||
>
|
||||
<p
|
||||
className="small font-italic"
|
||||
>
|
||||
* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="d-flex flex-column align-items-center"
|
||||
>
|
||||
<button
|
||||
className="w-100 btn btn-outline-primary"
|
||||
disabled={false}
|
||||
type="submit"
|
||||
>
|
||||
Sign up for coaching
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="mt-3"
|
||||
>
|
||||
<a
|
||||
className="mt-3 text-dark btn-link small"
|
||||
href="http://localhost:18000/dashboard/"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
I prefer not to be contacted with free coaching services
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
@@ -1,84 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-i18n'; // eslint-disable-line
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { StatefulButton, Hyperlink } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { resetPassword } from '../actions';
|
||||
import { resetPasswordSelector } from '../selectors';
|
||||
import messages from '../AccountSettingsPage.messages';
|
||||
import Alert from './Alert';
|
||||
|
||||
function PasswordReset({ email, intl, ...props }) {
|
||||
const renderConfirmationMessage = () => (
|
||||
|
||||
<Alert
|
||||
className="alert-warning mt-n2"
|
||||
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="account.settings.editable.field.password.reset.button"
|
||||
defaultMessage="We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}."
|
||||
description="The password reset button in account settings"
|
||||
values={{
|
||||
email,
|
||||
technicalSupportLink: (
|
||||
<Hyperlink
|
||||
destination="https://support.edx.org/hc/en-us/articles/206212088-What-if-I-did-not-receive-a-password-reset-message-"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="account.settings.editable.field.password.reset.button.support.link"
|
||||
defaultMessage="technical support"
|
||||
description="link text used in message: account.settings.editable.field.password.reset.button 'Contact technical support.'"
|
||||
/>
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="form-group">
|
||||
<h6>
|
||||
<FormattedMessage
|
||||
id="account.settings.editable.field.password.reset.label"
|
||||
defaultMessage="Password"
|
||||
description="The password label in account settings"
|
||||
/>
|
||||
</h6>
|
||||
<p>
|
||||
<StatefulButton
|
||||
className="btn-link"
|
||||
state={props.resetPasswordState}
|
||||
onClick={props.resetPassword}
|
||||
disabledStates={[]}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['account.settings.editable.field.password.reset.button']),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
{props.resetPasswordState === 'complete' ? renderConfirmationMessage() : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PasswordReset.propTypes = {
|
||||
resetPassword: PropTypes.func.isRequired,
|
||||
email: PropTypes.string,
|
||||
resetPasswordState: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
PasswordReset.defaultProps = {
|
||||
email: '',
|
||||
resetPasswordState: undefined,
|
||||
};
|
||||
|
||||
|
||||
export default connect(resetPasswordSelector, {
|
||||
resetPassword,
|
||||
})(injectIntl(PasswordReset));
|
||||
@@ -1,151 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import { fetchThirdPartyAuthProviders } from '../actions';
|
||||
import { thirdPartyAuthSelector } from '../selectors';
|
||||
|
||||
class ThirdPartyAuth extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.fetchThirdPartyAuthProviders();
|
||||
}
|
||||
|
||||
renderConnectedProvider(url, name) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h6>{name}</h6>
|
||||
<Hyperlink destination={url} className="btn btn-outline-primary">
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.link.account"
|
||||
defaultMessage="Sign in with {name}"
|
||||
description="An action link to link a connected third party account.m {name} will be Google, Facebook, etc."
|
||||
values={{ name }}
|
||||
/>
|
||||
</Hyperlink>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderUnconnectedProvider(url, name) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h6>
|
||||
{name}
|
||||
<span className="small font-weight-normal text-muted ml-2">
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.account.connected"
|
||||
defaultMessage="Linked"
|
||||
description="A badge to show that a third party account is linked"
|
||||
/>
|
||||
</span>
|
||||
</h6>
|
||||
<Hyperlink destination={url}>
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.unlink.account"
|
||||
defaultMessage="Unlink {name} account"
|
||||
description="An action link to unlink a connected third party account"
|
||||
values={{ name }}
|
||||
/>
|
||||
</Hyperlink>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderProvider({
|
||||
name, disconnectUrl, connectUrl, connected, id,
|
||||
}) {
|
||||
return (
|
||||
<div className="form-group" key={id}>
|
||||
{
|
||||
connected ?
|
||||
this.renderUnconnectedProvider(disconnectUrl, name) :
|
||||
this.renderConnectedProvider(connectUrl, name)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderNoProviders() {
|
||||
return (
|
||||
<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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.loading"
|
||||
defaultMessage="Loading..."
|
||||
description="Waiting for data to load in the third party auth provider list"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderLoadingError() {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.loading.error"
|
||||
defaultMessage="There was a problem loading linked accounts."
|
||||
description="Error message for failing to load the third party auth provider list"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.section.header"
|
||||
defaultMessage="Linked Accounts"
|
||||
description="Section header for the third party auth settings"
|
||||
/>
|
||||
</h2>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.section.subheader"
|
||||
defaultMessage="You can link your identity accounts to simplify signing in to edX."
|
||||
description="Section subheader for the third party auth settings"
|
||||
/>
|
||||
</p>
|
||||
{this.props.providers.map(this.renderProvider, this)}
|
||||
{this.props.loaded && this.props.providers.length === 0 ? this.renderNoProviders() : null}
|
||||
{this.props.loading ? this.renderLoading() : null}
|
||||
{this.props.loadingError ? this.renderLoadingError() : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ThirdPartyAuth.propTypes = {
|
||||
fetchThirdPartyAuthProviders: PropTypes.func.isRequired,
|
||||
providers: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
disconnectUrl: PropTypes.string,
|
||||
connectUrl: PropTypes.string,
|
||||
connected: PropTypes.bool,
|
||||
id: PropTypes.string,
|
||||
})),
|
||||
loading: PropTypes.bool,
|
||||
loaded: PropTypes.bool,
|
||||
loadingError: PropTypes.string,
|
||||
};
|
||||
|
||||
ThirdPartyAuth.defaultProps = {
|
||||
providers: [],
|
||||
loading: false,
|
||||
loaded: false,
|
||||
loadingError: undefined,
|
||||
};
|
||||
|
||||
|
||||
export default connect(thirdPartyAuthSelector, {
|
||||
fetchThirdPartyAuthProviders,
|
||||
})(ThirdPartyAuth);
|
||||
@@ -1,106 +0,0 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
class Input extends React.Component {
|
||||
componentDidMount() {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
this.checkHasLabel();
|
||||
}
|
||||
}
|
||||
|
||||
getHTMLTagForType() {
|
||||
const { type } = this.props;
|
||||
if (type === 'select' || type === 'textarea') return type;
|
||||
return 'input';
|
||||
}
|
||||
|
||||
getClassNameForType() {
|
||||
switch (this.props.type) {
|
||||
case 'file':
|
||||
return 'form-control-file';
|
||||
case 'checkbox':
|
||||
case 'radio':
|
||||
return 'form-check-input';
|
||||
default:
|
||||
return 'form-control';
|
||||
}
|
||||
}
|
||||
|
||||
getRef(forwardedRef) {
|
||||
if (process.env.NODE_ENV !== 'development') return forwardedRef;
|
||||
if (forwardedRef) return forwardedRef;
|
||||
if (!this.innerRef) this.innerRef = React.createRef();
|
||||
return this.innerRef;
|
||||
}
|
||||
|
||||
checkHasLabel() {
|
||||
const htmlNode = this.getRef().current;
|
||||
|
||||
if (htmlNode.labels.length > 0) return;
|
||||
if (htmlNode.getAttribute('aria-label') !== null) return;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
if (console) console.warn('Input[a11y]: There is no associated label for this Input');
|
||||
}
|
||||
|
||||
renderOptions(options) {
|
||||
return options.map((option) => {
|
||||
const {
|
||||
value, label, group, ...attributes
|
||||
} = option;
|
||||
|
||||
if (group) {
|
||||
return (
|
||||
<optgroup key={`optgroup-${label}`} label={label} {...attributes}>
|
||||
{this.renderOptions(group)}
|
||||
</optgroup>
|
||||
);
|
||||
}
|
||||
return <option key={value} value={value} {...attributes}>{label}</option>;
|
||||
}, this);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
type, className, options, innerRef, ...attributes // eslint-disable-line react/prop-types
|
||||
} = this.props;
|
||||
|
||||
const htmlTag = this.getHTMLTagForType();
|
||||
const htmlProps = {
|
||||
className: classNames(this.getClassNameForType(), className),
|
||||
type: htmlTag === 'input' ? type : undefined,
|
||||
...attributes,
|
||||
ref: this.getRef(innerRef),
|
||||
};
|
||||
const htmlChildren = type === 'select' ? this.renderOptions(options) : null;
|
||||
|
||||
return React.createElement(htmlTag, htmlProps, htmlChildren);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Input.propTypes = {
|
||||
type: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
className: PropTypes.string,
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
disabled: PropTypes.bool,
|
||||
group: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
disabled: PropTypes.bool,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
Input.defaultProps = {
|
||||
className: undefined,
|
||||
options: [],
|
||||
};
|
||||
|
||||
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
export default React.forwardRef((props, ref) => <Input innerRef={ref} {...props} />);
|
||||
@@ -1,92 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Input from './Input';
|
||||
|
||||
const propTypes = {
|
||||
for: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
invalid: PropTypes.bool,
|
||||
valid: PropTypes.bool,
|
||||
validMessage: PropTypes.node,
|
||||
invalidMessage: PropTypes.node,
|
||||
helpText: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
invalid: undefined,
|
||||
valid: undefined,
|
||||
validMessage: undefined,
|
||||
invalidMessage: undefined,
|
||||
helpText: undefined,
|
||||
children: undefined,
|
||||
className: undefined,
|
||||
};
|
||||
|
||||
|
||||
function ValidationFormGroup(props) {
|
||||
const {
|
||||
className,
|
||||
invalidMessage,
|
||||
invalid,
|
||||
valid,
|
||||
validMessage,
|
||||
helpText,
|
||||
for: id,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
const renderChildren = () => React.Children.map(children, (child) => {
|
||||
// Any non-user input element should pass through unmodified
|
||||
|
||||
if (['input', 'textarea', 'select', Input].indexOf(child.type) === -1) return child;
|
||||
|
||||
// Add validation class names and describedby values to input element
|
||||
return React.cloneElement(child, {
|
||||
className: classNames(child.props.className, {
|
||||
'is-invalid': invalid,
|
||||
'is-valid': valid,
|
||||
}),
|
||||
// This is a non-standard use of the classNames package, but it's exactly the same use case.
|
||||
'aria-describedby': classNames(child.props['aria-describedby'], {
|
||||
[`${id}-help-text`]: Boolean(helpText),
|
||||
[`${id}-invalid-feedback`]: invalid && invalidMessage,
|
||||
[`${id}-valid-feedback`]: valid && validMessage,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
const renderHelpText = (text) => {
|
||||
if (!text) return null;
|
||||
return <small id={`${id}-help-text`} className="form-text text-muted">{text}</small>;
|
||||
};
|
||||
|
||||
const renderFeedback = (message, state) => {
|
||||
if (!message) return null;
|
||||
return (
|
||||
<div
|
||||
className={`${state}-feedback`}
|
||||
id={`${id}-${state}-feedback`}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames('form-group', className)}>
|
||||
{renderChildren()}
|
||||
{renderHelpText(helpText)}
|
||||
{renderFeedback(invalidMessage, 'invalid')}
|
||||
{renderFeedback(validMessage, 'valid')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
ValidationFormGroup.propTypes = propTypes;
|
||||
ValidationFormGroup.defaultProps = defaultProps;
|
||||
|
||||
|
||||
export default ValidationFormGroup;
|
||||
@@ -1,32 +0,0 @@
|
||||
|
||||
export const YEAR_OF_BIRTH_OPTIONS = (() => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const years = [];
|
||||
let startYear = currentYear - 120;
|
||||
while (startYear < currentYear) {
|
||||
startYear += 1;
|
||||
|
||||
years.push({ value: startYear, label: startYear });
|
||||
}
|
||||
return years.reverse();
|
||||
})();
|
||||
|
||||
export const EDUCATION_LEVELS = [
|
||||
null,
|
||||
'p',
|
||||
'm',
|
||||
'b',
|
||||
'a',
|
||||
'hs',
|
||||
'jhs',
|
||||
'el',
|
||||
'none',
|
||||
'o',
|
||||
];
|
||||
|
||||
export const GENDER_OPTIONS = [
|
||||
null,
|
||||
'f',
|
||||
'm',
|
||||
'o',
|
||||
];
|
||||
130
src/account-settings/data/actions.js
Normal file
130
src/account-settings/data/actions.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import { AsyncActionType } from './utils';
|
||||
|
||||
export const FETCH_SETTINGS = new AsyncActionType('ACCOUNT_SETTINGS', 'FETCH_SETTINGS');
|
||||
export const SAVE_SETTINGS = new AsyncActionType('ACCOUNT_SETTINGS', 'SAVE_SETTINGS');
|
||||
export const SAVE_MULTIPLE_SETTINGS = new AsyncActionType('ACCOUNT_SETTINGS', 'SAVE_MULTIPLE_SETTINGS');
|
||||
export const FETCH_TIME_ZONES = new AsyncActionType('ACCOUNT_SETTINGS', 'FETCH_TIME_ZONES');
|
||||
export const SAVE_PREVIOUS_SITE_LANGUAGE = 'SAVE_PREVIOUS_SITE_LANGUAGE';
|
||||
export const OPEN_FORM = 'OPEN_FORM';
|
||||
export const CLOSE_FORM = 'CLOSE_FORM';
|
||||
export const UPDATE_DRAFT = 'UPDATE_DRAFT';
|
||||
export const RESET_DRAFTS = 'RESET_DRAFTS';
|
||||
|
||||
// FETCH SETTINGS ACTIONS
|
||||
|
||||
export const fetchSettings = () => ({
|
||||
type: FETCH_SETTINGS.BASE,
|
||||
});
|
||||
|
||||
export const fetchSettingsBegin = () => ({
|
||||
type: FETCH_SETTINGS.BEGIN,
|
||||
});
|
||||
|
||||
export const fetchSettingsSuccess = ({
|
||||
values,
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
}) => ({
|
||||
type: FETCH_SETTINGS.SUCCESS,
|
||||
payload: {
|
||||
values,
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
},
|
||||
});
|
||||
|
||||
export const fetchSettingsFailure = error => ({
|
||||
type: FETCH_SETTINGS.FAILURE,
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
export const fetchSettingsReset = () => ({
|
||||
type: FETCH_SETTINGS.RESET,
|
||||
});
|
||||
|
||||
// FORM STATE ACTIONS
|
||||
|
||||
export const openForm = formId => ({
|
||||
type: OPEN_FORM,
|
||||
payload: { formId },
|
||||
});
|
||||
|
||||
export const closeForm = formId => ({
|
||||
type: CLOSE_FORM,
|
||||
payload: { formId },
|
||||
});
|
||||
|
||||
export const updateDraft = (name, value) => ({
|
||||
type: UPDATE_DRAFT,
|
||||
payload: {
|
||||
name,
|
||||
value,
|
||||
},
|
||||
});
|
||||
|
||||
export const resetDrafts = () => ({
|
||||
type: RESET_DRAFTS,
|
||||
});
|
||||
|
||||
// SAVE SETTINGS ACTIONS
|
||||
|
||||
export const saveSettings = (formId, commitValues) => ({
|
||||
type: SAVE_SETTINGS.BASE,
|
||||
payload: { formId, commitValues },
|
||||
});
|
||||
|
||||
export const saveSettingsBegin = () => ({
|
||||
type: SAVE_SETTINGS.BEGIN,
|
||||
});
|
||||
|
||||
export const saveSettingsSuccess = (values, confirmationValues) => ({
|
||||
type: SAVE_SETTINGS.SUCCESS,
|
||||
payload: { values, confirmationValues },
|
||||
});
|
||||
|
||||
export const saveSettingsReset = () => ({
|
||||
type: SAVE_SETTINGS.RESET,
|
||||
});
|
||||
|
||||
export const saveSettingsFailure = ({ fieldErrors, message }) => ({
|
||||
type: SAVE_SETTINGS.FAILURE,
|
||||
payload: { errors: fieldErrors, message },
|
||||
});
|
||||
|
||||
export const savePreviousSiteLanguage = previousSiteLanguage => ({
|
||||
type: SAVE_PREVIOUS_SITE_LANGUAGE,
|
||||
payload: { previousSiteLanguage },
|
||||
});
|
||||
|
||||
export const saveMultipleSettings = (settingsArray, form = null) => ({
|
||||
type: SAVE_MULTIPLE_SETTINGS.BASE,
|
||||
payload: { settingsArray, form },
|
||||
});
|
||||
|
||||
export const saveMultipleSettingsBegin = () => ({
|
||||
type: SAVE_MULTIPLE_SETTINGS.BEGIN,
|
||||
});
|
||||
|
||||
export const saveMultipleSettingsSuccess = settingsArray => ({
|
||||
type: SAVE_MULTIPLE_SETTINGS.SUCCESS,
|
||||
payload: { settingsArray },
|
||||
});
|
||||
|
||||
export const saveMultipleSettingsFailure = ({ fieldErrors, message }) => ({
|
||||
type: SAVE_MULTIPLE_SETTINGS.FAILURE,
|
||||
payload: { errors: fieldErrors, message },
|
||||
});
|
||||
|
||||
// FETCH TIME_ZONE ACTIONS
|
||||
|
||||
export const fetchTimeZones = country => ({
|
||||
type: FETCH_TIME_ZONES.BASE,
|
||||
payload: { country },
|
||||
});
|
||||
|
||||
export const fetchTimeZonesSuccess = timeZones => ({
|
||||
type: FETCH_TIME_ZONES.SUCCESS,
|
||||
payload: { timeZones },
|
||||
});
|
||||
117
src/account-settings/data/constants.js
Normal file
117
src/account-settings/data/constants.js
Normal file
@@ -0,0 +1,117 @@
|
||||
export const YEAR_OF_BIRTH_OPTIONS = (() => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const years = [];
|
||||
let startYear = currentYear - 120;
|
||||
while (startYear < currentYear) {
|
||||
startYear += 1;
|
||||
|
||||
years.push({ value: startYear, label: startYear });
|
||||
}
|
||||
return years.reverse();
|
||||
})();
|
||||
|
||||
export const EDUCATION_LEVELS = [
|
||||
'',
|
||||
'p',
|
||||
'm',
|
||||
'b',
|
||||
'a',
|
||||
'hs',
|
||||
'jhs',
|
||||
'el',
|
||||
'none',
|
||||
'o',
|
||||
];
|
||||
|
||||
export const GENDER_OPTIONS = [
|
||||
'',
|
||||
'f',
|
||||
'm',
|
||||
'o',
|
||||
];
|
||||
|
||||
export const COUNTRY_WITH_STATES = 'US';
|
||||
|
||||
export const TRANSIFEX_LANGUAGE_BASE_URL = 'https://www.transifex.com/open-edx/edx-platform/language/';
|
||||
|
||||
const COUNTRY_STATES_MAP = {
|
||||
CA: [
|
||||
{ value: 'AB', label: 'Alberta' },
|
||||
{ value: 'BC', label: 'British Columbia' },
|
||||
{ value: 'MB', label: 'Manitoba' },
|
||||
{ value: 'NB', label: 'New Brunswick' },
|
||||
{ value: 'NL', label: 'Newfoundland and Labrador' },
|
||||
{ value: 'NS', label: 'Nova Scotia' },
|
||||
{ value: 'NT', label: 'Northwest Territories' },
|
||||
{ value: 'NU', label: 'Nunavut' },
|
||||
{ value: 'ON', label: 'Ontario' },
|
||||
{ value: 'PE', label: 'Prince Edward Island' },
|
||||
{ value: 'QC', label: 'Québec' },
|
||||
{ value: 'SK', label: 'Saskatchewan' },
|
||||
{ value: 'YT', label: 'Yukon' },
|
||||
],
|
||||
US: [
|
||||
{ value: 'AL', label: 'Alabama' },
|
||||
{ value: 'AK', label: 'Alaska' },
|
||||
{ value: 'AZ', label: 'Arizona' },
|
||||
{ value: 'AR', label: 'Arkansas' },
|
||||
{ value: 'AA', label: 'Armed Forces Americas' },
|
||||
{ value: 'AE', label: 'Armed Forces Europe' },
|
||||
{ value: 'AP', label: 'Armed Forces Pacific' },
|
||||
{ value: 'CA', label: 'California' },
|
||||
{ value: 'CO', label: 'Colorado' },
|
||||
{ value: 'CT', label: 'Connecticut' },
|
||||
{ value: 'DE', label: 'Delaware' },
|
||||
{ value: 'DC', label: 'District Of Columbia' },
|
||||
{ value: 'FL', label: 'Florida' },
|
||||
{ value: 'GA', label: 'Georgia' },
|
||||
{ value: 'HI', label: 'Hawaii' },
|
||||
{ value: 'ID', label: 'Idaho' },
|
||||
{ value: 'IL', label: 'Illinois' },
|
||||
{ value: 'IN', label: 'Indiana' },
|
||||
{ value: 'IA', label: 'Iowa' },
|
||||
{ value: 'KS', label: 'Kansas' },
|
||||
{ value: 'KY', label: 'Kentucky' },
|
||||
{ value: 'LA', label: 'Louisiana' },
|
||||
{ value: 'ME', label: 'Maine' },
|
||||
{ value: 'MD', label: 'Maryland' },
|
||||
{ value: 'MA', label: 'Massachusetts' },
|
||||
{ value: 'MI', label: 'Michigan' },
|
||||
{ value: 'MN', label: 'Minnesota' },
|
||||
{ value: 'MS', label: 'Mississippi' },
|
||||
{ value: 'MO', label: 'Missouri' },
|
||||
{ value: 'MT', label: 'Montana' },
|
||||
{ value: 'NE', label: 'Nebraska' },
|
||||
{ value: 'NV', label: 'Nevada' },
|
||||
{ value: 'NH', label: 'New Hampshire' },
|
||||
{ value: 'NJ', label: 'New Jersey' },
|
||||
{ value: 'NM', label: 'New Mexico' },
|
||||
{ value: 'NY', label: 'New York' },
|
||||
{ value: 'NC', label: 'North Carolina' },
|
||||
{ value: 'ND', label: 'North Dakota' },
|
||||
{ value: 'OH', label: 'Ohio' },
|
||||
{ value: 'OK', label: 'Oklahoma' },
|
||||
{ value: 'OR', label: 'Oregon' },
|
||||
{ value: 'PA', label: 'Pennsylvania' },
|
||||
{ value: 'RI', label: 'Rhode Island' },
|
||||
{ value: 'SC', label: 'South Carolina' },
|
||||
{ value: 'SD', label: 'South Dakota' },
|
||||
{ value: 'TN', label: 'Tennessee' },
|
||||
{ value: 'TX', label: 'Texas' },
|
||||
{ value: 'UT', label: 'Utah' },
|
||||
{ value: 'VT', label: 'Vermont' },
|
||||
{ value: 'VA', label: 'Virginia' },
|
||||
{ value: 'WA', label: 'Washington' },
|
||||
{ value: 'WV', label: 'West Virginia' },
|
||||
{ value: 'WI', label: 'Wisconsin' },
|
||||
{ value: 'WY', label: 'Wyoming' },
|
||||
],
|
||||
};
|
||||
|
||||
export function getStatesList(country) {
|
||||
return country && COUNTRY_STATES_MAP[country.toUpperCase()];
|
||||
}
|
||||
|
||||
export const DECLINED = 'declined';
|
||||
export const SELF_DESCRIBE = 'self-describe';
|
||||
export const OTHER = 'other';
|
||||
215
src/account-settings/data/reducers.js
Normal file
215
src/account-settings/data/reducers.js
Normal file
@@ -0,0 +1,215 @@
|
||||
import {
|
||||
FETCH_SETTINGS,
|
||||
OPEN_FORM,
|
||||
CLOSE_FORM,
|
||||
SAVE_SETTINGS,
|
||||
FETCH_TIME_ZONES,
|
||||
SAVE_PREVIOUS_SITE_LANGUAGE,
|
||||
UPDATE_DRAFT,
|
||||
RESET_DRAFTS,
|
||||
SAVE_MULTIPLE_SETTINGS,
|
||||
} 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 thirdPartyAuthReducer, DISCONNECT_AUTH } from '../third-party-auth';
|
||||
|
||||
export const defaultState = {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
loadingError: null,
|
||||
data: null,
|
||||
values: {},
|
||||
errors: {},
|
||||
confirmationValues: {},
|
||||
drafts: {},
|
||||
saveState: null,
|
||||
timeZones: [],
|
||||
countryTimeZones: [],
|
||||
previousSiteLanguage: null,
|
||||
deleteAccount: deleteAccountReducer(),
|
||||
siteLanguage: siteLanguageReducer(),
|
||||
resetPassword: resetPasswordReducer(),
|
||||
thirdPartyAuth: thirdPartyAuthReducer(),
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
let dispatcherIsOpenForm;
|
||||
|
||||
switch (action.type) {
|
||||
case FETCH_SETTINGS.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
loaded: false,
|
||||
loadingError: null,
|
||||
};
|
||||
case FETCH_SETTINGS.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
values: { ...state.values, ...action.payload.values },
|
||||
// Dump the providers into thirdPartyAuth.
|
||||
thirdPartyAuth: { ...state.thirdPartyAuth, providers: action.payload.thirdPartyAuthProviders },
|
||||
profileDataManager: action.payload.profileDataManager,
|
||||
timeZones: action.payload.timeZones,
|
||||
loading: false,
|
||||
loaded: true,
|
||||
loadingError: null,
|
||||
};
|
||||
case FETCH_SETTINGS.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
loaded: false,
|
||||
loadingError: action.payload.error,
|
||||
};
|
||||
case FETCH_SETTINGS.RESET:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
loaded: false,
|
||||
loadingError: null,
|
||||
};
|
||||
|
||||
case OPEN_FORM:
|
||||
return {
|
||||
...state,
|
||||
openFormId: action.payload.formId,
|
||||
saveState: null,
|
||||
errors: {},
|
||||
drafts: {},
|
||||
};
|
||||
case CLOSE_FORM:
|
||||
dispatcherIsOpenForm = action.payload.formId === state.openFormId;
|
||||
if (dispatcherIsOpenForm) {
|
||||
return {
|
||||
...state,
|
||||
openFormId: null,
|
||||
saveState: null,
|
||||
errors: {},
|
||||
drafts: {},
|
||||
};
|
||||
}
|
||||
return state;
|
||||
case UPDATE_DRAFT:
|
||||
return {
|
||||
...state,
|
||||
drafts: { ...state.drafts, [action.payload.name]: action.payload.value },
|
||||
saveState: null,
|
||||
errors: {},
|
||||
};
|
||||
|
||||
case RESET_DRAFTS:
|
||||
return {
|
||||
...state,
|
||||
drafts: {},
|
||||
};
|
||||
|
||||
case SAVE_SETTINGS.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'pending',
|
||||
errors: {},
|
||||
};
|
||||
case SAVE_SETTINGS.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'complete',
|
||||
values: { ...state.values, ...action.payload.values },
|
||||
errors: {},
|
||||
confirmationValues: {
|
||||
|
||||
...state.confirmationValues,
|
||||
...action.payload.confirmationValues,
|
||||
},
|
||||
};
|
||||
case SAVE_SETTINGS.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'error',
|
||||
errors: { ...state.errors, ...action.payload.errors },
|
||||
};
|
||||
case SAVE_SETTINGS.RESET:
|
||||
return {
|
||||
...state,
|
||||
saveState: null,
|
||||
errors: {},
|
||||
};
|
||||
case SAVE_PREVIOUS_SITE_LANGUAGE:
|
||||
return {
|
||||
...state,
|
||||
previousSiteLanguage: action.payload.previousSiteLanguage,
|
||||
};
|
||||
case SAVE_MULTIPLE_SETTINGS.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'pending',
|
||||
};
|
||||
|
||||
case SAVE_MULTIPLE_SETTINGS.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'complete',
|
||||
};
|
||||
|
||||
case SAVE_MULTIPLE_SETTINGS.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'error',
|
||||
errors: { ...state.errors, ...action.payload.errors },
|
||||
};
|
||||
|
||||
case FETCH_TIME_ZONES.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
countryTimeZones: action.payload.timeZones,
|
||||
};
|
||||
|
||||
// TODO: Once all the above cases have been converted into sub-reducers, we can use
|
||||
// combineReducers in this file to greatly simplify it.
|
||||
|
||||
// Delete My Account
|
||||
case DELETE_ACCOUNT.CONFIRMATION:
|
||||
case DELETE_ACCOUNT.BEGIN:
|
||||
case DELETE_ACCOUNT.SUCCESS:
|
||||
case DELETE_ACCOUNT.FAILURE:
|
||||
case DELETE_ACCOUNT.RESET:
|
||||
case DELETE_ACCOUNT.CANCEL:
|
||||
return {
|
||||
...state,
|
||||
deleteAccount: deleteAccountReducer(state.deleteAccount, action),
|
||||
};
|
||||
|
||||
case FETCH_SITE_LANGUAGES.BEGIN:
|
||||
case FETCH_SITE_LANGUAGES.SUCCESS:
|
||||
case FETCH_SITE_LANGUAGES.FAILURE:
|
||||
case FETCH_SITE_LANGUAGES.RESET:
|
||||
return {
|
||||
...state,
|
||||
siteLanguage: siteLanguageReducer(state.siteLanguage, action),
|
||||
};
|
||||
|
||||
case RESET_PASSWORD.BEGIN:
|
||||
case RESET_PASSWORD.SUCCESS:
|
||||
case RESET_PASSWORD.FORBIDDEN:
|
||||
return {
|
||||
...state,
|
||||
resetPassword: resetPasswordReducer(state.resetPassword, action),
|
||||
};
|
||||
|
||||
case DISCONNECT_AUTH.BEGIN:
|
||||
case DISCONNECT_AUTH.SUCCESS:
|
||||
case DISCONNECT_AUTH.FAILURE:
|
||||
case DISCONNECT_AUTH.RESET:
|
||||
return {
|
||||
...state,
|
||||
thirdPartyAuth: thirdPartyAuthReducer(state.thirdPartyAuth, action),
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
||||
157
src/account-settings/data/sagas.js
Normal file
157
src/account-settings/data/sagas.js
Normal file
@@ -0,0 +1,157 @@
|
||||
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';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
FETCH_SETTINGS,
|
||||
fetchSettingsBegin,
|
||||
fetchSettingsSuccess,
|
||||
fetchSettingsFailure,
|
||||
closeForm,
|
||||
SAVE_SETTINGS,
|
||||
SAVE_MULTIPLE_SETTINGS,
|
||||
saveSettingsBegin,
|
||||
saveSettingsSuccess,
|
||||
saveSettingsFailure,
|
||||
savePreviousSiteLanguage,
|
||||
FETCH_TIME_ZONES,
|
||||
fetchTimeZones,
|
||||
fetchTimeZonesSuccess,
|
||||
saveMultipleSettingsBegin,
|
||||
saveMultipleSettingsSuccess,
|
||||
saveMultipleSettingsFailure,
|
||||
} from './actions';
|
||||
|
||||
// Sub-modules
|
||||
import { saga as deleteAccountSaga } from '../delete-account';
|
||||
import { saga as resetPasswordSaga } from '../reset-password';
|
||||
import {
|
||||
saga as siteLanguageSaga,
|
||||
patchPreferences,
|
||||
postSetLang,
|
||||
} from '../site-language';
|
||||
import { saga as thirdPartyAuthSaga } from '../third-party-auth';
|
||||
|
||||
// Services
|
||||
import {
|
||||
getSettings,
|
||||
patchSettings,
|
||||
getTimeZones,
|
||||
} from './service';
|
||||
|
||||
export function* handleFetchSettings() {
|
||||
try {
|
||||
yield put(fetchSettingsBegin());
|
||||
const { username, userId, roles: userRoles } = getAuthenticatedUser();
|
||||
|
||||
const {
|
||||
thirdPartyAuthProviders, profileDataManager, timeZones, ...values
|
||||
} = yield call(
|
||||
getSettings,
|
||||
username,
|
||||
userRoles,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (values.country) { yield put(fetchTimeZones(values.country)); }
|
||||
|
||||
yield put(fetchSettingsSuccess({
|
||||
values,
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
}));
|
||||
} catch (e) {
|
||||
yield put(fetchSettingsFailure(e.message));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function* handleSaveSettings(action) {
|
||||
try {
|
||||
yield put(saveSettingsBegin());
|
||||
|
||||
const { username, userId } = getAuthenticatedUser();
|
||||
const { commitValues, formId } = action.payload;
|
||||
const commitData = { [formId]: commitValues };
|
||||
let savedValues = null;
|
||||
if (formId === 'siteLanguage') {
|
||||
const previousSiteLanguage = getLocale();
|
||||
// The following two requests need to be done sequentially, with patching preferences before
|
||||
// the post to setlang. They used to be done in parallel, but this might create ambiguous
|
||||
// behavior.
|
||||
yield call(patchPreferences, username, { prefLang: commitValues });
|
||||
yield call(postSetLang, commitValues);
|
||||
|
||||
yield put(savePreviousSiteLanguage(previousSiteLanguage));
|
||||
|
||||
publish(LOCALE_CHANGED, getLocale());
|
||||
handleRtl();
|
||||
savedValues = commitData;
|
||||
} else {
|
||||
savedValues = yield call(patchSettings, username, commitData, userId);
|
||||
}
|
||||
yield put(saveSettingsSuccess(savedValues, commitData));
|
||||
if (savedValues.country) { yield put(fetchTimeZones(savedValues.country)); }
|
||||
yield delay(1000);
|
||||
yield put(closeForm(action.payload.formId));
|
||||
} catch (e) {
|
||||
if (e.fieldErrors) {
|
||||
yield put(saveSettingsFailure({ fieldErrors: e.fieldErrors }));
|
||||
} else {
|
||||
yield put(saveSettingsFailure(e.message));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handles mutiple settings saved at once, in order, and stops executing on first failure.
|
||||
export function* handleSaveMultipleSettings(action) {
|
||||
try {
|
||||
yield put(saveMultipleSettingsBegin());
|
||||
const { username, userId } = getAuthenticatedUser();
|
||||
const { settingsArray, form } = action.payload;
|
||||
for (let i = 0; i < settingsArray.length; i += 1) {
|
||||
const { formId, commitValues } = settingsArray[i];
|
||||
yield put(saveSettingsBegin());
|
||||
const commitData = { [formId]: commitValues };
|
||||
const savedSettings = yield call(patchSettings, username, commitData, userId);
|
||||
yield put(saveSettingsSuccess(savedSettings, commitData));
|
||||
}
|
||||
yield put(saveMultipleSettingsSuccess(action));
|
||||
if (form) {
|
||||
yield delay(1000);
|
||||
yield put(closeForm(form));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.fieldErrors) {
|
||||
yield put(saveMultipleSettingsFailure({ fieldErrors: e.fieldErrors }));
|
||||
} else {
|
||||
yield put(saveMultipleSettingsFailure(e.message));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* handleFetchTimeZones(action) {
|
||||
const response = yield call(getTimeZones, action.payload.country);
|
||||
yield put(fetchTimeZonesSuccess(response, action.payload.country));
|
||||
}
|
||||
|
||||
export default function* saga() {
|
||||
yield takeEvery(FETCH_SETTINGS.BASE, handleFetchSettings);
|
||||
yield takeEvery(SAVE_SETTINGS.BASE, handleSaveSettings);
|
||||
yield takeEvery(SAVE_MULTIPLE_SETTINGS.BASE, handleSaveMultipleSettings);
|
||||
yield takeEvery(FETCH_TIME_ZONES.BASE, handleFetchTimeZones);
|
||||
yield all([
|
||||
deleteAccountSaga(),
|
||||
siteLanguageSaga(),
|
||||
resetPasswordSaga(),
|
||||
thirdPartyAuthSaga(),
|
||||
]);
|
||||
}
|
||||
271
src/account-settings/data/selectors.js
Normal file
271
src/account-settings/data/selectors.js
Normal file
@@ -0,0 +1,271 @@
|
||||
import { createSelector, createStructuredSelector } from 'reselect';
|
||||
import { siteLanguageListSelector, siteLanguageOptionsSelector } from '../site-language';
|
||||
|
||||
export const storeName = 'accountSettings';
|
||||
|
||||
export const accountSettingsSelector = state => ({ ...state[storeName] });
|
||||
|
||||
const editableFieldNameSelector = (state, props) => props.name;
|
||||
|
||||
const sortedVerifiedNameHistorySelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => {
|
||||
function sortDates(a, b) {
|
||||
const aTimeSinceEpoch = new Date(a).getTime();
|
||||
const bTimeSinceEpoch = new Date(b).getTime();
|
||||
return bTimeSinceEpoch - aTimeSinceEpoch;
|
||||
}
|
||||
|
||||
const history = accountSettings.values.verifiedNameHistory && accountSettings.values.verifiedNameHistory.results;
|
||||
|
||||
if (Array.isArray(history)) {
|
||||
return history.sort(sortDates);
|
||||
}
|
||||
|
||||
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':
|
||||
verifiedName = approvedVerifiedName;
|
||||
break;
|
||||
case 'submitted':
|
||||
verifiedName = mostRecentVerifiedName;
|
||||
break;
|
||||
default:
|
||||
verifiedName = null;
|
||||
}
|
||||
return verifiedName;
|
||||
},
|
||||
);
|
||||
|
||||
const valuesSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
mostRecentVerifiedNameSelector,
|
||||
mostRecentApprovedVerifiedNameValueSelector,
|
||||
(accountSettings, mostRecentVerifiedNameValue, mostRecentApprovedVerifiedNameValue) => (
|
||||
{
|
||||
...accountSettings.values,
|
||||
verifiedName: mostRecentApprovedVerifiedNameValue,
|
||||
mostRecentVerifiedName: mostRecentVerifiedNameValue,
|
||||
}),
|
||||
);
|
||||
|
||||
const draftsSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.drafts,
|
||||
);
|
||||
|
||||
const previousSiteLanguageSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.previousSiteLanguage,
|
||||
);
|
||||
|
||||
const editableFieldErrorSelector = createSelector(
|
||||
editableFieldNameSelector,
|
||||
accountSettingsSelector,
|
||||
(name, accountSettings) => accountSettings.errors[name],
|
||||
);
|
||||
|
||||
const editableFieldConfirmationValuesSelector = createSelector(
|
||||
editableFieldNameSelector,
|
||||
accountSettingsSelector,
|
||||
(name, accountSettings) => accountSettings.confirmationValues[name],
|
||||
);
|
||||
|
||||
const isEditingSelector = createSelector(
|
||||
editableFieldNameSelector,
|
||||
accountSettingsSelector,
|
||||
(name, accountSettings) => accountSettings.openFormId === name,
|
||||
);
|
||||
|
||||
const confirmationValuesSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.confirmationValues,
|
||||
);
|
||||
|
||||
const errorSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.errors,
|
||||
);
|
||||
|
||||
const saveStateSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.saveState,
|
||||
);
|
||||
|
||||
export const editableFieldSelector = createStructuredSelector({
|
||||
error: editableFieldErrorSelector,
|
||||
confirmationValue: editableFieldConfirmationValuesSelector,
|
||||
saveState: saveStateSelector,
|
||||
isEditing: isEditingSelector,
|
||||
});
|
||||
|
||||
export const profileDataManagerSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.profileDataManager,
|
||||
);
|
||||
|
||||
export const staticFieldsSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
mostRecentVerifiedNameSelector,
|
||||
(accountSettings, verifiedName) => {
|
||||
const staticFields = [];
|
||||
if (accountSettings.profileDataManager) {
|
||||
staticFields.push('name', 'email', 'country');
|
||||
}
|
||||
if (verifiedName && ['pending', 'submitted'].includes(verifiedName.status)) {
|
||||
staticFields.push('verifiedName');
|
||||
}
|
||||
|
||||
return staticFields;
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* If there's no draft present at all (undefined), use the original committed value.
|
||||
*/
|
||||
function chooseFormValue(draft, committed) {
|
||||
return draft !== undefined ? draft : committed;
|
||||
}
|
||||
|
||||
const formValuesSelector = createSelector(
|
||||
valuesSelector,
|
||||
draftsSelector,
|
||||
(values, drafts) => {
|
||||
const formValues = {};
|
||||
Object.entries(values).forEach(([name, value]) => {
|
||||
formValues[name] = chooseFormValue(drafts[name], value) || '';
|
||||
});
|
||||
return formValues;
|
||||
},
|
||||
);
|
||||
|
||||
const transformTimeZonesToOptions = timeZoneArr => timeZoneArr
|
||||
.map(({ time_zone, description }) => ({ // eslint-disable-line camelcase
|
||||
value: time_zone, label: description,
|
||||
}));
|
||||
|
||||
const timeZonesSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => transformTimeZonesToOptions(accountSettings.timeZones),
|
||||
);
|
||||
|
||||
const countryTimeZonesSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => transformTimeZonesToOptions(accountSettings.countryTimeZones),
|
||||
);
|
||||
|
||||
const activeAccountSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.values.is_active,
|
||||
);
|
||||
|
||||
export const siteLanguageSelector = createSelector(
|
||||
previousSiteLanguageSelector,
|
||||
draftsSelector,
|
||||
(previousValue, drafts) => ({
|
||||
previousValue,
|
||||
draft: drafts.siteLanguage,
|
||||
}),
|
||||
);
|
||||
|
||||
export const betaLanguageBannerSelector = createStructuredSelector({
|
||||
siteLanguageList: siteLanguageListSelector,
|
||||
siteLanguage: siteLanguageSelector,
|
||||
});
|
||||
|
||||
export const accountSettingsPageSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
siteLanguageOptionsSelector,
|
||||
siteLanguageSelector,
|
||||
formValuesSelector,
|
||||
profileDataManagerSelector,
|
||||
staticFieldsSelector,
|
||||
timeZonesSelector,
|
||||
countryTimeZonesSelector,
|
||||
activeAccountSelector,
|
||||
(
|
||||
accountSettings,
|
||||
siteLanguageOptions,
|
||||
siteLanguage,
|
||||
formValues,
|
||||
profileDataManager,
|
||||
staticFields,
|
||||
timeZoneOptions,
|
||||
countryTimeZoneOptions,
|
||||
activeAccount,
|
||||
) => ({
|
||||
siteLanguageOptions,
|
||||
siteLanguage,
|
||||
loading: accountSettings.loading,
|
||||
loaded: accountSettings.loaded,
|
||||
loadingError: accountSettings.loadingError,
|
||||
timeZoneOptions,
|
||||
countryTimeZoneOptions,
|
||||
isActive: activeAccount,
|
||||
formValues,
|
||||
profileDataManager,
|
||||
staticFields,
|
||||
tpaProviders: accountSettings.thirdPartyAuth.providers,
|
||||
}),
|
||||
);
|
||||
|
||||
export const coachingConsentPageSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
formValuesSelector,
|
||||
activeAccountSelector,
|
||||
profileDataManagerSelector,
|
||||
saveStateSelector,
|
||||
confirmationValuesSelector,
|
||||
errorSelector,
|
||||
(
|
||||
accountSettings,
|
||||
formValues,
|
||||
activeAccount,
|
||||
profileDataManager,
|
||||
saveState,
|
||||
confirmationValues,
|
||||
errors,
|
||||
) => ({
|
||||
loading: accountSettings.loading,
|
||||
loaded: accountSettings.loaded,
|
||||
loadingError: accountSettings.loadingError,
|
||||
isActive: activeAccount,
|
||||
profileDataManager,
|
||||
formValues,
|
||||
saveState,
|
||||
confirmationValues,
|
||||
formErrors: errors,
|
||||
}),
|
||||
);
|
||||
|
||||
export const demographicsSectionSelector = createSelector(
|
||||
formValuesSelector,
|
||||
draftsSelector,
|
||||
errorSelector,
|
||||
(
|
||||
formValues,
|
||||
drafts,
|
||||
errors,
|
||||
) => ({
|
||||
formValues,
|
||||
drafts,
|
||||
formErrors: errors,
|
||||
}),
|
||||
);
|
||||
284
src/account-settings/data/service.js
Normal file
284
src/account-settings/data/service.js
Normal file
@@ -0,0 +1,284 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import pick from 'lodash.pick';
|
||||
import pickBy from 'lodash.pickby';
|
||||
import omit from 'lodash.omit';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { handleRequestError, unpackFieldErrors } from './utils';
|
||||
import { getThirdPartyAuthProviders } from '../third-party-auth';
|
||||
import { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service';
|
||||
import { getDemographics, getDemographicsOptions, patchDemographics } from '../demographics/data/service';
|
||||
import { DEMOGRAPHICS_FIELDS } from '../demographics/data/utils';
|
||||
|
||||
const SOCIAL_PLATFORMS = [
|
||||
{ id: 'twitter', key: 'social_link_twitter' },
|
||||
{ id: 'facebook', key: 'social_link_facebook' },
|
||||
{ id: 'linkedin', key: 'social_link_linkedin' },
|
||||
];
|
||||
|
||||
function unpackAccountResponseData(data) {
|
||||
const unpackedData = data;
|
||||
|
||||
// This is handled by preferences
|
||||
delete unpackedData.time_zone;
|
||||
|
||||
SOCIAL_PLATFORMS.forEach(({ id, key }) => {
|
||||
const platformData = data.social_links.find(({ platform }) => platform === id);
|
||||
unpackedData[key] = typeof platformData === 'object' ? platformData.social_link : '';
|
||||
});
|
||||
|
||||
if (Array.isArray(data.language_proficiencies)) {
|
||||
if (data.language_proficiencies.length) {
|
||||
unpackedData.language_proficiencies = data.language_proficiencies[0].code;
|
||||
} else {
|
||||
unpackedData.language_proficiencies = '';
|
||||
}
|
||||
}
|
||||
|
||||
return unpackedData;
|
||||
}
|
||||
|
||||
function packAccountCommitData(commitData) {
|
||||
const packedData = commitData;
|
||||
|
||||
SOCIAL_PLATFORMS.forEach(({ id, key }) => {
|
||||
// Skip missing values. Empty strings are valid values and should be preserved.
|
||||
if (commitData[key] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
packedData.social_links = [{ platform: id, social_link: commitData[key] }];
|
||||
delete packedData[key];
|
||||
});
|
||||
|
||||
if (commitData.language_proficiencies !== undefined) {
|
||||
if (commitData.language_proficiencies) {
|
||||
packedData.language_proficiencies = [{ code: commitData.language_proficiencies }];
|
||||
} else {
|
||||
// An empty string should be sent as an array.
|
||||
packedData.language_proficiencies = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (commitData.year_of_birth !== undefined) {
|
||||
if (commitData.year_of_birth) {
|
||||
packedData.year_of_birth = commitData.year_of_birth;
|
||||
} else {
|
||||
// An empty string should be sent as null.
|
||||
packedData.year_of_birth = null;
|
||||
}
|
||||
}
|
||||
return packedData;
|
||||
}
|
||||
|
||||
export async function getAccount(username) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`);
|
||||
return unpackAccountResponseData(data);
|
||||
}
|
||||
|
||||
export async function patchAccount(username, commitValues) {
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/merge-patch+json' },
|
||||
};
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.patch(
|
||||
`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`,
|
||||
packAccountCommitData(commitValues),
|
||||
requestConfig,
|
||||
)
|
||||
.catch((error) => {
|
||||
const unpackFunction = (fieldErrors) => {
|
||||
const unpackedFieldErrors = fieldErrors;
|
||||
if (fieldErrors.social_links) {
|
||||
SOCIAL_PLATFORMS.forEach(({ key }) => {
|
||||
unpackedFieldErrors[key] = fieldErrors.social_links;
|
||||
});
|
||||
}
|
||||
return unpackFieldErrors(unpackedFieldErrors);
|
||||
};
|
||||
handleRequestError(error, unpackFunction);
|
||||
});
|
||||
|
||||
return unpackAccountResponseData(data);
|
||||
}
|
||||
|
||||
export async function getPreferences(username) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function patchPreferences(username, commitValues) {
|
||||
const requestConfig = { headers: { 'Content-Type': 'application/merge-patch+json' } };
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`;
|
||||
|
||||
// Ignore the success response, the API does not currently return any data.
|
||||
await getAuthenticatedHttpClient()
|
||||
.patch(requestUrl, commitValues, requestConfig).catch(handleRequestError);
|
||||
|
||||
return commitValues;
|
||||
}
|
||||
|
||||
export async function getTimeZones(forCountry) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getConfig().LMS_BASE_URL}/user_api/v1/preferences/time_zones/`, {
|
||||
params: { country_code: forCountry },
|
||||
})
|
||||
.catch(handleRequestError);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user's profile data is managed by a third-party identity provider.
|
||||
*/
|
||||
export async function getProfileDataManager(username, userRoles) {
|
||||
const userRoleNames = userRoles.map(role => role.split(':')[0]);
|
||||
|
||||
if (userRoleNames.includes('enterprise_learner')) {
|
||||
const url = `${getConfig().LMS_BASE_URL}/enterprise/api/v1/enterprise-learner/?username=${username}`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url).catch(handleRequestError);
|
||||
|
||||
if (data.results.length > 0) {
|
||||
const enterprise = data.results[0] && data.results[0].enterprise_customer;
|
||||
// To ensure that enterprise returned is current enterprise & it manages profile settings
|
||||
if (enterprise && enterprise.sync_learner_profile_data) {
|
||||
return enterprise.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function to determine if the Demographics questions should be displayed to the user. For the
|
||||
* MVP release of Demographics we are limiting the Demographics question visibility only to
|
||||
* MicroBachelors learners.
|
||||
*/
|
||||
export async function shouldDisplayDemographicsQuestions() {
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/demographics/v1/demographics/status/`;
|
||||
let data = {};
|
||||
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient().get(requestUrl));
|
||||
if (data.display) {
|
||||
return data.display;
|
||||
}
|
||||
} catch (error) {
|
||||
// if there was an error then we just hide the section
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function getVerifiedName() {
|
||||
let data;
|
||||
const client = getAuthenticatedHttpClient();
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 [
|
||||
account,
|
||||
preferences,
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
coaching,
|
||||
shouldDisplayDemographicsQuestionsResponse,
|
||||
demographics,
|
||||
demographicsOptions,
|
||||
verifiedNameHistory,
|
||||
] = await Promise.all([
|
||||
getAccount(username),
|
||||
getPreferences(username),
|
||||
getThirdPartyAuthProviders(),
|
||||
getProfileDataManager(username, userRoles),
|
||||
getTimeZones(),
|
||||
getConfig().COACHING_ENABLED && getCoachingPreferences(userId),
|
||||
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && shouldDisplayDemographicsQuestions(),
|
||||
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographics(userId),
|
||||
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographicsOptions(),
|
||||
getVerifiedNameHistory(),
|
||||
]);
|
||||
|
||||
return {
|
||||
...account,
|
||||
...preferences,
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
coaching,
|
||||
shouldDisplayDemographicsSection: shouldDisplayDemographicsQuestionsResponse,
|
||||
...demographics,
|
||||
demographicsOptions,
|
||||
verifiedNameHistory,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A single function to PATCH everything considered a setting.
|
||||
* Currently encapsulates Account, Preferences, coaching and ThirdPartyAuth
|
||||
*/
|
||||
export async function patchSettings(username, commitValues, userId) {
|
||||
// Note: time_zone exists in the return value from user/v1/accounts
|
||||
// but it is always null and won't update. It also exists in
|
||||
// user/v1/preferences where it does update. This is the one we use.
|
||||
const preferenceKeys = ['time_zone'];
|
||||
const coachingKeys = ['coaching'];
|
||||
const demographicsKeys = DEMOGRAPHICS_FIELDS;
|
||||
const isDemographicsKey = (value, key) => key.includes('demographics');
|
||||
const accountCommitValues = omit(commitValues, preferenceKeys, coachingKeys, demographicsKeys);
|
||||
const preferenceCommitValues = pick(commitValues, preferenceKeys);
|
||||
const coachingCommitValues = pick(commitValues, coachingKeys);
|
||||
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
|
||||
const patchRequests = [];
|
||||
|
||||
if (!isEmpty(accountCommitValues)) {
|
||||
patchRequests.push(patchAccount(username, accountCommitValues));
|
||||
}
|
||||
if (!isEmpty(preferenceCommitValues)) {
|
||||
patchRequests.push(patchPreferences(username, preferenceCommitValues));
|
||||
}
|
||||
if (!isEmpty(coachingCommitValues)) {
|
||||
patchRequests.push(patchCoachingPreferences(userId, coachingCommitValues));
|
||||
}
|
||||
if (!isEmpty(demographicsCommitValues)) {
|
||||
patchRequests.push(patchDemographics(userId, demographicsCommitValues));
|
||||
}
|
||||
|
||||
const results = await Promise.all(patchRequests);
|
||||
// Assigns in order of requests. Preference keys
|
||||
// will override account keys. Notably time_zone.
|
||||
const combinedResults = Object.assign({}, ...results);
|
||||
return combinedResults;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getModuleState should throw an exception on a bad path 1`] = `"Unexpected state key uhoh given to getModuleState. Is your state path set up correctly?"`;
|
||||
6
src/account-settings/data/utils/index.js
Normal file
6
src/account-settings/data/utils/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
AsyncActionType,
|
||||
getModuleState,
|
||||
} from './reduxUtils';
|
||||
export { default as handleFailure } from './sagaUtils';
|
||||
export { unpackFieldErrors, handleRequestError } from './serviceUtils';
|
||||
66
src/account-settings/data/utils/reduxUtils.js
Normal file
66
src/account-settings/data/utils/reduxUtils.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Helper class to save time when writing out action types for asynchronous methods. Also helps
|
||||
* ensure that actions are namespaced.
|
||||
*/
|
||||
export class AsyncActionType {
|
||||
constructor(topic, name) {
|
||||
this.topic = topic;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
get BASE() {
|
||||
return `${this.topic}__${this.name}`;
|
||||
}
|
||||
|
||||
get BEGIN() {
|
||||
return `${this.topic}__${this.name}__BEGIN`;
|
||||
}
|
||||
|
||||
get SUCCESS() {
|
||||
return `${this.topic}__${this.name}__SUCCESS`;
|
||||
}
|
||||
|
||||
get FAILURE() {
|
||||
return `${this.topic}__${this.name}__FAILURE`;
|
||||
}
|
||||
|
||||
get RESET() {
|
||||
return `${this.topic}__${this.name}__RESET`;
|
||||
}
|
||||
|
||||
get FORBIDDEN() {
|
||||
return `${this.topic}__${this.name}__FORBIDDEN`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a state tree and an array representing a set of keys to traverse in that tree, returns
|
||||
* the portion of the tree at that key path.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* const result = getModuleState(
|
||||
* {
|
||||
* first: { red: { awesome: 'sauce' }, blue: { weak: 'sauce' } },
|
||||
* second: { other: 'data', }
|
||||
* },
|
||||
* ['first', 'red']
|
||||
* );
|
||||
*
|
||||
* result will be:
|
||||
*
|
||||
* {
|
||||
* awesome: 'sauce'
|
||||
* }
|
||||
*/
|
||||
export function getModuleState(state, originalPath) {
|
||||
const path = [...originalPath]; // don't modify your argument
|
||||
if (path.length < 1) {
|
||||
return state;
|
||||
}
|
||||
const key = path.shift();
|
||||
if (state[key] === undefined) {
|
||||
throw new Error(`Unexpected state key ${key} given to getModuleState. Is your state path set up correctly?`);
|
||||
}
|
||||
return getModuleState(state[key], path);
|
||||
}
|
||||
52
src/account-settings/data/utils/reduxUtils.test.js
Normal file
52
src/account-settings/data/utils/reduxUtils.test.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
AsyncActionType,
|
||||
getModuleState,
|
||||
} from './reduxUtils';
|
||||
|
||||
describe('AsyncActionType', () => {
|
||||
it('should return well formatted action strings', () => {
|
||||
const actionType = new AsyncActionType('HOUSE_CATS', 'START_THE_RACE');
|
||||
|
||||
expect(actionType.BASE).toBe('HOUSE_CATS__START_THE_RACE');
|
||||
expect(actionType.BEGIN).toBe('HOUSE_CATS__START_THE_RACE__BEGIN');
|
||||
expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS');
|
||||
expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE');
|
||||
expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET');
|
||||
expect(actionType.FORBIDDEN).toBe('HOUSE_CATS__START_THE_RACE__FORBIDDEN');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModuleState', () => {
|
||||
const state = {
|
||||
first: { red: { awesome: 'sauce' }, blue: { weak: 'sauce' } },
|
||||
second: { other: 'data' },
|
||||
};
|
||||
|
||||
it('should return everything if given an empty path', () => {
|
||||
expect(getModuleState(state, [])).toEqual(state);
|
||||
});
|
||||
|
||||
it('should resolve paths correctly', () => {
|
||||
expect(getModuleState(
|
||||
state,
|
||||
['first'],
|
||||
)).toEqual({ red: { awesome: 'sauce' }, blue: { weak: 'sauce' } });
|
||||
|
||||
expect(getModuleState(
|
||||
state,
|
||||
['first', 'red'],
|
||||
)).toEqual({ awesome: 'sauce' });
|
||||
|
||||
expect(getModuleState(state, ['second'])).toEqual({ other: 'data' });
|
||||
});
|
||||
|
||||
it('should throw an exception on a bad path', () => {
|
||||
expect(() => {
|
||||
getModuleState(state, ['uhoh']);
|
||||
}).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
it('should return non-objects correctly', () => {
|
||||
expect(getModuleState(state, ['first', 'red', 'awesome'])).toEqual('sauce');
|
||||
});
|
||||
});
|
||||
16
src/account-settings/data/utils/sagaUtils.js
Normal file
16
src/account-settings/data/utils/sagaUtils.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { put } from 'redux-saga/effects';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
|
||||
export default function* handleFailure(error, failureAction = null, failureRedirectPath = null) {
|
||||
if (error.fieldErrors && failureAction !== null) {
|
||||
yield put(failureAction({ fieldErrors: error.fieldErrors }));
|
||||
}
|
||||
logError(error);
|
||||
if (failureAction !== null) {
|
||||
yield put(failureAction(error.message));
|
||||
}
|
||||
if (failureRedirectPath !== null) {
|
||||
history.push(failureRedirectPath);
|
||||
}
|
||||
}
|
||||
48
src/account-settings/data/utils/serviceUtils.js
Normal file
48
src/account-settings/data/utils/serviceUtils.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Turns field errors of the form:
|
||||
*
|
||||
* {
|
||||
* "name":{
|
||||
* "developer_message": "Nerdy message here",
|
||||
* "user_message": "This value is invalid."
|
||||
* },
|
||||
* "other_field": {
|
||||
* "developer_message": "Other Nerdy message here",
|
||||
* "user_message": "This other value is invalid."
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Into:
|
||||
*
|
||||
* {
|
||||
* "name": "This value is invalid.",
|
||||
* "other_field": "This other value is invalid"
|
||||
* }
|
||||
*/
|
||||
export function unpackFieldErrors(fieldErrors) {
|
||||
return Object.entries(fieldErrors).reduce((acc, [k, v]) => {
|
||||
acc[k] = v.user_message;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes and re-throws request errors. If the response contains a field_errors field, will
|
||||
* massage the data into a form expected by the client.
|
||||
*
|
||||
* Field errors will be packaged as an api error with a fieldErrors field usable by the client.
|
||||
* Takes an optional unpack function which is used to process the field errors,
|
||||
* otherwise uses the default unpackFieldErrors function.
|
||||
*
|
||||
* @param error The original error object.
|
||||
* @param unpackFunction (Optional) A function to use to unpack the field errors as a replacement
|
||||
* for the default.
|
||||
*/
|
||||
export function handleRequestError(error, unpackFunction = unpackFieldErrors) {
|
||||
if (error.response && error.response.data.field_errors) {
|
||||
const apiError = Object.create(error);
|
||||
apiError.fieldErrors = unpackFunction(error.response.data.field_errors);
|
||||
throw apiError;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
// Messages
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
|
||||
// Components
|
||||
import Alert from '../Alert';
|
||||
|
||||
const BeforeProceedingBanner = (props) => {
|
||||
const { instructionMessageId, intl, supportArticleUrl } = props;
|
||||
|
||||
return (
|
||||
<Alert
|
||||
className="alert-warning mt-n2"
|
||||
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage=intl.formatMessage(
|
||||
messages[instructionMessageId], {linkStart: <Hyperlink destination={supportArticleURL}>, linkEnd=</Hyperlink>}
|
||||
)
|
||||
values={{
|
||||
siteName: getConfig().SITE_NAME,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
BeforeProceedingBanner.propTypes = {
|
||||
instructionMessageId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
supportArticleUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(BeforeProceedingBanner);
|
||||
149
src/account-settings/delete-account/ConfirmationModal.jsx
Normal file
149
src/account-settings/delete-account/ConfirmationModal.jsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
Button, Input, Modal, ValidationFormGroup,
|
||||
} from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { faExclamationCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
import Alert from '../Alert';
|
||||
import PrintingInstructions from './PrintingInstructions';
|
||||
|
||||
export class ConfirmationModal extends Component {
|
||||
/**
|
||||
* @returns String The message id for a short description of the error, suitable for a header or
|
||||
* as the error message under an input field.
|
||||
*/
|
||||
getShortErrorMessageId(reason) {
|
||||
switch (reason) {
|
||||
case 'empty-password':
|
||||
return 'account.settings.delete.account.error.no.password';
|
||||
case 'invalid-password':
|
||||
return 'account.settings.delete.account.error.invalid.password';
|
||||
default:
|
||||
return 'account.settings.delete.account.error.unable.to.delete';
|
||||
}
|
||||
}
|
||||
|
||||
renderError(reason) {
|
||||
const { errorType, intl } = this.props;
|
||||
|
||||
if (errorType === null) {
|
||||
return null;
|
||||
}
|
||||
const headerMessageId = this.getShortErrorMessageId(errorType);
|
||||
const detailsMessageId = reason === 'empty-password'
|
||||
? null
|
||||
: 'account.settings.delete.account.error.unable.to.delete.details';
|
||||
|
||||
return (
|
||||
<Alert
|
||||
className="alert-danger mt-n2"
|
||||
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationCircle} />}
|
||||
>
|
||||
<h6>{intl.formatMessage(messages[headerMessageId])}</h6>
|
||||
{detailsMessageId ? (
|
||||
<p className="text-danger">{intl.formatMessage(messages[detailsMessageId])}</p>
|
||||
) : null}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
status,
|
||||
errorType,
|
||||
intl,
|
||||
onCancel,
|
||||
onChange,
|
||||
onSubmit,
|
||||
password,
|
||||
} = this.props;
|
||||
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}
|
||||
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'],
|
||||
{ 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>
|
||||
)}
|
||||
buttons={[
|
||||
<Button variant="danger" onClick={onSubmit}>
|
||||
{intl.formatMessage(messages['account.settings.delete.account.modal.confirm.delete'])}
|
||||
</Button>,
|
||||
]}
|
||||
closeText={intl.formatMessage(messages['account.settings.delete.account.modal.confirm.cancel'])}
|
||||
renderHeaderCloseButton={false}
|
||||
onClose={onCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmationModal.propTypes = {
|
||||
status: PropTypes.oneOf(['confirming', 'pending', 'deleted', 'failed']),
|
||||
errorType: PropTypes.oneOf(['empty-password', 'server']),
|
||||
intl: intlShape.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
password: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
ConfirmationModal.defaultProps = {
|
||||
status: null,
|
||||
errorType: null,
|
||||
};
|
||||
|
||||
export default injectIntl(ConfirmationModal);
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
|
||||
import { ConfirmationModal } from './ConfirmationModal'; // eslint-disable-line import/first
|
||||
|
||||
const IntlConfirmationModal = injectIntl(ConfirmationModal);
|
||||
|
||||
describe('ConfirmationModal', () => {
|
||||
let props = {};
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
onCancel: jest.fn(),
|
||||
onChange: jest.fn(),
|
||||
onSubmit: jest.fn(),
|
||||
status: null,
|
||||
errorType: null,
|
||||
password: 'fluffy bunnies',
|
||||
};
|
||||
});
|
||||
|
||||
it('should match default closed confirmation modal snapshot', () => {
|
||||
const tree = renderer
|
||||
.create((
|
||||
<IntlProvider locale="en">
|
||||
<IntlConfirmationModal
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match open confirmation modal snapshot', () => {
|
||||
const tree = renderer
|
||||
.create((
|
||||
<IntlProvider locale="en">
|
||||
<IntlConfirmationModal
|
||||
{...props}
|
||||
status="pending" // This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
|
||||
/>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match empty password confirmation modal snapshot', () => {
|
||||
const tree = renderer
|
||||
.create((
|
||||
<IntlProvider locale="en">
|
||||
<IntlConfirmationModal
|
||||
{...props}
|
||||
errorType="empty-password"
|
||||
status="pending" // This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
|
||||
/>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
170
src/account-settings/delete-account/DeleteAccount.jsx
Normal file
170
src/account-settings/delete-account/DeleteAccount.jsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Hyperlink } from '@edx/paragon';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
deleteAccount,
|
||||
deleteAccountConfirmation,
|
||||
deleteAccountFailure,
|
||||
deleteAccountReset,
|
||||
deleteAccountCancel,
|
||||
} from './data/actions';
|
||||
|
||||
// Messages
|
||||
import messages from './messages';
|
||||
|
||||
// Components
|
||||
import ConnectedConfirmationModal from './ConfirmationModal';
|
||||
import PrintingInstructions from './PrintingInstructions';
|
||||
import ConnectedSuccessModal from './SuccessModal';
|
||||
import BeforeProceedingBanner from './BeforeProceedingBanner';
|
||||
|
||||
export class DeleteAccount extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
password: '',
|
||||
};
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
if (this.state.password === '') {
|
||||
this.props.deleteAccountFailure('empty-password');
|
||||
} else {
|
||||
this.props.deleteAccount(this.state.password);
|
||||
}
|
||||
};
|
||||
|
||||
handleCancel = () => {
|
||||
this.setState({ password: '' });
|
||||
this.props.deleteAccountCancel();
|
||||
};
|
||||
|
||||
handlePasswordChange = (e) => {
|
||||
this.setState({ password: e.target.value.trim() });
|
||||
this.props.deleteAccountReset();
|
||||
};
|
||||
|
||||
handleFinalClose = () => {
|
||||
global.location = getConfig().LOGOUT_URL;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
hasLinkedTPA, isVerifiedAccount, status, errorType, intl,
|
||||
} = 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';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="section-heading">
|
||||
{intl.formatMessage(messages['account.settings.delete.account.header'])}
|
||||
</h2>
|
||||
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages['account.settings.delete.account.text.1'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages[deleteAccountText2MessageKey],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<PrintingInstructions />
|
||||
</p>
|
||||
<p className="text-danger h6">
|
||||
{intl.formatMessage(messages['account.settings.delete.account.text.warning'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</p>
|
||||
<p>
|
||||
<Hyperlink destination="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings">
|
||||
{intl.formatMessage(messages['account.settings.delete.account.text.change.instead'])}
|
||||
</Hyperlink>
|
||||
</p>
|
||||
<p>
|
||||
<Button
|
||||
variant="outline-danger"
|
||||
onClick={canDelete ? this.props.deleteAccountConfirmation : null}
|
||||
disabled={!canDelete}
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.delete.account.button'])}
|
||||
</Button>
|
||||
</p>
|
||||
|
||||
{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-"
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasLinkedTPA ? (
|
||||
<BeforeProceedingBanner
|
||||
instructionMessageId="account.settings.delete.account.please.unlink"
|
||||
supportArticleUrl="https://support.edx.org/hc/en-us/articles/207206067"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<ConnectedConfirmationModal
|
||||
status={status}
|
||||
errorType={errorType}
|
||||
onSubmit={this.handleSubmit}
|
||||
onCancel={this.handleCancel}
|
||||
onChange={this.handlePasswordChange}
|
||||
password={this.state.password}
|
||||
/>
|
||||
|
||||
<ConnectedSuccessModal status={status} onClose={this.handleFinalClose} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeleteAccount.propTypes = {
|
||||
deleteAccount: PropTypes.func.isRequired,
|
||||
deleteAccountConfirmation: PropTypes.func.isRequired,
|
||||
deleteAccountFailure: PropTypes.func.isRequired,
|
||||
deleteAccountReset: PropTypes.func.isRequired,
|
||||
deleteAccountCancel: PropTypes.func.isRequired,
|
||||
status: PropTypes.oneOf(['confirming', 'pending', 'deleted', 'failed']),
|
||||
errorType: PropTypes.oneOf(['empty-password', 'server']),
|
||||
hasLinkedTPA: PropTypes.bool,
|
||||
isVerifiedAccount: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
DeleteAccount.defaultProps = {
|
||||
hasLinkedTPA: false,
|
||||
isVerifiedAccount: true,
|
||||
status: null,
|
||||
errorType: null,
|
||||
};
|
||||
|
||||
// Assume we're part of the accountSettings state.
|
||||
const mapStateToProps = state => state.accountSettings.deleteAccount;
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
deleteAccount,
|
||||
deleteAccountConfirmation,
|
||||
deleteAccountFailure,
|
||||
deleteAccountReset,
|
||||
deleteAccountCancel,
|
||||
},
|
||||
)(injectIntl(DeleteAccount));
|
||||
70
src/account-settings/delete-account/DeleteAccount.test.jsx
Normal file
70
src/account-settings/delete-account/DeleteAccount.test.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
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');
|
||||
|
||||
import { DeleteAccount } from './DeleteAccount'; // eslint-disable-line import/first
|
||||
|
||||
const IntlDeleteAccount = injectIntl(DeleteAccount);
|
||||
|
||||
describe('DeleteAccount', () => {
|
||||
let props = {};
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
deleteAccount: jest.fn(),
|
||||
deleteAccountConfirmation: jest.fn(),
|
||||
deleteAccountFailure: jest.fn(),
|
||||
deleteAccountReset: jest.fn(),
|
||||
deleteAccountCancel: jest.fn(),
|
||||
status: null,
|
||||
errorType: null,
|
||||
hasLinkedTPA: false,
|
||||
isVerifiedAccount: true,
|
||||
};
|
||||
});
|
||||
|
||||
it('should match default section snapshot', () => {
|
||||
const tree = renderer
|
||||
.create((
|
||||
<IntlProvider locale="en">
|
||||
<IntlDeleteAccount
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match unverified account section snapshot', () => {
|
||||
const tree = renderer
|
||||
.create((
|
||||
<IntlProvider locale="en">
|
||||
<IntlDeleteAccount
|
||||
{...props}
|
||||
isVerifiedAccount={false}
|
||||
/>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match unverified account section snapshot', () => {
|
||||
const tree = renderer
|
||||
.create((
|
||||
<IntlProvider locale="en">
|
||||
<IntlDeleteAccount
|
||||
{...props}
|
||||
hasLinkedTPA
|
||||
/>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
47
src/account-settings/delete-account/PrintingInstructions.jsx
Normal file
47
src/account-settings/delete-account/PrintingInstructions.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
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
|
||||
// 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. 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 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
PrintingInstructions.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(PrintingInstructions);
|
||||
38
src/account-settings/delete-account/SuccessModal.jsx
Normal file
38
src/account-settings/delete-account/SuccessModal.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Modal } 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>
|
||||
<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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
SuccessModal.propTypes = {
|
||||
status: PropTypes.oneOf(['confirming', 'pending', 'deleted', 'failed']),
|
||||
intl: intlShape.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
SuccessModal.defaultProps = {
|
||||
status: null,
|
||||
};
|
||||
|
||||
export default injectIntl(SuccessModal);
|
||||
58
src/account-settings/delete-account/SuccessModal.test.jsx
Normal file
58
src/account-settings/delete-account/SuccessModal.test.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
|
||||
import { SuccessModal } from './SuccessModal'; // eslint-disable-line import/first
|
||||
|
||||
const IntlSuccessModal = injectIntl(SuccessModal);
|
||||
|
||||
describe('SuccessModal', () => {
|
||||
let props = {};
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
onClose: jest.fn(),
|
||||
status: null,
|
||||
};
|
||||
});
|
||||
|
||||
it('should match default closed success modal snapshot', () => {
|
||||
let tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} /></IntlProvider>))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
||||
tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} status="confirming" /></IntlProvider>))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
||||
tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} status="pending" /></IntlProvider>))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
||||
tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} status="failed" /></IntlProvider>))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match open success modal snapshot', () => {
|
||||
const tree = renderer
|
||||
.create((
|
||||
<IntlProvider locale="en">
|
||||
<IntlSuccessModal
|
||||
{...props}
|
||||
status="deleted" // This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
|
||||
/>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,566 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConfirmationModal should match default closed confirmation modal snapshot 1`] = `
|
||||
Array [
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>,
|
||||
<div
|
||||
className="modal fade"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id1"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled="disabled"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id1"
|
||||
>
|
||||
Are you sure?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6>
|
||||
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
|
||||
</h6>
|
||||
<p>
|
||||
If you proceed, you will be unable to use this account to take courses on localhost.
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="d-block"
|
||||
htmlFor="passwordFieldId"
|
||||
>
|
||||
If you still wish to continue and delete your account, please enter your account password:
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
className="form-control"
|
||||
id="passwordFieldId"
|
||||
name="password"
|
||||
onChange={[MockFunction]}
|
||||
type="password"
|
||||
value="fluffy bunnies"
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="passwordFieldId-invalid-feedback"
|
||||
>
|
||||
Unable to delete account
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
disabled={false}
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
Yes, Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ConfirmationModal should match empty password confirmation modal snapshot 1`] = `
|
||||
Array [
|
||||
<div
|
||||
className="modal-backdrop show"
|
||||
role="presentation"
|
||||
/>,
|
||||
<div
|
||||
className="modal show d-block"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id3"
|
||||
aria-modal={true}
|
||||
className="modal-dialog"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id3"
|
||||
>
|
||||
Are you sure?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-danger mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-circle fa-w-16 mr-2"
|
||||
data-icon="exclamation-circle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6>
|
||||
A password is required
|
||||
</h6>
|
||||
<p
|
||||
className="text-danger"
|
||||
>
|
||||
Sorry, there was an error trying to process your request. Please try again later.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6>
|
||||
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
|
||||
</h6>
|
||||
<p>
|
||||
If you proceed, you will be unable to use this account to take courses on localhost.
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="d-block"
|
||||
htmlFor="passwordFieldId"
|
||||
>
|
||||
If you still wish to continue and delete your account, please enter your account password:
|
||||
</label>
|
||||
<input
|
||||
aria-describedby="passwordFieldId-invalid-feedback"
|
||||
className="form-control is-invalid"
|
||||
id="passwordFieldId"
|
||||
name="password"
|
||||
onChange={[MockFunction]}
|
||||
type="password"
|
||||
value="fluffy bunnies"
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="passwordFieldId-invalid-feedback"
|
||||
>
|
||||
A password is required
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
disabled={false}
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
Yes, Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
|
||||
Array [
|
||||
<div
|
||||
className="modal-backdrop show"
|
||||
role="presentation"
|
||||
/>,
|
||||
<div
|
||||
className="modal show d-block"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id2"
|
||||
aria-modal={true}
|
||||
className="modal-dialog"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id2"
|
||||
>
|
||||
Are you sure?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6>
|
||||
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
|
||||
</h6>
|
||||
<p>
|
||||
If you proceed, you will be unable to use this account to take courses on localhost.
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="d-block"
|
||||
htmlFor="passwordFieldId"
|
||||
>
|
||||
If you still wish to continue and delete your account, please enter your account password:
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
className="form-control"
|
||||
id="passwordFieldId"
|
||||
name="password"
|
||||
onChange={[MockFunction]}
|
||||
type="password"
|
||||
value="fluffy bunnies"
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="passwordFieldId-invalid-feedback"
|
||||
>
|
||||
Unable to delete account
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
disabled={false}
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
Yes, Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,222 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DeleteAccount should match default section snapshot 1`] = `
|
||||
<div>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Delete My Account
|
||||
</h2>
|
||||
<p>
|
||||
We're sorry to see you go!
|
||||
</p>
|
||||
<p>
|
||||
Please note: Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
|
||||
</p>
|
||||
<p>
|
||||
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. You can make a copy of these for your records before proceeding with deletion.
|
||||
</span>
|
||||
</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 localhost.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Want to change your email, name, or password instead?
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
disabled={false}
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
Delete My Account
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
<div>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Delete My Account
|
||||
</h2>
|
||||
<p>
|
||||
We're sorry to see you go!
|
||||
</p>
|
||||
<p>
|
||||
Please note: Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
|
||||
</p>
|
||||
<p>
|
||||
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. You can make a copy of these for your records before proceeding with deletion.
|
||||
</span>
|
||||
</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 localhost.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Want to change your email, name, or password instead?
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
disabled={true}
|
||||
onClick={null}
|
||||
type="button"
|
||||
>
|
||||
Delete My Account
|
||||
</button>
|
||||
</p>
|
||||
<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>
|
||||
<span>
|
||||
Before proceeding, please
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DeleteAccount should match unverified account section snapshot 2`] = `
|
||||
<div>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Delete My Account
|
||||
</h2>
|
||||
<p>
|
||||
We're sorry to see you go!
|
||||
</p>
|
||||
<p>
|
||||
Please note: Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
|
||||
</p>
|
||||
<p>
|
||||
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. You can make a copy of these for your records before proceeding with deletion.
|
||||
</span>
|
||||
</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 localhost.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
Want to change your email, name, or password instead?
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
disabled={true}
|
||||
onClick={null}
|
||||
type="button"
|
||||
>
|
||||
Delete My Account
|
||||
</button>
|
||||
</p>
|
||||
<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>
|
||||
<span>
|
||||
Before proceeding, please
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/articles/207206067"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
unlink all social media accounts
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,563 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SuccessModal should match default closed success modal snapshot 1`] = `
|
||||
Array [
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>,
|
||||
<div
|
||||
className="modal fade"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id1"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled="disabled"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id1"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`SuccessModal should match default closed success modal snapshot 2`] = `
|
||||
Array [
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>,
|
||||
<div
|
||||
className="modal fade"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id2"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled="disabled"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id2"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`SuccessModal should match default closed success modal snapshot 3`] = `
|
||||
Array [
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>,
|
||||
<div
|
||||
className="modal fade"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id3"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled="disabled"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id3"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`SuccessModal should match default closed success modal snapshot 4`] = `
|
||||
Array [
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>,
|
||||
<div
|
||||
className="modal fade"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id4"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled="disabled"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id4"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`SuccessModal should match open success modal snapshot 1`] = `
|
||||
Array [
|
||||
<div
|
||||
className="modal-backdrop show"
|
||||
role="presentation"
|
||||
/>,
|
||||
<div
|
||||
className="modal show d-block"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id5"
|
||||
aria-modal={true}
|
||||
className="modal-dialog"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id5"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
37
src/account-settings/delete-account/data/actions.js
Normal file
37
src/account-settings/delete-account/data/actions.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { AsyncActionType } from '../../data/utils';
|
||||
|
||||
export const DELETE_ACCOUNT = new AsyncActionType('ACCOUNT_SETTINGS', 'DELETE_ACCOUNT');
|
||||
DELETE_ACCOUNT.CONFIRMATION = 'ACCOUNT_SETTINGS__DELETE_ACCOUNT__CONFIRMATION';
|
||||
DELETE_ACCOUNT.CANCEL = 'ACCOUNT_SETTINGS__DELETE_ACCOUNT__CANCEL';
|
||||
|
||||
export const deleteAccount = password => ({
|
||||
type: DELETE_ACCOUNT.BASE,
|
||||
payload: { password },
|
||||
});
|
||||
|
||||
export const deleteAccountConfirmation = () => ({
|
||||
type: DELETE_ACCOUNT.CONFIRMATION,
|
||||
});
|
||||
|
||||
export const deleteAccountBegin = () => ({
|
||||
type: DELETE_ACCOUNT.BEGIN,
|
||||
});
|
||||
|
||||
export const deleteAccountSuccess = () => ({
|
||||
type: DELETE_ACCOUNT.SUCCESS,
|
||||
});
|
||||
|
||||
export const deleteAccountFailure = reason => ({
|
||||
type: DELETE_ACCOUNT.FAILURE,
|
||||
payload: { reason },
|
||||
});
|
||||
|
||||
// to clear errors from the confirmation modal
|
||||
export const deleteAccountReset = () => ({
|
||||
type: DELETE_ACCOUNT.RESET,
|
||||
});
|
||||
|
||||
// to close the modal
|
||||
export const deleteAccountCancel = () => ({
|
||||
type: DELETE_ACCOUNT.CANCEL,
|
||||
});
|
||||
60
src/account-settings/delete-account/data/reducers.js
Normal file
60
src/account-settings/delete-account/data/reducers.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { DELETE_ACCOUNT } from './actions';
|
||||
|
||||
export const defaultState = {
|
||||
status: null,
|
||||
errorType: null,
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action = null) => {
|
||||
if (action !== null) {
|
||||
switch (action.type) {
|
||||
case DELETE_ACCOUNT.CONFIRMATION:
|
||||
return {
|
||||
...state,
|
||||
status: 'confirming',
|
||||
};
|
||||
|
||||
case DELETE_ACCOUNT.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
status: 'pending',
|
||||
};
|
||||
|
||||
case DELETE_ACCOUNT.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
status: 'deleted',
|
||||
};
|
||||
|
||||
case DELETE_ACCOUNT.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
status: 'failed',
|
||||
errorType: action.payload.reason || 'server',
|
||||
};
|
||||
|
||||
case DELETE_ACCOUNT.RESET: {
|
||||
const oldStatus = state.status;
|
||||
|
||||
return {
|
||||
...state,
|
||||
// clear the error state if applicable, otherwise don't change state
|
||||
status: oldStatus === 'failed' ? 'confirming' : oldStatus,
|
||||
errorType: null,
|
||||
};
|
||||
}
|
||||
|
||||
case DELETE_ACCOUNT.CANCEL:
|
||||
return {
|
||||
...state,
|
||||
status: null,
|
||||
errorType: null,
|
||||
};
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default reducer;
|
||||
107
src/account-settings/delete-account/data/reducers.test.js
Normal file
107
src/account-settings/delete-account/data/reducers.test.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import reducer from './reducers';
|
||||
import {
|
||||
deleteAccountConfirmation,
|
||||
deleteAccountBegin,
|
||||
deleteAccountSuccess,
|
||||
deleteAccountFailure,
|
||||
deleteAccountReset,
|
||||
deleteAccountCancel,
|
||||
} from './actions';
|
||||
|
||||
describe('delete-account reducer', () => {
|
||||
let state = null;
|
||||
|
||||
beforeEach(() => {
|
||||
state = reducer();
|
||||
});
|
||||
|
||||
it('should process DELETE_ACCOUNT.CONFIRMATION', () => {
|
||||
const result = reducer(state, deleteAccountConfirmation());
|
||||
expect(result).toEqual({
|
||||
errorType: null,
|
||||
status: 'confirming',
|
||||
});
|
||||
});
|
||||
|
||||
it('should process DELETE_ACCOUNT.BEGIN', () => {
|
||||
const result = reducer(state, deleteAccountBegin());
|
||||
expect(result).toEqual({
|
||||
errorType: null,
|
||||
status: 'pending',
|
||||
});
|
||||
});
|
||||
|
||||
it('should process DELETE_ACCOUNT.SUCCESS', () => {
|
||||
const result = reducer(state, deleteAccountSuccess());
|
||||
expect(result).toEqual({
|
||||
errorType: null,
|
||||
status: 'deleted',
|
||||
});
|
||||
});
|
||||
|
||||
it('should process DELETE_ACCOUNT.FAILURE no reason', () => {
|
||||
const result = reducer(state, deleteAccountFailure());
|
||||
expect(result).toEqual({
|
||||
errorType: 'server',
|
||||
status: 'failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('should process DELETE_ACCOUNT.FAILURE with reason', () => {
|
||||
const result = reducer(state, deleteAccountFailure('carnivorous buns'));
|
||||
expect(result).toEqual({
|
||||
errorType: 'carnivorous buns',
|
||||
status: 'failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('should process DELETE_ACCOUNT.RESET no status', () => {
|
||||
const result = reducer(state, deleteAccountReset());
|
||||
expect(result).toEqual({
|
||||
errorType: null,
|
||||
status: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should process DELETE_ACCOUNT.RESET with failed old status', () => {
|
||||
const result = reducer(
|
||||
{
|
||||
errorType: 'carnivorous buns',
|
||||
status: 'failed',
|
||||
},
|
||||
deleteAccountReset(),
|
||||
);
|
||||
expect(result).toEqual({
|
||||
errorType: null,
|
||||
status: 'confirming',
|
||||
});
|
||||
});
|
||||
|
||||
it('should process DELETE_ACCOUNT.RESET with pending old status', () => {
|
||||
const result = reducer(
|
||||
{
|
||||
errorType: 'carnivorous buns',
|
||||
status: 'pending',
|
||||
},
|
||||
deleteAccountReset(),
|
||||
);
|
||||
expect(result).toEqual({
|
||||
errorType: null,
|
||||
status: 'pending',
|
||||
});
|
||||
});
|
||||
|
||||
it('should process DELETE_ACCOUNT.CANCEL', () => {
|
||||
const result = reducer(
|
||||
{
|
||||
errorType: 'carnivorous buns',
|
||||
status: 'failed',
|
||||
},
|
||||
deleteAccountCancel(),
|
||||
);
|
||||
expect(result).toEqual({
|
||||
errorType: null,
|
||||
status: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
30
src/account-settings/delete-account/data/sagas.js
Normal file
30
src/account-settings/delete-account/data/sagas.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { put, call, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
import {
|
||||
DELETE_ACCOUNT,
|
||||
deleteAccountBegin,
|
||||
deleteAccountSuccess,
|
||||
deleteAccountFailure,
|
||||
} from './actions';
|
||||
|
||||
import { postDeleteAccount } from './service';
|
||||
|
||||
export function* handleDeleteAccount(action) {
|
||||
try {
|
||||
yield put(deleteAccountBegin());
|
||||
const response = yield call(postDeleteAccount, action.payload.password);
|
||||
yield put(deleteAccountSuccess(response));
|
||||
} catch (e) {
|
||||
if (e.response.status === 403) {
|
||||
yield put(deleteAccountFailure('invalid-password'));
|
||||
} else if (typeof e.response.data === 'string') {
|
||||
yield put(deleteAccountFailure());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function* saga() {
|
||||
yield takeEvery(DELETE_ACCOUNT.BASE, handleDeleteAccount);
|
||||
}
|
||||
23
src/account-settings/delete-account/data/service.js
Normal file
23
src/account-settings/delete-account/data/service.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import formurlencoded from 'form-urlencoded';
|
||||
import { handleRequestError } from '../../data/utils';
|
||||
|
||||
/**
|
||||
* Request deletion of the user's account.
|
||||
*/
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function postDeleteAccount(password) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(
|
||||
`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/deactivate_logout/`,
|
||||
formurlencoded({ password }),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
},
|
||||
)
|
||||
.catch(handleRequestError);
|
||||
return data;
|
||||
}
|
||||
4
src/account-settings/delete-account/index.js
Normal file
4
src/account-settings/delete-account/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default } from './DeleteAccount';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
export { DELETE_ACCOUNT } from './data/actions';
|
||||
131
src/account-settings/delete-account/messages.js
Normal file
131
src/account-settings/delete-account/messages.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.delete.account.header': {
|
||||
id: 'account.settings.delete.account.header',
|
||||
defaultMessage: 'Delete My Account',
|
||||
description: 'Header for the user account deletion area',
|
||||
},
|
||||
'account.settings.delete.account.subheader': {
|
||||
id: 'account.settings.delete.account.subheader',
|
||||
defaultMessage: 'We\'re sorry to see you go!',
|
||||
description: 'A message in the user account deletion area',
|
||||
},
|
||||
'account.settings.delete.account.text.1': {
|
||||
id: 'account.settings.delete.account.text.1',
|
||||
defaultMessage: 'Please note: Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.',
|
||||
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. This message will NOT be used on Open edX installations. Please do not translate \'MIT Open Learning\', \'Wharton Executive Education\', or \'Harvard Medical School\'',
|
||||
},
|
||||
'account.settings.delete.account.text.3.link': {
|
||||
id: 'account.settings.delete.account.text.3.link',
|
||||
defaultMessage: 'Follow these instructions for printing or downloading a certificate',
|
||||
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 {siteName}.',
|
||||
description: 'A message in the user account deletion area',
|
||||
},
|
||||
'account.settings.delete.account.text.change.instead': {
|
||||
id: 'account.settings.delete.account.text.change.instead',
|
||||
defaultMessage: 'Want to change your email, name, or password instead?',
|
||||
description: 'A message in the user account deletion area',
|
||||
},
|
||||
'account.settings.delete.account.button': {
|
||||
id: 'account.settings.delete.account.button',
|
||||
defaultMessage: 'Delete My Account',
|
||||
description: 'Button label to permanently delete your platform account',
|
||||
},
|
||||
'account.settings.delete.account.please.activate': {
|
||||
id: 'account.settings.delete.account.please.activate',
|
||||
defaultMessage: 'Before proceeding, please {linkStart}activate your account{linkEnd}',
|
||||
description: 'This message links to a support article',
|
||||
},
|
||||
'account.settings.delete.account.please.unlink': {
|
||||
id: 'account.settings.delete.account.please.unlink',
|
||||
defaultMessage: 'Before proceeding, please {linkStart}unlink all social media accounts{linkEnd}',
|
||||
description: 'This message links to a support article',
|
||||
},
|
||||
'account.settings.delete.account.modal.header': {
|
||||
id: 'account.settings.delete.account.modal.header',
|
||||
defaultMessage: 'Are you sure?',
|
||||
description: 'Title of the dialog asking user to confirm that they want to delete their entire account',
|
||||
},
|
||||
'account.settings.delete.account.modal.text.1': {
|
||||
id: 'account.settings.delete.account.modal.text.1',
|
||||
defaultMessage: 'You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.',
|
||||
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. This message will NOT be used on Open edX installations. Please do not translate \'MIT Open Learning\', \'Wharton Executive Education\', or \'Harvard Medical School\'',
|
||||
},
|
||||
'account.settings.delete.account.modal.enter.password': {
|
||||
id: 'account.settings.delete.account.modal.enter.password',
|
||||
defaultMessage: 'If you still wish to continue and delete your account, please enter your account password:',
|
||||
description: 'Asking for the user\'s account password',
|
||||
},
|
||||
'account.settings.delete.account.modal.confirm.delete': {
|
||||
id: 'account.settings.delete.account.modal.confirm.delete',
|
||||
defaultMessage: 'Yes, Delete',
|
||||
description: 'Button label for user to confirm it is okay to delete their account',
|
||||
},
|
||||
'account.settings.delete.account.modal.confirm.cancel': {
|
||||
id: 'account.settings.delete.account.modal.confirm.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
description: 'The cancel button on the delete my account modal confirmation',
|
||||
},
|
||||
'account.settings.delete.account.error.unable.to.delete': {
|
||||
id: 'account.settings.delete.account.error.unable.to.delete',
|
||||
defaultMessage: 'Unable to delete account',
|
||||
description: 'Error message when account deletion failed',
|
||||
},
|
||||
'account.settings.delete.account.error.no.password': {
|
||||
id: 'account.settings.delete.account.error.no.password',
|
||||
defaultMessage: 'A password is required',
|
||||
description: 'Error message when user has not entered their password',
|
||||
},
|
||||
'account.settings.delete.account.error.invalid.password': {
|
||||
id: 'account.settings.delete.account.error.invalid.password',
|
||||
defaultMessage: 'Password is incorrect',
|
||||
description: 'Error message when user has entered incorrect password',
|
||||
},
|
||||
'account.settings.delete.account.error.unable.to.delete.details': {
|
||||
id: 'account.settings.delete.account.error.unable.to.delete.details',
|
||||
defaultMessage: 'Sorry, there was an error trying to process your request. Please try again later.',
|
||||
description: 'Error message when account deletion failed',
|
||||
},
|
||||
'account.settings.delete.account.modal.after.header': {
|
||||
id: 'account.settings.delete.account.modal.after.header',
|
||||
defaultMessage: 'We\'re sorry to see you go! Your account will be deleted shortly.',
|
||||
description: 'Title displayed after user account is deleted',
|
||||
},
|
||||
'account.settings.delete.account.modal.after.text': {
|
||||
id: 'account.settings.delete.account.modal.after.text',
|
||||
defaultMessage: '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.',
|
||||
description: 'Text displayed after user account is deleted',
|
||||
},
|
||||
'account.settings.delete.account.modal.after.button': {
|
||||
id: 'account.settings.delete.account.modal.after.button',
|
||||
defaultMessage: 'Close',
|
||||
description: 'Label on button to close a dialog',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
79
src/account-settings/demographics/Checkboxes.jsx
Normal file
79
src/account-settings/demographics/Checkboxes.jsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CheckBox } from '@edx/paragon';
|
||||
import { DECLINED } from '../data/constants';
|
||||
|
||||
const Checkboxes = (props) => {
|
||||
const {
|
||||
id,
|
||||
options,
|
||||
values,
|
||||
onChange,
|
||||
} = props;
|
||||
|
||||
const [selected, setSelected] = useState(values);
|
||||
useEffect(() => {
|
||||
onChange(id, selected);
|
||||
}, [selected]);
|
||||
|
||||
const handleToggle = (value, option) => {
|
||||
// If the user checked 'declined', uncheck all other options
|
||||
if (value && option === DECLINED) {
|
||||
setSelected([DECLINED]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If option checked, make sure this option is in `selected` (and remove 'declined')
|
||||
if (value && !selected.includes(option)) {
|
||||
const newSelected = selected.filter(i => i !== DECLINED).concat(option);
|
||||
setSelected(newSelected);
|
||||
}
|
||||
|
||||
// If unchecked, make sure this option is NOT in `selected`
|
||||
if (!value) {
|
||||
setSelected(selected.filter(i => i !== option));
|
||||
}
|
||||
};
|
||||
|
||||
const renderCheckboxes = () => options.map((option, index) => {
|
||||
const isFirst = index === 0;
|
||||
const isChecked = selected.includes(option.value);
|
||||
return (
|
||||
<div key={option.value} className="checkboxOption">
|
||||
<CheckBox
|
||||
type="checkbox"
|
||||
id={option.value}
|
||||
name={option.value}
|
||||
value={option.value}
|
||||
checked={isChecked}
|
||||
autoFocus={isFirst}
|
||||
label={option.label}
|
||||
onChange={(value) => handleToggle(value, option.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div role="group">
|
||||
{renderCheckboxes()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Checkboxes.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
value: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
})),
|
||||
values: PropTypes.arrayOf(PropTypes.string),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Checkboxes.defaultProps = {
|
||||
options: [],
|
||||
values: [],
|
||||
};
|
||||
|
||||
export default Checkboxes;
|
||||
357
src/account-settings/demographics/DemographicsSection.jsx
Normal file
357
src/account-settings/demographics/DemographicsSection.jsx
Normal file
@@ -0,0 +1,357 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Hyperlink, Input } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import get from 'lodash.get';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import memoize from 'memoize-one';
|
||||
import { demographicsSectionSelector } from '../data/selectors';
|
||||
import EditableField from '../EditableField';
|
||||
import Checkboxes from './Checkboxes';
|
||||
import Alert from '../Alert';
|
||||
import { saveMultipleSettings, updateDraft } from '../data/actions';
|
||||
import {
|
||||
OTHER,
|
||||
SELF_DESCRIBE,
|
||||
} from '../data/constants';
|
||||
import messages from './DemographicsSection.messages';
|
||||
|
||||
class DemographicsSection extends React.Component {
|
||||
// We check the `demographicsOptions` prop to see if it is empty before we attempt to extract and
|
||||
// format the available options for each question from the API response.
|
||||
getApiOptions = memoize((demographicsOptions) => (this.hasRetrievedDemographicsOptions() && {
|
||||
demographicsGenderOptions: this.addDefaultOption('account.settings.field.demographics.gender.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.gender.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
/* Ethnicity options don't need the blank/default option */
|
||||
demographicsEthnicityOptions: demographicsOptions.actions.POST.user_ethnicity.child.children.ethnicity.choices.map(
|
||||
key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}),
|
||||
),
|
||||
demographicsIncomeOptions: this.addDefaultOption('account.settings.field.demographics.income.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.income.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsMilitaryHistoryOptions: this.addDefaultOption('account.settings.field.demographics.military_history.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.military_history.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsEducationLevelOptions: this.addDefaultOption('account.settings.field.demographics.education_level.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.learner_education_level.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsWorkStatusOptions: this.addDefaultOption('account.settings.field.demographics.work_status.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.work_status.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsWorkSectorOptions: this.addDefaultOption('account.settings.field.demographics.work_sector.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.current_work_sector.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
}));
|
||||
|
||||
ethnicityFieldDisplay = (demographicsEthnicityOptions) => {
|
||||
let ethnicities = [];
|
||||
if (get(this, 'props.formValues.demographics_user_ethnicity')) {
|
||||
ethnicities = this.props.formValues.demographics_user_ethnicity;
|
||||
}
|
||||
return ethnicities.map((e) => {
|
||||
const matchingOption = demographicsEthnicityOptions.filter(option => option.value === e)[0];
|
||||
return matchingOption && matchingOption.label;
|
||||
}).join(', ');
|
||||
}
|
||||
|
||||
handleEditableFieldChange = (name, value) => {
|
||||
this.props.updateDraft(name, value);
|
||||
};
|
||||
|
||||
handleSubmit = (formId) => {
|
||||
// We have some custom fields in this section. Instead of relying on the
|
||||
// submitted values, submit the values stored in 'drafts'.
|
||||
const { drafts } = this.props;
|
||||
const settingsArray = Object.entries(drafts).map(([field, value]) => ({
|
||||
formId: field,
|
||||
commitValues: value,
|
||||
}));
|
||||
|
||||
this.props.saveMultipleSettings(settingsArray, formId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility method that adds the specified message as a default option to the list of available
|
||||
* choices.
|
||||
*
|
||||
* @param {*} messageId id of message matching desired default label text
|
||||
*/
|
||||
addDefaultOption(messageId) {
|
||||
return [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages[messageId]),
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method that helps determine if we were able to retrieve the available options for
|
||||
* the Demographics questions. Returns true if the `demographicsOptions` prop is _not_ empty,
|
||||
* otherwise false. This prop being empty is indicative of a failure communicating with the
|
||||
* Demographics IDA's API.
|
||||
*/
|
||||
hasRetrievedDemographicsOptions() {
|
||||
return !isEmpty(this.props.formValues.demographicsOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* If an error is encountered when trying to communicate with the Demographics IDA then we will
|
||||
* display an Alert letting the user know that their info will not be displayed and temporarily
|
||||
* cannot be updated.
|
||||
*/
|
||||
renderDemographicsServiceIssueWarning() {
|
||||
if (!isEmpty(this.props.formErrors.demographicsError)
|
||||
|| !this.hasRetrievedDemographicsOptions()) {
|
||||
return (
|
||||
<div
|
||||
tabIndex="-1"
|
||||
ref={this.alertRef}
|
||||
>
|
||||
<Alert className="alert alert-danger" role="alert">
|
||||
<FormattedMessage
|
||||
id="account.settings.message.demographics.service.issue"
|
||||
defaultMessage="An error occurred attempting to retrieve or save your account information. Please try again later."
|
||||
description="alert message informing the user that the there is a problem retrieving or updating information from the Demographics microservice"
|
||||
/>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const editableFieldProps = {
|
||||
onChange: this.handleEditableFieldChange,
|
||||
onSubmit: this.handleSubmit,
|
||||
};
|
||||
|
||||
const {
|
||||
demographicsGenderOptions,
|
||||
demographicsEthnicityOptions,
|
||||
demographicsIncomeOptions,
|
||||
demographicsMilitaryHistoryOptions,
|
||||
demographicsEducationLevelOptions,
|
||||
demographicsWorkStatusOptions,
|
||||
demographicsWorkSectorOptions,
|
||||
} = this.getApiOptions(this.props.formValues.demographicsOptions);
|
||||
|
||||
const showSelfDescribe = this.props.formValues.demographics_gender === SELF_DESCRIBE;
|
||||
const showWorkStatusDescribe = this.props.formValues.demographics_work_status === OTHER;
|
||||
|
||||
return (
|
||||
<div className="account-section" id="demographics-information" ref={this.props.forwardRef}>
|
||||
<h2 className="section-heading">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.demographics.information'])}
|
||||
</h2>
|
||||
<p>
|
||||
<Hyperlink
|
||||
destination={`${getConfig().MARKETING_SITE_BASE_URL}/demographics`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{this.props.intl.formatMessage(
|
||||
messages['account.settings.section.demographics.why'],
|
||||
{
|
||||
siteName: getConfig().SITE_NAME,
|
||||
},
|
||||
)}
|
||||
</Hyperlink>
|
||||
</p>
|
||||
{this.renderDemographicsServiceIssueWarning()}
|
||||
{/*
|
||||
If the demographicsOptions props are empty then there is no need to display the fields as
|
||||
the user will not have any choices available to select, nor will they be able to update
|
||||
their answers.
|
||||
*/}
|
||||
{this.hasRetrievedDemographicsOptions() && (
|
||||
<div id="demographics-fields">
|
||||
<EditableField
|
||||
name="demographics_gender"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_gender}
|
||||
userSuppliedValue={showSelfDescribe ? this.props.formValues.demographics_gender_description : null}
|
||||
options={demographicsGenderOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender.empty'])}
|
||||
{...editableFieldProps}
|
||||
>
|
||||
{showSelfDescribe && (
|
||||
<Input
|
||||
name="demographics_gender_description"
|
||||
id="field-demographics_gender_description"
|
||||
type="text"
|
||||
placeholder={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender_description.empty'])}
|
||||
value={this.props.formValues.demographics_gender_description}
|
||||
onChange={(e) => this.handleEditableFieldChange('demographics_gender_description', e.target.value)}
|
||||
aria-label={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender_description'])}
|
||||
className="mt-1"
|
||||
/>
|
||||
)}
|
||||
</EditableField>
|
||||
<EditableField
|
||||
name="demographics_user_ethnicity"
|
||||
type="select"
|
||||
hidden
|
||||
value={this.ethnicityFieldDisplay(demographicsEthnicityOptions)}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.ethnicity'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.ethnicity.empty'])}
|
||||
{...editableFieldProps}
|
||||
>
|
||||
<Checkboxes
|
||||
id="demographics_user_ethnicity"
|
||||
options={demographicsEthnicityOptions}
|
||||
values={this.props.formValues.demographics_user_ethnicity}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
</EditableField>
|
||||
<EditableField
|
||||
name="demographics_income"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_income}
|
||||
options={demographicsIncomeOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.income'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.income.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="demographics_military_history"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_military_history}
|
||||
options={demographicsMilitaryHistoryOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.military_history'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.military_history.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="demographics_learner_education_level"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_learner_education_level}
|
||||
options={demographicsEducationLevelOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.learner_education_level'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.learner_education_level.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="demographics_parent_education_level"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_parent_education_level}
|
||||
options={demographicsEducationLevelOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.parent_education_level'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.parent_education_level.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="demographics_work_status"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_work_status}
|
||||
userSuppliedValue={showWorkStatusDescribe
|
||||
? this.props.formValues.demographics_work_status_description
|
||||
: null}
|
||||
options={demographicsWorkStatusOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status.empty'])}
|
||||
{...editableFieldProps}
|
||||
>
|
||||
{showWorkStatusDescribe && (
|
||||
<Input
|
||||
name="demographics_work_status_description"
|
||||
id="field-demographics_work_status_description"
|
||||
type="text"
|
||||
placeholder={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status_description.empty'])}
|
||||
value={this.props.formValues.demographics_work_status_description}
|
||||
onChange={(e) => this.handleEditableFieldChange('demographics_work_status_description', e.target.value)}
|
||||
aria-label={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status_description'])}
|
||||
className="mt-1"
|
||||
/>
|
||||
)}
|
||||
</EditableField>
|
||||
<EditableField
|
||||
name="demographics_current_work_sector"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_current_work_sector}
|
||||
options={demographicsWorkSectorOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.current_work_sector'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.current_work_sector.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="demographics_future_work_sector"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_future_work_sector}
|
||||
options={demographicsWorkSectorOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.future_work_sector'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.future_work_sector.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DemographicsSection.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
formValues: PropTypes.shape({
|
||||
demographics_gender: PropTypes.string,
|
||||
demographics_user_ethnicity: PropTypes.array,
|
||||
demographics_income: PropTypes.string,
|
||||
demographics_military_history: PropTypes.string,
|
||||
demographics_learner_education_level: PropTypes.string,
|
||||
demographics_parent_education_level: PropTypes.string,
|
||||
demographics_work_status: PropTypes.string,
|
||||
demographics_current_work_sector: PropTypes.string,
|
||||
demographics_future_work_sector: PropTypes.string,
|
||||
demographics_work_status_description: PropTypes.string,
|
||||
demographics_gender_description: PropTypes.string,
|
||||
demographicsOptions: PropTypes.object,
|
||||
}).isRequired,
|
||||
drafts: PropTypes.shape({
|
||||
demographics_gender: PropTypes.string,
|
||||
demographics_user_ethnicity: PropTypes.array,
|
||||
demographics_income: PropTypes.string,
|
||||
demographics_military_history: PropTypes.string,
|
||||
demographics_learner_education_level: PropTypes.string,
|
||||
demographics_parent_education_level: PropTypes.string,
|
||||
demographics_work_status: PropTypes.string,
|
||||
demographics_current_work_sector: PropTypes.string,
|
||||
demographics_future_work_sector: PropTypes.string,
|
||||
demographics_work_status_description: PropTypes.string,
|
||||
demographics_gender_description: PropTypes.string,
|
||||
demographicsOptions: PropTypes.object,
|
||||
}).isRequired,
|
||||
formErrors: PropTypes.shape({
|
||||
demographicsError: PropTypes.string,
|
||||
}).isRequired,
|
||||
forwardRef: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
saveMultipleSettings: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(demographicsSectionSelector, {
|
||||
saveMultipleSettings,
|
||||
updateDraft,
|
||||
})(injectIntl(DemographicsSection));
|
||||
@@ -0,0 +1,170 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
/* Demographics section heading */
|
||||
'account.settings.section.demographics.information': {
|
||||
id: 'account.settings.section.demographics.information',
|
||||
defaultMessage: 'Optional Information',
|
||||
description: 'The optional information section heading.',
|
||||
},
|
||||
/* Gender identity */
|
||||
'account.settings.field.demographics.gender': {
|
||||
id: 'account.settings.field.demographics.gender',
|
||||
defaultMessage: 'Gender identity',
|
||||
description: 'Label for account settings gender identity field.',
|
||||
},
|
||||
'account.settings.field.demographics.gender.empty': {
|
||||
id: 'account.settings.field.demographics.gender.empty',
|
||||
defaultMessage: 'Add gender identity',
|
||||
description: 'Placeholder for empty account settings gender identity field.',
|
||||
},
|
||||
'account.settings.field.demographics.gender.options.empty': {
|
||||
id: 'account.settings.field.demographics.gender.options.empty',
|
||||
defaultMessage: 'Select a gender identity',
|
||||
description: 'Placeholder for the gender identity options dropdown.',
|
||||
},
|
||||
'account.settings.field.demographics.gender_description': {
|
||||
id: 'account.settings.field.demographics.gender_description',
|
||||
defaultMessage: 'Gender identity description',
|
||||
description: 'Label for account settings gender identity description field.',
|
||||
},
|
||||
'account.settings.field.demographics.gender_description.empty': {
|
||||
id: 'account.settings.field.demographics.gender_description.empty',
|
||||
defaultMessage: 'Enter description',
|
||||
description: 'Placeholder for empty account settings gender identity field.',
|
||||
},
|
||||
/* Ethnicity */
|
||||
'account.settings.field.demographics.ethnicity': {
|
||||
id: 'account.settings.field.demographics.ethnicity',
|
||||
defaultMessage: 'Race/Ethnicity identity',
|
||||
description: 'Label for account settings ethnic background field.',
|
||||
},
|
||||
'account.settings.field.demographics.ethnicity.empty': {
|
||||
id: 'account.settings.field.demographics.ethnicity.empty',
|
||||
defaultMessage: 'Add race/ethnicity identity',
|
||||
description: 'Placeholder for empty account settings ethnic background field.',
|
||||
},
|
||||
'account.settings.field.demographics.ethnicity.options.empty': {
|
||||
id: 'account.settings.field.demographics.ethnicity.options.empty',
|
||||
defaultMessage: 'Select all that apply', // TODO: Is this the desired text?
|
||||
description: 'Placeholder for the ethnic background options field.',
|
||||
},
|
||||
/* Income */
|
||||
'account.settings.field.demographics.income': {
|
||||
id: 'account.settings.field.demographics.income',
|
||||
defaultMessage: 'Family income',
|
||||
description: 'Label for account settings household income field.',
|
||||
},
|
||||
'account.settings.field.demographics.income.empty': {
|
||||
id: 'account.settings.field.demographics.income.empty',
|
||||
defaultMessage: 'Add family income',
|
||||
description: 'Placeholder for empty account settings household income field.',
|
||||
},
|
||||
'account.settings.field.demographics.income.options.empty': {
|
||||
id: 'account.settings.field.demographics.income.options.empty',
|
||||
defaultMessage: 'Select a family income range',
|
||||
description: 'Placeholder for the household income dropdown.',
|
||||
},
|
||||
/* Military history */
|
||||
'account.settings.field.demographics.military_history': {
|
||||
id: 'account.settings.field.demographics.military_history',
|
||||
defaultMessage: 'U.S. Military status',
|
||||
description: 'Label for account settings military history field.',
|
||||
},
|
||||
'account.settings.field.demographics.military_history.empty': {
|
||||
id: 'account.settings.field.demographics.military_history.empty',
|
||||
defaultMessage: 'Add military status',
|
||||
description: 'Placeholder for empty account settings military history field.',
|
||||
},
|
||||
'account.settings.field.demographics.military_history.options.empty': {
|
||||
id: 'account.settings.field.demographics.military_history.options.empty',
|
||||
defaultMessage: 'Select military status',
|
||||
description: 'Placeholder for the military history dropdown.',
|
||||
},
|
||||
/* Learner and family education level */
|
||||
'account.settings.field.demographics.learner_education_level': {
|
||||
id: 'account.settings.field.demographics.learner_education_level',
|
||||
defaultMessage: 'Your education level',
|
||||
description: 'Label for account settings learner education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.learner_education_level.empty': {
|
||||
id: 'account.settings.field.demographics.learner_education_level.empty',
|
||||
defaultMessage: 'Add education level',
|
||||
description: 'Placeholder for empty account settings learner education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.parent_education_level': {
|
||||
id: 'account.settings.field.demographics.parent_education_level',
|
||||
defaultMessage: 'Parents/Guardians education level',
|
||||
description: 'Label for account settings parent education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.parent_education_level.empty': {
|
||||
id: 'account.settings.field.demographics.parent_education_level.empty',
|
||||
defaultMessage: 'Add education level',
|
||||
description: 'Placeholder for empty account settings parent education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.education_level.options.empty': {
|
||||
id: 'account.settings.field.demographics.education_level.options.empty',
|
||||
defaultMessage: 'Select education level',
|
||||
description: 'Placeholder for the education level options dropdown.',
|
||||
},
|
||||
/* Work status */
|
||||
'account.settings.field.demographics.work_status': {
|
||||
id: 'account.settings.field.demographics.work_status',
|
||||
defaultMessage: 'Employment status',
|
||||
description: 'Label for account settings work status field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status.empty': {
|
||||
id: 'account.settings.field.demographics.work_status.empty',
|
||||
defaultMessage: 'Add employment status',
|
||||
description: 'Placeholder for empty account settings work status field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status.options.empty': {
|
||||
id: 'account.settings.field.demographics.work_status.options.empty',
|
||||
defaultMessage: 'Select employment status',
|
||||
description: 'Placeholder for the work status options dropdown.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status_description': {
|
||||
id: 'account.settings.field.demographics.work_status_description',
|
||||
defaultMessage: 'Employment status description',
|
||||
description: 'Label for account settings work status description field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status_description.empty': {
|
||||
id: 'account.settings.field.demographics.work_status_description.empty',
|
||||
defaultMessage: 'Enter description',
|
||||
description: 'Placeholder for empty account settings work status description field.',
|
||||
},
|
||||
/* Work sector */
|
||||
'account.settings.field.demographics.current_work_sector': {
|
||||
id: 'account.settings.field.demographics.current_work_sector',
|
||||
defaultMessage: 'Current work industry',
|
||||
description: 'Label for account settings current work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.current_work_sector.empty': {
|
||||
id: 'account.settings.field.demographics.current_work_sector.empty',
|
||||
defaultMessage: 'Add work industry',
|
||||
description: 'Placeholder for empty account settings current work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.future_work_sector': {
|
||||
id: 'account.settings.field.demographics.future_work_sector',
|
||||
defaultMessage: 'Future work industry',
|
||||
description: 'Label for account settings future work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.future_work_sector.empty': {
|
||||
id: 'account.settings.field.demographics.future_work_sector.empty',
|
||||
defaultMessage: 'Add work industry',
|
||||
description: 'Placeholder for empty account settings future work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_sector.options.empty': {
|
||||
id: 'account.settings.field.demographics.work_sector.options.empty',
|
||||
defaultMessage: 'Select work industry',
|
||||
description: 'Placeholder for the work sector options dropdown.',
|
||||
},
|
||||
/* Legal copy link text */
|
||||
'account.settings.section.demographics.why': {
|
||||
id: 'account.settings.section.demographics.why',
|
||||
defaultMessage: 'Why does {siteName} collect this information?',
|
||||
description: 'Link text for a link to external legal text',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
140
src/account-settings/demographics/data/service.js
Normal file
140
src/account-settings/demographics/data/service.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import get from 'lodash.get';
|
||||
import { convertData, TO, FROM } from './utils';
|
||||
|
||||
/**
|
||||
* Utility method that attempts to extract errors from the response of a PATCH request in order to
|
||||
* display a warning or otherwise meaningful message to the user.
|
||||
*
|
||||
* @param {Error} error
|
||||
*/
|
||||
export function createDemographicsError(error) {
|
||||
const apiError = Object.create(error);
|
||||
// If the error received has the `httpResponseData` field in it, then we should have reason to
|
||||
// believe the Demographics service is alive and responding. Extract errors from fields where
|
||||
// appropriate so we can display them to the user.
|
||||
if (get(error, 'customAttributes.httpErrorResponseData')) {
|
||||
apiError.fieldErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
|
||||
if (get(apiError, 'fieldErrors.gender_description')) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
apiError.fieldErrors.demographics_gender = apiError.fieldErrors.gender_description[0];
|
||||
delete apiError.fieldErrors.gender_description;
|
||||
} else if (get(apiError, 'fieldErrors.work_status_description')) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
apiError.fieldErrors.demographics_work_status = apiError.fieldErrors.work_status_description[0];
|
||||
delete apiError.fieldErrors.work_status_description;
|
||||
}
|
||||
// Otherwise, when the service is down, the error response will not contain a
|
||||
// `httpErrorResponseData` field. Add a generic 'demographicsError' field to the fieldErrors that
|
||||
// will trigger showing an Alert to the user to them them know the update was unsuccessful.
|
||||
} else {
|
||||
apiError.fieldErrors = {
|
||||
demographicsError: error.customAttributes.httpErrorType,
|
||||
};
|
||||
}
|
||||
|
||||
return apiError;
|
||||
}
|
||||
|
||||
/**
|
||||
* post all of the data related to demographics.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
* @param {Object} commitValues { demographics }
|
||||
*/
|
||||
export async function postDemographics(userId) {
|
||||
const requestConfig = { headers: { 'Content-Type': 'application/json' } };
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/`;
|
||||
const commitValues = { user: userId };
|
||||
let data = {};
|
||||
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.post(requestUrl, commitValues, requestConfig)
|
||||
.catch((error) => {
|
||||
const apiError = createDemographicsError(error);
|
||||
throw apiError;
|
||||
}));
|
||||
|
||||
return convertData(data, FROM);
|
||||
}
|
||||
|
||||
/**
|
||||
* get all data related to the demographics.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
*/
|
||||
export async function getDemographics(userId) {
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/${userId}/`;
|
||||
let data = {};
|
||||
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.get(requestUrl));
|
||||
|
||||
data = convertData(data, FROM);
|
||||
} catch (error) {
|
||||
const apiError = Object.create(error);
|
||||
// if the API called resulted in this user receiving a 404 then follow up with a POST call to
|
||||
// try and create the demographics entity on the backend
|
||||
if (apiError.customAttributes.httpErrorStatus) {
|
||||
if (apiError.customAttributes.httpErrorStatus === 404) {
|
||||
data = await postDemographics(userId);
|
||||
}
|
||||
} else {
|
||||
data = {
|
||||
user: userId,
|
||||
demographics_gender: '',
|
||||
demographics_gender_description: '',
|
||||
demographics_income: '',
|
||||
demographics_learner_education_level: '',
|
||||
demographics_parent_education_level: '',
|
||||
demographics_military_history: '',
|
||||
demographics_work_status: '',
|
||||
demographics_work_status_description: '',
|
||||
demographics_current_work_sector: '',
|
||||
demographics_future_work_sector: '',
|
||||
demographics_user_ethnicity: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* patch all of the data related to demographics.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
* @param {Object} commitValues { demographics }
|
||||
*/
|
||||
export async function patchDemographics(userId, commitValues) {
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/${userId}/`;
|
||||
const convertedCommitValues = convertData(commitValues, TO);
|
||||
let data = {};
|
||||
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.patch(requestUrl, convertedCommitValues)
|
||||
.catch((error) => {
|
||||
const apiError = createDemographicsError(error);
|
||||
throw apiError;
|
||||
}));
|
||||
|
||||
return convertData(data, FROM);
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieve the options for each field from the Demographics API
|
||||
*/
|
||||
export async function getDemographicsOptions() {
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/`;
|
||||
let data = {};
|
||||
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient().options(requestUrl));
|
||||
} catch (error) {
|
||||
// We are catching and suppressing errors here on purpose. If an error occurs during the
|
||||
// getDemographicsOptions call we will pass back an empty `data` object. Downstream we make
|
||||
// the assumption that if the demographicsOptions object is empty that there was an issue or
|
||||
// error communicating with the service/API.
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
63
src/account-settings/demographics/data/utils.js
Normal file
63
src/account-settings/demographics/data/utils.js
Normal file
@@ -0,0 +1,63 @@
|
||||
export const TO = 'to';
|
||||
export const FROM = 'from';
|
||||
export const DEMOGRAPHICS_FIELDS = [
|
||||
'demographics_gender',
|
||||
'demographics_gender_description',
|
||||
'demographics_income',
|
||||
'demographics_learner_education_level',
|
||||
'demographics_parent_education_level',
|
||||
'demographics_military_history',
|
||||
'demographics_work_status',
|
||||
'demographics_work_status_description',
|
||||
'demographics_current_work_sector',
|
||||
'demographics_future_work_sector',
|
||||
'demographics_user_ethnicity',
|
||||
];
|
||||
|
||||
// Frontend wants (example):
|
||||
// demographics_user_ethnicity: ["asian", "white", "other"]
|
||||
//
|
||||
// Demographics wants (example):
|
||||
// user_ethnicity: [
|
||||
// { ethnicity: "asian" },
|
||||
// { ethnicity: "white" },
|
||||
// { ethnicity: "other" }
|
||||
// ]
|
||||
function convertEthnicity(ethnicityData, direction) {
|
||||
if (direction === FROM) {
|
||||
return ethnicityData.map(e => e.ethnicity);
|
||||
}
|
||||
|
||||
if (direction === TO) {
|
||||
return ethnicityData.map(e => ({ ethnicity: e }));
|
||||
}
|
||||
|
||||
return ethnicityData;
|
||||
}
|
||||
|
||||
// Handles conversion of data to/from Demographics IDA to/from format needed for
|
||||
// frontend
|
||||
// * handles ethnicity field
|
||||
// * adds/removes 'demographics' to/from key
|
||||
// * replace `null` with empty string or empty string with null
|
||||
export function convertData(dataObject, direction) {
|
||||
const converted = {};
|
||||
|
||||
Object.entries(dataObject).forEach(([key, value]) => {
|
||||
let newValue = value;
|
||||
|
||||
if (key.includes('ethnicity')) {
|
||||
newValue = convertEthnicity(value, direction);
|
||||
}
|
||||
|
||||
if (direction === TO) {
|
||||
converted[key.replace('demographics_', '')] = newValue || null;
|
||||
}
|
||||
|
||||
if (direction === FROM) {
|
||||
converted[`demographics_${key}`] = newValue || '';
|
||||
}
|
||||
});
|
||||
|
||||
return converted;
|
||||
}
|
||||
@@ -0,0 +1,583 @@
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Provider } from 'react-redux';
|
||||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import renderer from 'react-test-renderer';
|
||||
import DemographicsSection from '../DemographicsSection';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
const IntlDemographicsSection = injectIntl(DemographicsSection);
|
||||
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ demographicsSectionSelector: () => ({}) })));
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('DemographicsSection', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
updateDraft: jest.fn(),
|
||||
formValues: {
|
||||
demographics_gender: 'declined',
|
||||
demographics_gender_description: '',
|
||||
demographics_user_ethnicity: [],
|
||||
demographics_income: 'declined',
|
||||
demographics_military_history: 'declined',
|
||||
demographics_learner_education_level: 'declined',
|
||||
demographics_parent_education_level: 'declined',
|
||||
demographics_work_status: 'declined',
|
||||
demographics_work_status_description: '',
|
||||
demographics_current_work_sector: 'declined',
|
||||
demographics_future_work_sector: 'declined',
|
||||
demographics_user: 1,
|
||||
demographicsOptions: {
|
||||
actions: {
|
||||
POST: {
|
||||
gender: {
|
||||
choices: [
|
||||
{
|
||||
value: 'woman',
|
||||
display_name: 'Woman',
|
||||
},
|
||||
{
|
||||
value: 'man',
|
||||
display_name: 'Man',
|
||||
},
|
||||
{
|
||||
value: 'non-binary',
|
||||
display_name: 'Non-binary',
|
||||
},
|
||||
{
|
||||
value: 'self-describe',
|
||||
display_name: 'Prefer to self describe',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
income: {
|
||||
choices: [
|
||||
{
|
||||
value: 'less-than-10k',
|
||||
display_name: 'Less than US $10,000',
|
||||
},
|
||||
{
|
||||
value: '10k-25k',
|
||||
display_name: 'US $10,000 - $25,000',
|
||||
},
|
||||
{
|
||||
value: '25k-50k',
|
||||
display_name: 'US $25,000 - $50,000',
|
||||
},
|
||||
{
|
||||
value: '50k-75k',
|
||||
display_name: 'US $50,000 - $75,000',
|
||||
},
|
||||
{
|
||||
value: '75k-100k',
|
||||
display_name: 'US $75,000 - $100,000',
|
||||
},
|
||||
{
|
||||
value: 'over-100k',
|
||||
display_name: 'Over US $100,000',
|
||||
},
|
||||
{
|
||||
value: 'unsure',
|
||||
display_name: "I don't know",
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
learner_education_level: {
|
||||
choices: [
|
||||
{
|
||||
value: 'no-high-school',
|
||||
display_name: 'No High School',
|
||||
},
|
||||
{
|
||||
value: 'some-high-school',
|
||||
display_name: 'Some High School',
|
||||
},
|
||||
{
|
||||
value: 'high-school-ged-equivalent',
|
||||
display_name: 'High School diploma, GED, or equivalent',
|
||||
},
|
||||
{
|
||||
value: 'some-college',
|
||||
display_name: 'Some college, but no degree',
|
||||
},
|
||||
{
|
||||
value: 'associates',
|
||||
display_name: 'Associates degree',
|
||||
},
|
||||
{
|
||||
value: 'bachelors',
|
||||
display_name: 'Bachelors degree',
|
||||
},
|
||||
{
|
||||
value: 'masters',
|
||||
display_name: 'Masters degree',
|
||||
},
|
||||
{
|
||||
value: 'professional',
|
||||
display_name: 'Professional degree',
|
||||
},
|
||||
{
|
||||
value: 'doctorate',
|
||||
display_name: 'Doctorate degree',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
parent_education_level: {
|
||||
choices: [
|
||||
{
|
||||
value: 'no-high-school',
|
||||
display_name: 'No High School',
|
||||
},
|
||||
{
|
||||
value: 'some-high-school',
|
||||
display_name: 'Some High School',
|
||||
},
|
||||
{
|
||||
value: 'high-school-ged-equivalent',
|
||||
display_name: 'High School diploma, GED, or equivalent',
|
||||
},
|
||||
{
|
||||
value: 'some-college',
|
||||
display_name: 'Some college, but no degree',
|
||||
},
|
||||
{
|
||||
value: 'associates',
|
||||
display_name: 'Associates degree',
|
||||
},
|
||||
{
|
||||
value: 'bachelors',
|
||||
display_name: 'Bachelors degree',
|
||||
},
|
||||
{
|
||||
value: 'masters',
|
||||
display_name: 'Masters degree',
|
||||
},
|
||||
{
|
||||
value: 'professional',
|
||||
display_name: 'Professional degree',
|
||||
},
|
||||
{
|
||||
value: 'doctorate',
|
||||
display_name: 'Doctorate degree',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
military_history: {
|
||||
choices: [
|
||||
{
|
||||
value: 'never-served',
|
||||
display_name: 'Never served in the military',
|
||||
},
|
||||
{
|
||||
value: 'training',
|
||||
display_name: 'Only on active duty for training',
|
||||
},
|
||||
{
|
||||
value: 'active',
|
||||
display_name: 'Now on active duty',
|
||||
},
|
||||
{
|
||||
value: 'previously-active',
|
||||
display_name: 'On active duty in the past, but not now',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
work_status: {
|
||||
choices: [
|
||||
{
|
||||
value: 'full-time',
|
||||
display_name: 'Employed, working full-time',
|
||||
},
|
||||
{
|
||||
value: 'part-time',
|
||||
display_name: 'Employed, working part-time',
|
||||
},
|
||||
{
|
||||
value: 'self-employed',
|
||||
display_name: 'Self-Employed',
|
||||
},
|
||||
{
|
||||
value: 'not-employed-looking',
|
||||
display_name: 'Not employed, looking for work',
|
||||
},
|
||||
{
|
||||
value: 'not-employed-not-looking',
|
||||
display_name: 'Not employed, not looking for work',
|
||||
},
|
||||
{
|
||||
value: 'unable',
|
||||
display_name: 'Unable to work',
|
||||
},
|
||||
{
|
||||
value: 'retired',
|
||||
display_name: 'Retired',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Other',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
current_work_sector: {
|
||||
choices: [
|
||||
{
|
||||
value: 'accommodation-food',
|
||||
display_name: 'Accommodation and Food Services',
|
||||
},
|
||||
{
|
||||
value: 'administrative-support-waste-remediation',
|
||||
display_name: 'Administrative and Support and Waste Management and Remediation Services',
|
||||
},
|
||||
{
|
||||
value: 'agriculture-forestry-fishing-hunting',
|
||||
display_name: 'Agriculture, Forestry, Fishing and Hunting',
|
||||
},
|
||||
{
|
||||
value: 'arts-entertainment-recreation',
|
||||
display_name: 'Arts, Entertainment, and Recreation',
|
||||
},
|
||||
{
|
||||
value: 'construction',
|
||||
display_name: 'Construction',
|
||||
},
|
||||
{
|
||||
value: 'educational',
|
||||
display_name: 'Education Services',
|
||||
},
|
||||
{
|
||||
value: 'finance-insurance',
|
||||
display_name: 'Finance and Insurance',
|
||||
},
|
||||
{
|
||||
value: 'healthcare-social',
|
||||
display_name: 'Health Care and Social Assistance',
|
||||
},
|
||||
{
|
||||
value: 'information',
|
||||
display_name: 'Information',
|
||||
},
|
||||
{
|
||||
value: 'management',
|
||||
display_name: 'Management of Companies and Enterprises',
|
||||
},
|
||||
{
|
||||
value: 'manufacturing',
|
||||
display_name: 'Manufacturing',
|
||||
},
|
||||
{
|
||||
value: 'mining-quarry-oil-gas',
|
||||
display_name: 'Mining, Quarrying, and Oil and Gas Extraction',
|
||||
},
|
||||
{
|
||||
value: 'professional-scientific-technical',
|
||||
display_name: 'Professional, Scientific, and Technical Services',
|
||||
},
|
||||
{
|
||||
value: 'public-admin',
|
||||
display_name: 'Public Administration',
|
||||
},
|
||||
{
|
||||
value: 'real-estate',
|
||||
display_name: 'Real Estate and Rental and Leasing',
|
||||
},
|
||||
{
|
||||
value: 'retail',
|
||||
display_name: 'Retail Trade',
|
||||
},
|
||||
{
|
||||
value: 'transport-warehousing',
|
||||
display_name: 'Transportation and Warehousing',
|
||||
},
|
||||
{
|
||||
value: 'utilities',
|
||||
display_name: 'Utilities',
|
||||
},
|
||||
{
|
||||
value: 'trade',
|
||||
display_name: 'Wholesale Trade',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Other',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
future_work_sector: {
|
||||
choices: [
|
||||
{
|
||||
value: 'accommodation-food',
|
||||
display_name: 'Accommodation and Food Services',
|
||||
},
|
||||
{
|
||||
value: 'administrative-support-waste-remediation',
|
||||
display_name: 'Administrative and Support and Waste Management and Remediation Services',
|
||||
},
|
||||
{
|
||||
value: 'agriculture-forestry-fishing-hunting',
|
||||
display_name: 'Agriculture, Forestry, Fishing and Hunting',
|
||||
},
|
||||
{
|
||||
value: 'arts-entertainment-recreation',
|
||||
display_name: 'Arts, Entertainment, and Recreation',
|
||||
},
|
||||
{
|
||||
value: 'construction',
|
||||
display_name: 'Construction',
|
||||
},
|
||||
{
|
||||
value: 'educational',
|
||||
display_name: 'Education Services',
|
||||
},
|
||||
{
|
||||
value: 'finance-insurance',
|
||||
display_name: 'Finance and Insurance',
|
||||
},
|
||||
{
|
||||
value: 'healthcare-social',
|
||||
display_name: 'Health Care and Social Assistance',
|
||||
},
|
||||
{
|
||||
value: 'information',
|
||||
display_name: 'Information',
|
||||
},
|
||||
{
|
||||
value: 'management',
|
||||
display_name: 'Management of Companies and Enterprises',
|
||||
},
|
||||
{
|
||||
value: 'manufacturing',
|
||||
display_name: 'Manufacturing',
|
||||
},
|
||||
{
|
||||
value: 'mining-quarry-oil-gas',
|
||||
display_name: 'Mining, Quarrying, and Oil and Gas Extraction',
|
||||
},
|
||||
{
|
||||
value: 'professional-scientific-technical',
|
||||
display_name: 'Professional, Scientific, and Technical Services',
|
||||
},
|
||||
{
|
||||
value: 'public-admin',
|
||||
display_name: 'Public Administration',
|
||||
},
|
||||
{
|
||||
value: 'real-estate',
|
||||
display_name: 'Real Estate and Rental and Leasing',
|
||||
},
|
||||
{
|
||||
value: 'retail',
|
||||
display_name: 'Retail Trade',
|
||||
},
|
||||
{
|
||||
value: 'transport-warehousing',
|
||||
display_name: 'Transportation and Warehousing',
|
||||
},
|
||||
{
|
||||
value: 'utilities',
|
||||
display_name: 'Utilities',
|
||||
},
|
||||
{
|
||||
value: 'trade',
|
||||
display_name: 'Wholesale Trade',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Other',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
user_ethnicity: {
|
||||
child: {
|
||||
children: {
|
||||
ethnicity: {
|
||||
choices: [
|
||||
{
|
||||
value: 'american-indian-or-alaska-native',
|
||||
display_name: 'American Indian or Alaska Native',
|
||||
},
|
||||
{
|
||||
value: 'asian',
|
||||
display_name: 'Asian',
|
||||
},
|
||||
{
|
||||
value: 'black-or-african-american',
|
||||
display_name: 'Black or African American',
|
||||
},
|
||||
{
|
||||
value: 'hispanic-latin-spanish',
|
||||
display_name: 'Hispanic, Latin, or Spanish origin',
|
||||
},
|
||||
{
|
||||
value: 'middle-eastern-or-north-african',
|
||||
display_name: 'Middle Eastern or North African',
|
||||
},
|
||||
{
|
||||
value: 'native-hawaiian-or-pacific-islander',
|
||||
display_name: 'Native Hawaiian or Other Pacific Islander',
|
||||
},
|
||||
{
|
||||
value: 'white',
|
||||
display_name: 'White',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Some other race, ethnicity, or origin',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
formErrors: {},
|
||||
intl: {},
|
||||
forwardRef: () => {},
|
||||
drafts: {},
|
||||
};
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
patch: async () => ({
|
||||
data: { status: 200 },
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 1 }));
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render an Alert if an error occurs', () => {
|
||||
props = {
|
||||
...props,
|
||||
formErrors: {
|
||||
demographicsError: 'api-error',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should set user input correctly when user provides gender self-description', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_gender: 'self-describe',
|
||||
demographics_gender_description: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should set user input correctly when user provides answers to work_status question', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_work_status: 'other',
|
||||
demographics_work_status_description: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render ethnicity text correctly', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_user_ethnicity: ['asian'],
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render ethnicity correctly when multiple options are selected', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_user_ethnicity: ['hispanic-latin-spanish', 'white'],
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render an Alert when demographicsOptions props are empty', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
demographicsOptions: null,
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3922 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DemographicsSection should render 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="d-inline-block align-text-top"
|
||||
>
|
||||
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-label=""
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="d-inline-block align-text-top"
|
||||
>
|
||||
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-label=""
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert alert-danger"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<span>
|
||||
An error occurred attempting to retrieve or save your account information. Please try again later.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render an Alert when demographicsOptions props are empty 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="d-inline-block align-text-top"
|
||||
>
|
||||
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-label=""
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert alert-danger"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<span>
|
||||
An error occurred attempting to retrieve or save your account information. Please try again later.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render ethnicity correctly when multiple options are selected 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="d-inline-block align-text-top"
|
||||
>
|
||||
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-label=""
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Hispanic, Latin, or Spanish origin, White
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="d-inline-block align-text-top"
|
||||
>
|
||||
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-label=""
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Asian
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should set user input correctly when user provides answers to work_status question 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="d-inline-block align-text-top"
|
||||
>
|
||||
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-label=""
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Other: test
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should set user input correctly when user provides gender self-description 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="d-inline-block align-text-top"
|
||||
>
|
||||
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-label=""
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer to self describe: test
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,13 +1,5 @@
|
||||
import ConnectedAccountSettingsPage from './AccountSettingsPage';
|
||||
import reducer from './reducers';
|
||||
import saga from './sagas';
|
||||
import { configureApiService } from './service';
|
||||
import { storeName } from './selectors';
|
||||
|
||||
export {
|
||||
ConnectedAccountSettingsPage,
|
||||
reducer,
|
||||
saga,
|
||||
configureApiService,
|
||||
storeName,
|
||||
};
|
||||
export { default } from './AccountSettingsPage';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
export { storeName } from './data/selectors';
|
||||
export { default as NotFoundPage } from './NotFoundPage';
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
import {
|
||||
FETCH_ACCOUNT,
|
||||
OPEN_FORM,
|
||||
CLOSE_FORM,
|
||||
SAVE_ACCOUNT,
|
||||
RESET_PASSWORD,
|
||||
UPDATE_DRAFT,
|
||||
RESET_DRAFTS,
|
||||
FETCH_THIRD_PARTY_AUTH_PROVIDERS,
|
||||
} from './actions';
|
||||
|
||||
export const defaultState = {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
loadingError: null,
|
||||
data: null,
|
||||
values: {},
|
||||
errors: {},
|
||||
confirmationValues: {},
|
||||
drafts: {},
|
||||
saveState: null,
|
||||
resetPasswordState: null,
|
||||
};
|
||||
|
||||
const accountSettingsReducer = (state = defaultState, action) => {
|
||||
let dispatcherIsOpenForm;
|
||||
|
||||
switch (action.type) {
|
||||
case FETCH_ACCOUNT.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
loaded: false,
|
||||
loadingError: null,
|
||||
};
|
||||
case FETCH_ACCOUNT.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
values: Object.assign({}, state.values, action.payload.values),
|
||||
loading: false,
|
||||
loaded: true,
|
||||
loadingError: null,
|
||||
};
|
||||
case FETCH_ACCOUNT.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
loaded: false,
|
||||
loadingError: action.payload.error,
|
||||
};
|
||||
case FETCH_ACCOUNT.RESET:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
loaded: false,
|
||||
loadingError: null,
|
||||
};
|
||||
|
||||
case OPEN_FORM:
|
||||
return {
|
||||
...state,
|
||||
openFormId: action.payload.formId,
|
||||
saveState: null,
|
||||
errors: {},
|
||||
drafts: {},
|
||||
};
|
||||
case CLOSE_FORM:
|
||||
dispatcherIsOpenForm = action.payload.formId === state.openFormId;
|
||||
if (dispatcherIsOpenForm) {
|
||||
return {
|
||||
...state,
|
||||
openFormId: null,
|
||||
saveState: null,
|
||||
errors: {},
|
||||
drafts: {},
|
||||
};
|
||||
}
|
||||
return state;
|
||||
case UPDATE_DRAFT:
|
||||
return {
|
||||
...state,
|
||||
drafts: Object.assign({}, state.drafts, {
|
||||
[action.payload.name]: action.payload.value,
|
||||
}),
|
||||
saveState: null,
|
||||
errors: {},
|
||||
};
|
||||
|
||||
case RESET_DRAFTS:
|
||||
return {
|
||||
...state,
|
||||
drafts: {},
|
||||
};
|
||||
|
||||
case SAVE_ACCOUNT.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'pending',
|
||||
errors: {},
|
||||
};
|
||||
case SAVE_ACCOUNT.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'complete',
|
||||
values: Object.assign({}, state.values, action.payload.values),
|
||||
errors: {},
|
||||
confirmationValues: Object.assign(
|
||||
{},
|
||||
state.confirmationValues,
|
||||
action.payload.confirmationValues,
|
||||
),
|
||||
};
|
||||
case SAVE_ACCOUNT.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'error',
|
||||
errors: Object.assign({}, state.errors, action.payload.errors),
|
||||
};
|
||||
case SAVE_ACCOUNT.RESET:
|
||||
return {
|
||||
...state,
|
||||
saveState: null,
|
||||
errors: {},
|
||||
};
|
||||
|
||||
case RESET_PASSWORD.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
resetPasswordState: 'pending',
|
||||
};
|
||||
case RESET_PASSWORD.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
resetPasswordState: 'complete',
|
||||
};
|
||||
|
||||
case FETCH_THIRD_PARTY_AUTH_PROVIDERS.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
thirdPartyAuthLoading: true,
|
||||
thirdPartyAuthLoaded: false,
|
||||
thirdPartyAuthLoadingError: null,
|
||||
};
|
||||
case FETCH_THIRD_PARTY_AUTH_PROVIDERS.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
authProviders: action.payload.providers,
|
||||
thirdPartyAuthLoading: false,
|
||||
thirdPartyAuthLoaded: true,
|
||||
thirdPartyAuthLoadingError: null,
|
||||
};
|
||||
case FETCH_THIRD_PARTY_AUTH_PROVIDERS.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
thirdPartyAuthLoading: false,
|
||||
thirdPartyAuthLoaded: false,
|
||||
thirdPartyAuthLoadingError: action.payload.error,
|
||||
};
|
||||
case FETCH_THIRD_PARTY_AUTH_PROVIDERS.RESET:
|
||||
return {
|
||||
...state,
|
||||
thirdPartyAuthLoading: false,
|
||||
thirdPartyAuthLoaded: false,
|
||||
thirdPartyAuthLoadingError: null,
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default accountSettingsReducer;
|
||||
47
src/account-settings/reset-password/ConfirmationAlert.jsx
Normal file
47
src/account-settings/reset-password/ConfirmationAlert.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import Alert from '../Alert';
|
||||
|
||||
const ConfirmationAlert = (props) => {
|
||||
const { email } = props;
|
||||
|
||||
const technicalSupportLink = (
|
||||
<Hyperlink
|
||||
destination="https://support.edx.org/hc/en-us/articles/206212088-What-if-I-did-not-receive-a-password-reset-message-"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="account.settings.editable.field.password.reset.button.confirmation.support.link"
|
||||
defaultMessage="technical support"
|
||||
description="link text used in message: account.settings.editable.field.password.reset.button.confirmation 'Contact technical support.'"
|
||||
/>
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
return (
|
||||
<Alert
|
||||
className="alert-warning mt-n2"
|
||||
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="account.settings.editable.field.password.reset.button.confirmation"
|
||||
defaultMessage="We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}."
|
||||
description="The password reset button in account settings"
|
||||
values={{
|
||||
email,
|
||||
technicalSupportLink,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
ConfirmationAlert.propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ConfirmationAlert;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import Alert from '../Alert';
|
||||
|
||||
const RequestInProgressAlert = () => (
|
||||
<Alert
|
||||
className="alert-warning mt-n2"
|
||||
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="account.settings.editable.field.password.reset.button.forbidden"
|
||||
defaultMessage="Your previous request is in progress, please try again in few moments."
|
||||
description="A message displayed when a previous password reset request is still in progress."
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
export default RequestInProgressAlert;
|
||||
71
src/account-settings/reset-password/ResetPassword.jsx
Normal file
71
src/account-settings/reset-password/ResetPassword.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { StatefulButton } from '@edx/paragon';
|
||||
|
||||
import { resetPassword } from './data/actions';
|
||||
import messages from './messages';
|
||||
import ConfirmationAlert from './ConfirmationAlert';
|
||||
import RequestInProgressAlert from './RequestInProgressAlert';
|
||||
|
||||
const ResetPassword = (props) => {
|
||||
const { email, intl, status } = props;
|
||||
return (
|
||||
<div className="form-group">
|
||||
<h6 aria-level="3">
|
||||
<FormattedMessage
|
||||
id="account.settings.editable.field.password.reset.label"
|
||||
defaultMessage="Password"
|
||||
description="The password label in account settings"
|
||||
/>
|
||||
</h6>
|
||||
<p>
|
||||
<StatefulButton
|
||||
variant="link"
|
||||
state={status}
|
||||
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 (status === 'pending') {
|
||||
e.preventDefault();
|
||||
}
|
||||
props.resetPassword(email);
|
||||
}}
|
||||
disabledStates={[]}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['account.settings.editable.field.password.reset.button']),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
{status === 'complete' ? <ConfirmationAlert email={email} /> : null}
|
||||
{status === 'forbidden' ? <RequestInProgressAlert /> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ResetPassword.propTypes = {
|
||||
email: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
resetPassword: PropTypes.func.isRequired,
|
||||
status: PropTypes.string,
|
||||
};
|
||||
|
||||
ResetPassword.defaultProps = {
|
||||
email: '',
|
||||
status: null,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => state.accountSettings.resetPassword;
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
resetPassword,
|
||||
},
|
||||
)(injectIntl(ResetPassword));
|
||||
24
src/account-settings/reset-password/data/actions.js
Normal file
24
src/account-settings/reset-password/data/actions.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { AsyncActionType } from '../../data/utils';
|
||||
|
||||
export const RESET_PASSWORD = new AsyncActionType('ACCOUNT_SETTINGS', 'RESET_PASSWORD');
|
||||
|
||||
export const resetPassword = email => ({
|
||||
type: RESET_PASSWORD.BASE,
|
||||
payload: { email },
|
||||
});
|
||||
|
||||
export const resetPasswordBegin = () => ({
|
||||
type: RESET_PASSWORD.BEGIN,
|
||||
});
|
||||
|
||||
export const resetPasswordSuccess = () => ({
|
||||
type: RESET_PASSWORD.SUCCESS,
|
||||
});
|
||||
|
||||
export const resetPasswordReset = () => ({
|
||||
type: RESET_PASSWORD.RESET,
|
||||
});
|
||||
|
||||
export const resetPasswordForbidden = () => ({
|
||||
type: RESET_PASSWORD.FORBIDDEN,
|
||||
});
|
||||
32
src/account-settings/reset-password/data/reducers.js
Normal file
32
src/account-settings/reset-password/data/reducers.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { RESET_PASSWORD } from './actions';
|
||||
|
||||
export const defaultState = {
|
||||
status: null,
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action = null) => {
|
||||
if (action !== null) {
|
||||
switch (action.type) {
|
||||
case RESET_PASSWORD.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
status: 'pending',
|
||||
};
|
||||
case RESET_PASSWORD.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
status: 'complete',
|
||||
};
|
||||
case RESET_PASSWORD.FORBIDDEN:
|
||||
return {
|
||||
...state,
|
||||
status: 'forbidden',
|
||||
};
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default reducer;
|
||||
24
src/account-settings/reset-password/data/sagas.js
Normal file
24
src/account-settings/reset-password/data/sagas.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { put, call, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
import {
|
||||
resetPasswordBegin, resetPasswordForbidden, resetPasswordSuccess, RESET_PASSWORD,
|
||||
} from './actions';
|
||||
import { postResetPassword } from './service';
|
||||
|
||||
function* handleResetPassword(action) {
|
||||
yield put(resetPasswordBegin());
|
||||
try {
|
||||
const response = yield call(postResetPassword, action.payload.email);
|
||||
yield put(resetPasswordSuccess(response));
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 403) {
|
||||
yield put(resetPasswordForbidden(error));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function* saga() {
|
||||
yield takeEvery(RESET_PASSWORD.BASE, handleResetPassword);
|
||||
}
|
||||
21
src/account-settings/reset-password/data/service.js
Normal file
21
src/account-settings/reset-password/data/service.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import formurlencoded from 'form-urlencoded';
|
||||
import { handleRequestError } from '../../data/utils';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function postResetPassword(email) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(
|
||||
`${getConfig().LMS_BASE_URL}/password_reset/`,
|
||||
formurlencoded({ email }),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
},
|
||||
)
|
||||
.catch(handleRequestError);
|
||||
|
||||
return data;
|
||||
}
|
||||
4
src/account-settings/reset-password/index.js
Normal file
4
src/account-settings/reset-password/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default } from './ResetPassword';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { RESET_PASSWORD } from './data/actions';
|
||||
export { default as saga } from './data/sagas';
|
||||
11
src/account-settings/reset-password/messages.js
Normal file
11
src/account-settings/reset-password/messages.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.editable.field.password.reset.button': {
|
||||
id: 'account.settings.editable.field.password.reset.button',
|
||||
defaultMessage: 'Reset Password',
|
||||
description: 'The password reset button in account settings',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,94 +0,0 @@
|
||||
|
||||
import { call, put, delay, takeEvery, select } from 'redux-saga/effects';
|
||||
import { push } from 'connected-react-router';
|
||||
import { logAPIErrorResponse } from '@edx/frontend-logging';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
FETCH_ACCOUNT,
|
||||
fetchAccountBegin,
|
||||
fetchAccountSuccess,
|
||||
fetchAccountFailure,
|
||||
closeForm,
|
||||
SAVE_ACCOUNT,
|
||||
saveAccountBegin,
|
||||
saveAccountSuccess,
|
||||
saveAccountFailure,
|
||||
RESET_PASSWORD,
|
||||
resetPasswordBegin,
|
||||
resetPasswordSuccess,
|
||||
FETCH_THIRD_PARTY_AUTH_PROVIDERS,
|
||||
fetchThirdPartyAuthProvidersBegin,
|
||||
fetchThirdPartyAuthProvidersSuccess,
|
||||
fetchThirdPartyAuthProvidersFailure,
|
||||
} from './actions';
|
||||
import { usernameSelector } from './selectors';
|
||||
|
||||
// Services
|
||||
import * as ApiService from './service';
|
||||
|
||||
export function* handleFetchAccount() {
|
||||
try {
|
||||
yield put(fetchAccountBegin());
|
||||
|
||||
const username = yield select(usernameSelector);
|
||||
const values = yield call(ApiService.getAccount, username);
|
||||
yield put(fetchAccountSuccess(values));
|
||||
} catch (e) {
|
||||
logAPIErrorResponse(e);
|
||||
yield put(fetchAccountFailure(e.message));
|
||||
yield put(push('/error'));
|
||||
}
|
||||
}
|
||||
|
||||
export function* handleSaveAccount(action) {
|
||||
try {
|
||||
yield put(saveAccountBegin());
|
||||
|
||||
const username = yield select(usernameSelector);
|
||||
const { commitValues, formId } = action.payload;
|
||||
const commitData = { [formId]: commitValues };
|
||||
const savedValues = yield call(ApiService.patchAccount, username, commitData);
|
||||
yield put(saveAccountSuccess(savedValues, commitData));
|
||||
yield delay(1000);
|
||||
yield put(closeForm(action.payload.formId));
|
||||
} catch (e) {
|
||||
if (e.fieldErrors) {
|
||||
yield put(saveAccountFailure({ fieldErrors: e.fieldErrors }));
|
||||
} else {
|
||||
logAPIErrorResponse(e);
|
||||
yield put(saveAccountFailure(e.message));
|
||||
yield put(push('/error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* handleResetPassword() {
|
||||
try {
|
||||
yield put(resetPasswordBegin());
|
||||
const response = yield call(ApiService.postResetPassword);
|
||||
yield put(resetPasswordSuccess(response));
|
||||
} catch (e) {
|
||||
logAPIErrorResponse(e);
|
||||
yield put(push('/error'));
|
||||
}
|
||||
}
|
||||
|
||||
export function* handleFetchThirdPartyAuthProviders() {
|
||||
try {
|
||||
yield put(fetchThirdPartyAuthProvidersBegin());
|
||||
const authProviders = yield call(ApiService.getThirdPartyAuthProviders);
|
||||
yield put(fetchThirdPartyAuthProvidersSuccess(authProviders));
|
||||
} catch (e) {
|
||||
logAPIErrorResponse(e);
|
||||
yield put(fetchThirdPartyAuthProvidersFailure(e.message));
|
||||
yield put(push('/error'));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* saga() {
|
||||
yield takeEvery(FETCH_ACCOUNT.BASE, handleFetchAccount);
|
||||
yield takeEvery(SAVE_ACCOUNT.BASE, handleSaveAccount);
|
||||
yield takeEvery(RESET_PASSWORD.BASE, handleResetPassword);
|
||||
yield takeEvery(FETCH_THIRD_PARTY_AUTH_PROVIDERS.BASE, handleFetchThirdPartyAuthProviders);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { createSelector, createStructuredSelector } from 'reselect';
|
||||
|
||||
export const storeName = 'account-settings';
|
||||
|
||||
export const usernameSelector = state => state.authentication.username;
|
||||
|
||||
export const accountSettingsSelector = state => ({ ...state[storeName] });
|
||||
|
||||
const editableFieldNameSelector = (state, props) => props.name;
|
||||
|
||||
const valuesSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.values,
|
||||
);
|
||||
|
||||
const draftsSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.drafts,
|
||||
);
|
||||
|
||||
const editableFieldValueSelector = createSelector(
|
||||
editableFieldNameSelector,
|
||||
valuesSelector,
|
||||
draftsSelector,
|
||||
(name, values, drafts) => (drafts[name] !== undefined ? drafts[name] : values[name]),
|
||||
);
|
||||
|
||||
const editableFieldErrorSelector = createSelector(
|
||||
editableFieldNameSelector,
|
||||
accountSettingsSelector,
|
||||
(name, accountSettings) => accountSettings.errors[name],
|
||||
);
|
||||
|
||||
const editableFieldConfirmationValuesSelector = createSelector(
|
||||
editableFieldNameSelector,
|
||||
accountSettingsSelector,
|
||||
(name, accountSettings) => accountSettings.confirmationValues[name],
|
||||
);
|
||||
|
||||
const isEditingSelector = createSelector(
|
||||
editableFieldNameSelector,
|
||||
accountSettingsSelector,
|
||||
(name, accountSettings) => accountSettings.openFormId === name,
|
||||
);
|
||||
|
||||
const saveStateSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.saveState,
|
||||
);
|
||||
|
||||
export const editableFieldSelector = createStructuredSelector({
|
||||
value: editableFieldValueSelector,
|
||||
error: editableFieldErrorSelector,
|
||||
confirmationValue: editableFieldConfirmationValuesSelector,
|
||||
saveState: saveStateSelector,
|
||||
isEditing: isEditingSelector,
|
||||
});
|
||||
|
||||
export const resetPasswordSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => ({
|
||||
resetPasswordState: accountSettings.resetPasswordState,
|
||||
email: accountSettings.values.email,
|
||||
}),
|
||||
);
|
||||
|
||||
export const thirdPartyAuthSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => ({
|
||||
providers: accountSettings.authProviders,
|
||||
loading: accountSettings.thirdPartyAuthLoading,
|
||||
loaded: accountSettings.thirdPartyAuthLoaded,
|
||||
loadingError: accountSettings.thirdPartyAuthLoadingError,
|
||||
}),
|
||||
);
|
||||
@@ -1,128 +0,0 @@
|
||||
import pick from 'lodash.pick';
|
||||
|
||||
let config = {
|
||||
ACCOUNTS_API_BASE_URL: null,
|
||||
ECOMMERCE_API_BASE_URL: null,
|
||||
LMS_BASE_URL: null,
|
||||
PASSWORD_RESET_URL: null,
|
||||
};
|
||||
|
||||
const SOCIAL_PLATFORMS = [
|
||||
{ id: 'twitter', key: 'social_link_twitter' },
|
||||
{ id: 'facebook', key: 'social_link_facebook' },
|
||||
{ id: 'linkedin', key: 'social_link_linkedin' },
|
||||
];
|
||||
|
||||
let apiClient = null;
|
||||
|
||||
function validateConfiguration(newConfig) {
|
||||
Object.keys(config).forEach((key) => {
|
||||
if (newConfig[key] === undefined) {
|
||||
throw new Error(`Service configuration error: ${key} is required.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function configureApiService(newConfig, newApiClient) {
|
||||
validateConfiguration(newConfig);
|
||||
config = pick(newConfig, Object.keys(config));
|
||||
apiClient = newApiClient;
|
||||
}
|
||||
|
||||
|
||||
function unpackFieldErrors(fieldErrors) {
|
||||
const unpackedFieldErrors = fieldErrors;
|
||||
if (fieldErrors.social_links) {
|
||||
SOCIAL_PLATFORMS.forEach(({ key }) => {
|
||||
unpackedFieldErrors[key] = fieldErrors.social_links;
|
||||
});
|
||||
}
|
||||
return Object.entries(unpackedFieldErrors)
|
||||
.reduce((acc, [k, v]) => {
|
||||
acc[k] = v.user_message;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function unpackAccountResponseData(data) {
|
||||
const unpackedData = data;
|
||||
|
||||
SOCIAL_PLATFORMS.forEach(({ id, key }) => {
|
||||
const platformData = data.social_links.find(({ platform }) => platform === id);
|
||||
unpackedData[key] = typeof platformData === 'object' ? platformData.social_link : '';
|
||||
});
|
||||
|
||||
if (Array.isArray(data.language_proficiencies)) {
|
||||
if (data.language_proficiencies.length) {
|
||||
unpackedData.language_proficiencies = data.language_proficiencies[0].code;
|
||||
} else {
|
||||
unpackedData.language_proficiencies = '';
|
||||
}
|
||||
}
|
||||
|
||||
return unpackedData;
|
||||
}
|
||||
function packAccountCommitData(commitData) {
|
||||
const packedData = commitData;
|
||||
|
||||
SOCIAL_PLATFORMS.forEach(({ id, key }) => {
|
||||
if (commitData[key]) {
|
||||
packedData.social_links = [{ platform: id, social_link: commitData[key] }];
|
||||
}
|
||||
delete packedData[key];
|
||||
});
|
||||
|
||||
if (commitData.language_proficiencies) {
|
||||
packedData.language_proficiencies = [{ code: commitData.language_proficiencies }];
|
||||
}
|
||||
return packedData;
|
||||
}
|
||||
|
||||
|
||||
function handleRequestError(error) {
|
||||
if (error.response && error.response.data.field_errors) {
|
||||
const apiError = Object.create(error);
|
||||
apiError.fieldErrors = unpackFieldErrors(error.response.data.field_errors);
|
||||
throw apiError;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
export async function getAccount(username) {
|
||||
const { data } = await apiClient.get(`${config.ACCOUNTS_API_BASE_URL}/${username}`);
|
||||
return unpackAccountResponseData(data);
|
||||
}
|
||||
|
||||
export async function patchAccount(username, commitValues) {
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/merge-patch+json' },
|
||||
};
|
||||
|
||||
const { data } = await apiClient.patch(
|
||||
`${config.ACCOUNTS_API_BASE_URL}/${username}`,
|
||||
packAccountCommitData(commitValues),
|
||||
requestConfig,
|
||||
).catch(handleRequestError);
|
||||
|
||||
return unpackAccountResponseData(data);
|
||||
}
|
||||
|
||||
export async function postResetPassword() {
|
||||
const { data } = await apiClient
|
||||
.post(config.PASSWORD_RESET_URL)
|
||||
.catch(handleRequestError);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getThirdPartyAuthProviders() {
|
||||
const { data } = await apiClient.get(`${config.LMS_BASE_URL}/api/third_party_auth/v0/providers/user_status`)
|
||||
.catch(handleRequestError);
|
||||
|
||||
return data.map(({ connect_url: connectUrl, disconnect_url: disconnectUrl, ...provider }) => ({
|
||||
...provider,
|
||||
connectUrl: `${config.LMS_BASE_URL}${connectUrl}`,
|
||||
disconnectUrl: `${config.LMS_BASE_URL}${disconnectUrl}`,
|
||||
}));
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user