Compare commits
659 Commits
open-relea
...
frontend-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa7267f17c | ||
|
|
7b754edef8 | ||
|
|
0bb7ee2fd4 | ||
|
|
335dd7819d | ||
|
|
b18a01c302 | ||
|
|
981dccf2d5 | ||
|
|
e8be148ca9 | ||
|
|
167a8bd9a8 | ||
|
|
db2336ac09 | ||
|
|
a13c25d4ea | ||
|
|
9452b72525 | ||
|
|
3601cb6c05 | ||
|
|
b73c0f0f26 | ||
|
|
37feffc0db | ||
|
|
c9d2813009 | ||
|
|
8ec67d9ed2 | ||
|
|
1d149f12ea | ||
|
|
9516ee0e92 | ||
|
|
29fd7176c8 | ||
|
|
16ddd7abba | ||
|
|
577ef6ab0b | ||
|
|
8268fa4eab | ||
|
|
5665f8a0d6 | ||
|
|
4b7a3207e0 | ||
|
|
3db0289aab | ||
|
|
85d3eca9e4 | ||
|
|
f7fd2959ac | ||
|
|
8652206aa4 | ||
|
|
7a5e03967d | ||
|
|
da19dfaadc | ||
|
|
60d960276d | ||
|
|
c8a6f9fbd8 | ||
|
|
2ef5a7baff | ||
|
|
d59c641b3d | ||
|
|
4ea80a2a09 | ||
|
|
45fe50f7f7 | ||
|
|
933a177c78 | ||
|
|
14fff570a4 | ||
|
|
3d0f3806e1 | ||
|
|
b597e2cc14 | ||
|
|
22dc85470a | ||
|
|
9a26dc7088 | ||
|
|
770a248d8c | ||
|
|
70cb1803b4 | ||
|
|
e9aa787ade | ||
|
|
01e2f1af79 | ||
|
|
1e67d51394 | ||
|
|
95936419c2 | ||
|
|
00ada93994 | ||
|
|
c57a924cc3 | ||
|
|
dc2f03dfad | ||
|
|
5c7a521705 | ||
|
|
074374b2af | ||
|
|
0a3aad38dc | ||
|
|
1730b5a2c4 | ||
|
|
12269c2c8e | ||
|
|
630dbefb7e | ||
|
|
00c8697c59 | ||
|
|
8df3d06598 | ||
|
|
f6ed6ee1f5 | ||
|
|
7e8d22ec6a | ||
|
|
d0595e679d | ||
|
|
4c7e713b6e | ||
|
|
bc0683281f | ||
|
|
b954345b38 | ||
|
|
ca4f78bd1c | ||
|
|
3dc8b156fe | ||
|
|
a818cd4dc4 | ||
|
|
2e9dcd165e | ||
|
|
53a52b8f06 | ||
|
|
c66facee92 | ||
|
|
3270e27c94 | ||
|
|
01cd125d4f | ||
|
|
5fcef4edf4 | ||
|
|
d33c79a525 | ||
|
|
d36c61d44e | ||
|
|
aed6081d37 | ||
|
|
66a4eef910 | ||
|
|
6da6fedc57 | ||
|
|
a7e095f3bf | ||
|
|
9a393d8e43 | ||
|
|
1a5c4f2404 | ||
|
|
13aaca03fd | ||
|
|
422aff1915 | ||
|
|
cac4e42364 | ||
|
|
c6d39884c8 | ||
|
|
4bb3678d4c | ||
|
|
5c2951de40 | ||
|
|
35d9ae8fec | ||
|
|
ff32632411 | ||
|
|
2c97964f0b | ||
|
|
fa748719ae | ||
|
|
1fb7b96c40 | ||
|
|
e13d04a5ea | ||
|
|
e3f5129746 | ||
|
|
24b054f55f | ||
|
|
8498d9f04f | ||
|
|
0fa58d8112 | ||
|
|
312b114ef7 | ||
|
|
a05b29e406 | ||
|
|
6e3e67467a | ||
|
|
09b5c8b72d | ||
|
|
5e71737240 | ||
|
|
eb726e80ab | ||
|
|
4f1272b01c | ||
|
|
30e3fcdcba | ||
|
|
b13fc58bad | ||
|
|
31b31ba345 | ||
|
|
f7449bfdcc | ||
|
|
cb0f05955f | ||
|
|
f1586d260a | ||
|
|
b268c43978 | ||
|
|
ed58a41042 | ||
|
|
7e4ba48de8 | ||
|
|
e9e16cc595 | ||
|
|
88d786b49f | ||
|
|
a24fea54ec | ||
|
|
8980b22fff | ||
|
|
389d923a08 | ||
|
|
714851c329 | ||
|
|
b27300e63d | ||
|
|
0869a82a31 | ||
|
|
507525262c | ||
|
|
a69170a4d2 | ||
|
|
26203bb744 | ||
|
|
21fb4702b4 | ||
|
|
aa8d1f9473 | ||
|
|
b6e9df9778 | ||
|
|
a7ed0cd62a | ||
|
|
e61fada0b2 | ||
|
|
3b0b1be197 | ||
|
|
616847c6b5 | ||
|
|
95b41131fc | ||
|
|
59a3150706 | ||
|
|
774b67b208 | ||
|
|
9a855cd30e | ||
|
|
9c6647aeae | ||
|
|
ecdec9083a | ||
|
|
1c0746c907 | ||
|
|
e7f8a5d1ff | ||
|
|
d927b43c01 | ||
|
|
1a36d69bd8 | ||
|
|
db90ce5975 | ||
|
|
b22d411481 | ||
|
|
d7fff65162 | ||
|
|
cf5d69c5ba | ||
|
|
53d26746c1 | ||
|
|
048433a135 | ||
|
|
31136b0523 | ||
|
|
caaf8547dc | ||
|
|
e92332859a | ||
|
|
55529b0b74 | ||
|
|
7682971b63 | ||
|
|
6aaf615c63 | ||
|
|
3512fc56c9 | ||
|
|
fda2ef1977 | ||
|
|
3784c2a6df | ||
|
|
04d74389ed | ||
|
|
1d338d0127 | ||
|
|
0fd6e34a8e | ||
|
|
9d89c79fb2 | ||
|
|
bfc31696ee | ||
|
|
8ebdc1030f | ||
|
|
8c3d706071 | ||
|
|
ee8912530d | ||
|
|
db91e0f1a4 | ||
|
|
5f2d0c089e | ||
|
|
75be372ec9 | ||
|
|
9ef81f7485 | ||
|
|
6a32790f28 | ||
|
|
9f5f7f9a64 | ||
|
|
06f6e3537a | ||
|
|
44f0295d0d | ||
|
|
19be2e93e2 | ||
|
|
5090a62594 | ||
|
|
dc2bbee481 | ||
|
|
8dc37f9bba | ||
|
|
14d93d2f6c | ||
|
|
768351fc15 | ||
|
|
c3dc8b7330 | ||
|
|
9fd2d3f75a | ||
|
|
760fa0c6a2 | ||
|
|
cf6a62266c | ||
|
|
f16fd36a24 | ||
|
|
c30e1b3f17 | ||
|
|
9985afde2d | ||
|
|
ccd6e95f70 | ||
|
|
fb904a5cc2 | ||
|
|
a255bbe2b8 | ||
|
|
a314878cc4 | ||
|
|
61b8b0a509 | ||
|
|
f144cd857e | ||
|
|
c922a1cd29 | ||
|
|
6cefe7b3c0 | ||
|
|
a3611b026c | ||
|
|
d33d3d9a4b | ||
|
|
ec68baa74e | ||
|
|
7216b0f5cb | ||
|
|
83b21bff7d | ||
|
|
cb52875651 | ||
|
|
89bcceaadc | ||
|
|
51dc454948 | ||
|
|
c7d57e24c3 | ||
|
|
54c702fb7f | ||
|
|
da6c9ebd1e | ||
|
|
f987e45c97 | ||
|
|
1f2ac2e1e7 | ||
|
|
b13c26b06a | ||
|
|
b5ace63438 | ||
|
|
15a81ee09f | ||
|
|
fecfdc101c | ||
|
|
4fdf3ef190 | ||
|
|
88f4ac2349 | ||
|
|
deaebd3333 | ||
|
|
6b0655aed3 | ||
|
|
2901345ea6 | ||
|
|
e51677b0f0 | ||
|
|
676166a160 | ||
|
|
761995b623 | ||
|
|
abc1b682d4 | ||
|
|
3b37cbb76c | ||
|
|
6c6229916e | ||
|
|
2aeb7b0710 | ||
|
|
a36c0629e9 | ||
|
|
5a9d3887a7 | ||
|
|
3fdd68b203 | ||
|
|
a1264deda0 | ||
|
|
f9fa5360d9 | ||
|
|
b4a1ec6e60 | ||
|
|
c30eb00c8d | ||
|
|
fc75405597 | ||
|
|
5726470881 | ||
|
|
6bbae1f7f0 | ||
|
|
ef5fe2fa91 | ||
|
|
585856ad89 | ||
|
|
8d1dbfe55d | ||
|
|
8a9c78662f | ||
|
|
8eb0029970 | ||
|
|
2dc20dd3fd | ||
|
|
2fefab5311 | ||
|
|
e7ab90a778 | ||
|
|
28226e4d03 | ||
|
|
e59ab99d45 | ||
|
|
3f1fa50608 | ||
|
|
71a37ace78 | ||
|
|
d4e16347b8 | ||
|
|
76c94ef0e1 | ||
|
|
db6351560a | ||
|
|
bd06a7ada7 | ||
|
|
42810a5e36 | ||
|
|
01e1dd027a | ||
|
|
275b7f9ba5 | ||
|
|
e18b6497ba | ||
|
|
14a4efe86b | ||
|
|
5e602c9c0b | ||
|
|
e2eb4a6eae | ||
|
|
8b195f1531 | ||
|
|
650b47a631 | ||
|
|
bd5c909bbc | ||
|
|
eb347f4d68 | ||
|
|
b3b88779b4 | ||
|
|
ade2bad564 | ||
|
|
4ca59f0984 | ||
|
|
ab9b129b43 | ||
|
|
5d9ee134c1 | ||
|
|
b6e4c34b77 | ||
|
|
c36595170d | ||
|
|
cf6b4dae98 | ||
|
|
994852741e | ||
|
|
9403fd84f6 | ||
|
|
4d5562e2dd | ||
|
|
cad60cff61 | ||
|
|
a9ce16add6 | ||
|
|
8a2755751b | ||
|
|
1401c8b156 | ||
|
|
5cf7db140c | ||
|
|
860d2e6f5e | ||
|
|
a434c0a7b9 | ||
|
|
c6c5521ecf | ||
|
|
7175183d6a | ||
|
|
8566cf9095 | ||
|
|
efeedb3247 | ||
|
|
a3849495d5 | ||
|
|
47a7d77e55 | ||
|
|
943ff02a38 | ||
|
|
8b48dc8bad | ||
|
|
e195dbf1b4 | ||
|
|
7ae5956f06 | ||
|
|
f48a06b8c5 | ||
|
|
48d2766c13 | ||
|
|
d677d11558 | ||
|
|
c396800657 | ||
|
|
1baf21aad9 | ||
|
|
dc068cbf33 | ||
|
|
2f8b9963ce | ||
|
|
6957ad0401 | ||
|
|
ce616fa409 | ||
|
|
e216036d8d | ||
|
|
69cacc1e3b | ||
|
|
8f6b96983f | ||
|
|
779c5078ca | ||
|
|
e6c3d10b37 | ||
|
|
4cc9e53a4d | ||
|
|
88b583865a | ||
|
|
43f485d841 | ||
|
|
b892ba763e | ||
|
|
896905b457 | ||
|
|
095b91c8cb | ||
|
|
a6e63a8686 | ||
|
|
67c8d79aa2 | ||
|
|
f76185d57d | ||
|
|
f0678ca94c | ||
|
|
e73b646263 | ||
|
|
ddb8494471 | ||
|
|
a576bdf98b | ||
|
|
21dcadba5b | ||
|
|
e770101e4e | ||
|
|
8ca5ea5809 | ||
|
|
d687ea30cb | ||
|
|
ecda751786 | ||
|
|
e58b174c9e | ||
|
|
6c82805c7a | ||
|
|
d1d98794ab | ||
|
|
3c7baaa91b | ||
|
|
fe800f2ee9 | ||
|
|
e1d4e9b474 | ||
|
|
cf7568bcfb | ||
|
|
a0fd863bc4 | ||
|
|
b63341fe99 | ||
|
|
1887167d0e | ||
|
|
57de2b4156 | ||
|
|
aaf6935577 | ||
|
|
03b7859b20 | ||
|
|
2800e89f02 | ||
|
|
0cc03e5fec | ||
|
|
44f1a5f0cd | ||
|
|
2694f6f754 | ||
|
|
18afe62590 | ||
|
|
0b6d3dc9ac | ||
|
|
bd5984f27b | ||
|
|
f899727f8d | ||
|
|
803a2d5572 | ||
|
|
93b0b1b9da | ||
|
|
22b01f347d | ||
|
|
b537f1f82d | ||
|
|
c42e339051 | ||
|
|
528bbcbad8 | ||
|
|
fcc5ce77e7 | ||
|
|
ff4cd80ff2 | ||
|
|
25a6093a78 | ||
|
|
d3a4b4b62d | ||
|
|
fd52682b5c | ||
|
|
a6eb5f75d1 | ||
|
|
6803fb5199 | ||
|
|
f05db64d49 | ||
|
|
038c2a845f | ||
|
|
105486a44a | ||
|
|
7b44687db9 | ||
|
|
7aeac7be3b | ||
|
|
d4665797a1 | ||
|
|
4a02f0ef6d | ||
|
|
b0b8e96025 | ||
|
|
6d5c150d2f | ||
|
|
549d51509b | ||
|
|
ba74aef631 | ||
|
|
941652aba2 | ||
|
|
3b68201748 | ||
|
|
2b8f5a8b3d | ||
|
|
235644e570 | ||
|
|
5e0eef68e3 | ||
|
|
4692477738 | ||
|
|
465d3b481a | ||
|
|
79e29c46c5 | ||
|
|
97e70fa4ab | ||
|
|
60fec4152f | ||
|
|
26a82ec109 | ||
|
|
bf03f8b5ef | ||
|
|
2a4addbb38 | ||
|
|
e2b9ef7a2f | ||
|
|
a6bcc9c054 | ||
|
|
31d2c07c5b | ||
|
|
03e352208f | ||
|
|
ac7285dac5 | ||
|
|
60fe9cff9a | ||
|
|
93f757f3d7 | ||
|
|
6740ad3672 | ||
|
|
ca14d3d279 | ||
|
|
9dbe58b10f | ||
|
|
50d80ef614 | ||
|
|
fe6b76da7e | ||
|
|
7fff13137d | ||
|
|
91282cff74 | ||
|
|
45be830f18 | ||
|
|
122affbb6d | ||
|
|
48a97b769f | ||
|
|
bdcc09f6ba | ||
|
|
ac4fb6a340 | ||
|
|
409d365125 | ||
|
|
53985e94d8 | ||
|
|
0d9a39afd7 | ||
|
|
cbb860bb16 | ||
|
|
695df9aa0b | ||
|
|
603304b799 | ||
|
|
d3e5931d05 | ||
|
|
6804f7e127 | ||
|
|
4b16673780 | ||
|
|
6674025bd4 | ||
|
|
0dab2d03eb | ||
|
|
df1a84feb7 | ||
|
|
334a9b090e | ||
|
|
5d06276838 | ||
|
|
e391e427f1 | ||
|
|
b71328fd3f | ||
|
|
3b9b3f8840 | ||
|
|
30e837306f | ||
|
|
c7a0c1d799 | ||
|
|
337c97e3a0 | ||
|
|
0de4496953 | ||
|
|
359ae7f1fb | ||
|
|
8d467f01dc | ||
|
|
20debcd79e | ||
|
|
6a7cbf88df | ||
|
|
1b3880ee1b | ||
|
|
79cebaf6df | ||
|
|
8686af563e | ||
|
|
85d85007d2 | ||
|
|
9276fe25ad | ||
|
|
9c2dd68752 | ||
|
|
e4a9045e89 | ||
|
|
c1bbbe488a | ||
|
|
45ab2f8175 | ||
|
|
d9c7096fd7 | ||
|
|
c6825393c6 | ||
|
|
9354f11a99 | ||
|
|
e7fc8f52fb | ||
|
|
d8c8f5d7bd | ||
|
|
e5355e7ac8 | ||
|
|
99a80d3e66 | ||
|
|
abf9860f62 | ||
|
|
ccc62a0e48 | ||
|
|
650d3d469f | ||
|
|
ab80fd7671 | ||
|
|
f55b304732 | ||
|
|
65971820d4 | ||
|
|
7da386264b | ||
|
|
b1fe21cded | ||
|
|
40225d7db3 | ||
|
|
b4ba5276ae | ||
|
|
ddff5364ce | ||
|
|
f57f5c4725 | ||
|
|
e6feef00eb | ||
|
|
87487e37d7 | ||
|
|
6b451a4437 | ||
|
|
b10b31860d | ||
|
|
83e2b66c77 | ||
|
|
6fa5681e91 | ||
|
|
e9e48e4eb0 | ||
|
|
17b4933278 | ||
|
|
68dc8a1045 | ||
|
|
21a3e9259d | ||
|
|
a6086fd4bf | ||
|
|
0c427cf5e3 | ||
|
|
75ea8bc207 | ||
|
|
47c06c0f5d | ||
|
|
8e0ab6db4d | ||
|
|
20159f140e | ||
|
|
2d05de92af | ||
|
|
10f93420f4 | ||
|
|
a12f91f7a5 | ||
|
|
8f42e6fbfb | ||
|
|
4ecdf583ea | ||
|
|
e75864b860 | ||
|
|
a697e3c543 | ||
|
|
04607dba1d | ||
|
|
6a4c8d9138 | ||
|
|
dbf716eef5 | ||
|
|
5eaab4f07d | ||
|
|
a139c2f71d | ||
|
|
9e34fdd68d | ||
|
|
109c5d437d | ||
|
|
25d0ecb531 | ||
|
|
767af3c40b | ||
|
|
6a05552969 | ||
|
|
628914dce3 | ||
|
|
be7e204c91 | ||
|
|
c1d4c36a65 | ||
|
|
ce04a04c36 | ||
|
|
346c08e5d6 | ||
|
|
fe61237464 | ||
|
|
c89285f0e8 | ||
|
|
7523a1edb3 | ||
|
|
9f1c16a599 | ||
|
|
fdcef0edc8 | ||
|
|
1f056dfac7 | ||
|
|
2814349f37 | ||
|
|
a4327a98e4 | ||
|
|
50fe0ecb6f | ||
|
|
8e011fdf7b | ||
|
|
a6a35f3762 | ||
|
|
259f4b2a5e | ||
|
|
65a4091e78 | ||
|
|
e5ee7894b0 | ||
|
|
8f781ea867 | ||
|
|
7d208a91ac | ||
|
|
80599a617f | ||
|
|
652559b157 | ||
|
|
5363663170 | ||
|
|
b34041f090 | ||
|
|
a82e3e9918 | ||
|
|
6c15c2a0fd | ||
|
|
da5bf2f533 | ||
|
|
e507548d48 | ||
|
|
ff6c63c86b | ||
|
|
6e24a48570 | ||
|
|
42a0d27b47 | ||
|
|
6a79462567 | ||
|
|
afe39e8b9e | ||
|
|
38afcb8a5a | ||
|
|
5bf65de9e4 | ||
|
|
28423c261d | ||
|
|
0986fd05ab | ||
|
|
21adb70478 | ||
|
|
daca35ffbe | ||
|
|
9fe9164bb2 | ||
|
|
09e010443d | ||
|
|
87377a1443 | ||
|
|
5f89049506 | ||
|
|
4f00bc43b9 | ||
|
|
f732fa7ffc | ||
|
|
963ff21423 | ||
|
|
bb5a8004b9 | ||
|
|
de81959252 | ||
|
|
964dbe26b7 | ||
|
|
be0865c8d4 | ||
|
|
b4afc24ef2 | ||
|
|
392f01cecf | ||
|
|
cb504f2d57 | ||
|
|
599b655ac4 | ||
|
|
e28bc0a62e | ||
|
|
af46aa208f | ||
|
|
2ad4d24262 | ||
|
|
d4c49fb34b | ||
|
|
0034fcf79f | ||
|
|
b3bb371d05 | ||
|
|
1f5c4939e4 | ||
|
|
349c4f799d | ||
|
|
1d3c09f7a5 | ||
|
|
777ba47a3c | ||
|
|
379bb8a9c4 | ||
|
|
1e14092771 | ||
|
|
066f447d38 | ||
|
|
d7d6cd8ad4 | ||
|
|
4a40868afa | ||
|
|
22d0f8b688 | ||
|
|
95cffc73f1 | ||
|
|
759428dd3e | ||
|
|
8bb7dd7430 | ||
|
|
3733497ecb | ||
|
|
1b5f8ee732 | ||
|
|
8fa5bf57b0 | ||
|
|
011c4ef356 | ||
|
|
43a58961f3 | ||
|
|
939c18c765 | ||
|
|
c68ef187ee | ||
|
|
50289e3f52 | ||
|
|
188e29f6e9 | ||
|
|
7d11b73997 | ||
|
|
f1b7c86f88 | ||
|
|
ab62a2d231 | ||
|
|
728fd3f232 | ||
|
|
7503e6c5cb | ||
|
|
8363307cb4 | ||
|
|
34a9caa0bb | ||
|
|
392167e554 | ||
|
|
a087f4e983 | ||
|
|
1ffd5275bc | ||
|
|
87374a045d | ||
|
|
fbe9d9bb1c | ||
|
|
a0b49a7a1c | ||
|
|
a2215044f3 | ||
|
|
c6c4951890 | ||
|
|
f603f433d4 | ||
|
|
3195e199a1 | ||
|
|
fd5bc50a37 | ||
|
|
aedb698133 | ||
|
|
a941e6f073 | ||
|
|
84ea38de77 | ||
|
|
3460734ff7 | ||
|
|
2e9c124ced | ||
|
|
7b61520544 | ||
|
|
077f8d5b84 | ||
|
|
a1606709b2 | ||
|
|
d8b0ab3442 | ||
|
|
694165312c | ||
|
|
2b0dff352d | ||
|
|
23e61bdf72 | ||
|
|
bbfcb61ef3 | ||
|
|
547ce30879 | ||
|
|
a4d0f276be | ||
|
|
8f36e976e7 | ||
|
|
fa602f566f | ||
|
|
7842ec56d2 | ||
|
|
1acc1d0262 | ||
|
|
b69dc2d10a | ||
|
|
4cb73e0851 | ||
|
|
016e6bf184 | ||
|
|
e877d0d749 | ||
|
|
2e1cd607d5 | ||
|
|
e4033037f2 | ||
|
|
9a730117fb | ||
|
|
7a59c32660 | ||
|
|
bfcfe35530 | ||
|
|
cf9bd36eb4 | ||
|
|
af34fc4079 | ||
|
|
33ed66e916 | ||
|
|
4982e2768d | ||
|
|
26ba34e648 | ||
|
|
6e71b7281e | ||
|
|
5f9675a8a7 | ||
|
|
95155df409 | ||
|
|
ed97f0db72 | ||
|
|
abce0a5387 | ||
|
|
ba3388dee8 | ||
|
|
c50b401c14 | ||
|
|
0c50bce43d | ||
|
|
e56914eb80 | ||
|
|
918aa91e49 | ||
|
|
87230613c5 | ||
|
|
e2dfde2432 | ||
|
|
75ea16103a | ||
|
|
f5a6c483e2 | ||
|
|
ead91806e6 | ||
|
|
f5e46741d2 | ||
|
|
9cc77ddc1b | ||
|
|
2e0549a859 | ||
|
|
cb515e81fc | ||
|
|
f8520ca2dc | ||
|
|
cce3146fed | ||
|
|
1e48a55029 | ||
|
|
9d4d6ee2a1 | ||
|
|
b9e4fdaf64 | ||
|
|
7b14eeb42c | ||
|
|
2b2a94f78c | ||
|
|
5e94fa62ed | ||
|
|
f610c5bc70 | ||
|
|
5922947843 | ||
|
|
02da9797b4 | ||
|
|
8cf5b82a78 | ||
|
|
c265d57ea5 | ||
|
|
1069f23239 | ||
|
|
718400dcd1 | ||
|
|
59e112205d | ||
|
|
7948812a78 | ||
|
|
3da88dd557 | ||
|
|
7181950081 | ||
|
|
8fbedf008c | ||
|
|
ac912e5ffc | ||
|
|
299cbbea6c | ||
|
|
3f904fe8f6 |
11
.env
11
.env
@@ -10,6 +10,8 @@ LOGIN_URL=''
|
|||||||
LOGOUT_URL=''
|
LOGOUT_URL=''
|
||||||
MARKETING_SITE_BASE_URL=''
|
MARKETING_SITE_BASE_URL=''
|
||||||
ORDER_HISTORY_URL=''
|
ORDER_HISTORY_URL=''
|
||||||
|
ACCOUNT_SETTINGS_URL=''
|
||||||
|
ACCOUNT_PROFILE_URL=''
|
||||||
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
||||||
SEGMENT_KEY=''
|
SEGMENT_KEY=''
|
||||||
SITE_NAME=''
|
SITE_NAME=''
|
||||||
@@ -22,5 +24,10 @@ LOGO_URL=''
|
|||||||
LOGO_TRADEMARK_URL=''
|
LOGO_TRADEMARK_URL=''
|
||||||
LOGO_WHITE_URL=''
|
LOGO_WHITE_URL=''
|
||||||
FAVICON_URL=''
|
FAVICON_URL=''
|
||||||
ENABLE_LEARNER_RECORD_MFE=''
|
COLLECT_YEAR_OF_BIRTH=true
|
||||||
LEARNER_RECORD_MFE_BASE_URL=''
|
APP_ID=''
|
||||||
|
MFE_CONFIG_API_URL=''
|
||||||
|
SEARCH_CATALOG_URL=''
|
||||||
|
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||||
|
# Fallback in local style files
|
||||||
|
PARAGON_THEME_URLS={}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ PORT=1995
|
|||||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||||
BASE_URL='localhost:1995'
|
BASE_URL='localhost:1995'
|
||||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||||
|
ACCOUNT_SETTINGS_URL=http://localhost:1997
|
||||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||||
|
ACCOUNT_PROFILE_URL=http://localhost:1995
|
||||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||||
LMS_BASE_URL='http://localhost:18000'
|
LMS_BASE_URL='http://localhost:18000'
|
||||||
@@ -23,5 +25,10 @@ LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
|||||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.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
|
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||||
ENABLE_LEARNER_RECORD_MFE=''
|
COLLECT_YEAR_OF_BIRTH=true
|
||||||
LEARNER_RECORD_MFE_BASE_URL='http://localhost:1990'
|
APP_ID=''
|
||||||
|
MFE_CONFIG_API_URL=''
|
||||||
|
SEARCH_CATALOG_URL='http://localhost:18000/courses'
|
||||||
|
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||||
|
# Fallback in local style files
|
||||||
|
PARAGON_THEME_URLS={}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
|||||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||||
LMS_BASE_URL='http://localhost:18000'
|
LMS_BASE_URL='http://localhost:18000'
|
||||||
|
ACCOUNT_SETTINGS_URL='http://localhost:1997'
|
||||||
|
ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||||
LOGIN_URL='http://localhost:18000/login'
|
LOGIN_URL='http://localhost:18000/login'
|
||||||
LOGOUT_URL='http://localhost:18000/logout'
|
LOGOUT_URL='http://localhost:18000/logout'
|
||||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||||
@@ -18,4 +20,9 @@ LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
|||||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||||
ENABLE_LEARNER_RECORD_MFE=''
|
ENABLE_LEARNER_RECORD_MFE=''
|
||||||
|
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||||
LEARNER_RECORD_MFE_BASE_URL='http://localhost:1990'
|
LEARNER_RECORD_MFE_BASE_URL='http://localhost:1990'
|
||||||
|
COLLECT_YEAR_OF_BIRTH=true
|
||||||
|
APP_ID=''
|
||||||
|
MFE_CONFIG_API_URL=''
|
||||||
|
PARAGON_THEME_URLS={}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
const { createConfig } = require('@edx/frontend-build');
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
const { createConfig } = require('@openedx/frontend-build');
|
||||||
|
|
||||||
module.exports = createConfig('eslint');
|
module.exports = createConfig('eslint');
|
||||||
|
|||||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
|||||||
* @edx/community-engineering
|
* @openedx/2U-infinity
|
||||||
|
|||||||
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# Adding new check for github-actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
24
.github/pull_request_template.md
vendored
Normal file
24
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
### Description
|
||||||
|
|
||||||
|
Include a description of your changes here, along with a link to any relevant Jira tickets and/or GitHub issues.
|
||||||
|
|
||||||
|
#### How Has This Been Tested?
|
||||||
|
|
||||||
|
Please describe in detail how you tested your changes.
|
||||||
|
|
||||||
|
#### Screenshots/sandbox (optional):
|
||||||
|
Include a link to the sandbox for design changes or screenshot for before and after. **Remove this section if it's not applicable.**
|
||||||
|
|
||||||
|
|Before|After|
|
||||||
|
|-------|-----|
|
||||||
|
| | |
|
||||||
|
|
||||||
|
#### Merge Checklist
|
||||||
|
|
||||||
|
* [ ] If your update includes visual changes, have they been reviewed by a designer? Send them a link to the Sandbox, if applicable.
|
||||||
|
* [ ] Is there adequate test coverage for your changes?
|
||||||
|
|
||||||
|
#### Post-merge Checklist
|
||||||
|
|
||||||
|
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-infinity** to do it.
|
||||||
|
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.
|
||||||
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Run the workflow that adds new tickets that are either:
|
||||||
|
# - labelled "DEPR"
|
||||||
|
# - title starts with "[DEPR]"
|
||||||
|
# - body starts with "Proposal Date" (this is the first template field)
|
||||||
|
# to the org-wide DEPR project board
|
||||||
|
|
||||||
|
name: Add newly created DEPR issues to the DEPR project board
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
routeissue:
|
||||||
|
uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
|
||||||
|
secrets:
|
||||||
|
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||||
|
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||||
|
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||||
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# This workflow runs when a comment is made on the ticket
|
||||||
|
# If the comment starts with "label: " it tries to apply
|
||||||
|
# the label indicated in rest of comment.
|
||||||
|
# If the comment starts with "remove label: ", it tries
|
||||||
|
# to remove the indicated label.
|
||||||
|
# Note: Labels are allowed to have spaces and this script does
|
||||||
|
# not parse spaces (as often a space is legitimate), so the command
|
||||||
|
# "label: really long lots of words label" will apply the
|
||||||
|
# label "really long lots of words label"
|
||||||
|
|
||||||
|
name: Allows for the adding and removing of labels via comment
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add_remove_labels:
|
||||||
|
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master
|
||||||
|
|
||||||
21
.github/workflows/ci.yml
vendored
21
.github/workflows/ci.yml
vendored
@@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: ci
|
name: ci
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -6,27 +5,25 @@ on:
|
|||||||
- master
|
- master
|
||||||
pull_request:
|
pull_request:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version:
|
|
||||||
- 12
|
|
||||||
npm-test:
|
npm-test:
|
||||||
- i18n_extract
|
- i18n_extract
|
||||||
- is-es5
|
|
||||||
- lint
|
- lint
|
||||||
- test
|
- test
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version-file: '.nvmrc'
|
||||||
- run: npm install -g npm@6
|
|
||||||
- run: make requirements
|
- run: make requirements
|
||||||
- run: make test NPM_TESTS=build
|
- run: make test NPM_TESTS=build
|
||||||
- run: make test NPM_TESTS=${{ matrix.npm-test }}
|
- run: make test NPM_TESTS=${{ matrix.npm-test }}
|
||||||
- name: upload coverage
|
- name: Coverage
|
||||||
uses: codecov/codecov-action@v2
|
if: ${{ matrix.npm-test == 'test' }}
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: false
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
fail_ci_if_error: true
|
||||||
|
|||||||
2
.github/workflows/commitlint.yml
vendored
2
.github/workflows/commitlint.yml
vendored
@@ -7,4 +7,4 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
commitlint:
|
commitlint:
|
||||||
uses: edx/.github/.github/workflows/commitlint.yml@master
|
uses: openedx/.github/.github/workflows/commitlint.yml@master
|
||||||
|
|||||||
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#check package-lock file version
|
||||||
|
|
||||||
|
name: Lockfile Version check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
version-check:
|
||||||
|
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||||
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# This workflow runs when a comment is made on the ticket
|
||||||
|
# If the comment starts with "assign me" it assigns the author to the
|
||||||
|
# ticket (case insensitive)
|
||||||
|
|
||||||
|
name: Assign comment author to ticket if they say "assign me"
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
self_assign_by_comment:
|
||||||
|
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
|
||||||
12
.github/workflows/update-browserslist-db.yml
vendored
Normal file
12
.github/workflows/update-browserslist-db.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: Update Browserslist DB
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * 1'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-browserslist:
|
||||||
|
uses: openedx/.github/.github/workflows/update-browserslist-db.yml@master
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
requirements_bot_github_token: ${{ secrets.requirements_bot_github_token }}
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ temp/babel-plugin-react-intl
|
|||||||
/temp
|
/temp
|
||||||
/.vscode
|
/.vscode
|
||||||
/module.config.js
|
/module.config.js
|
||||||
|
src/i18n/messages
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[main]
|
|
||||||
host = https://www.transifex.com
|
|
||||||
|
|
||||||
[edx-platform.frontend-app-profile]
|
|
||||||
file_filter = src/i18n/messages/<lang>.json
|
|
||||||
source_file = src/i18n/transifex_input.json
|
|
||||||
source_lang = en
|
|
||||||
type = KEYVALUEJSON
|
|
||||||
35
Makefile
Executable file → Normal file
35
Makefile
Executable file → Normal file
@@ -1,16 +1,11 @@
|
|||||||
transifex_resource = frontend-app-profile
|
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
|
||||||
|
|
||||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||||
i18n = ./src/i18n
|
i18n = ./src/i18n
|
||||||
transifex_input = $(i18n)/transifex_input.json
|
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 .
|
# This directory must match .babelrc .
|
||||||
transifex_temp = ./temp/babel-plugin-react-intl
|
transifex_temp = ./temp/babel-plugin-formatjs
|
||||||
|
|
||||||
NPM_TESTS=build i18n_extract lint test is-es5
|
NPM_TESTS=build i18n_extract lint test
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
|
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
|
||||||
@@ -40,20 +35,18 @@ detect_changed_source_translations:
|
|||||||
# Checking for changed translations...
|
# Checking for changed translations...
|
||||||
git diff --exit-code $(i18n)
|
git diff --exit-code $(i18n)
|
||||||
|
|
||||||
# Pushes translations to Transifex. You must run make extract_translations first.
|
|
||||||
push_translations:
|
|
||||||
# Pushing strings to Transifex...
|
|
||||||
tx push -s
|
|
||||||
# Fetching hashes from Transifex...
|
|
||||||
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
|
|
||||||
# Writing out comments to file...
|
|
||||||
$(transifex_utils) $(transifex_temp) --comments
|
|
||||||
# Pushing comments to Transifex...
|
|
||||||
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
|
|
||||||
|
|
||||||
# Pulls translations from Transifex.
|
|
||||||
pull_translations:
|
pull_translations:
|
||||||
tx pull -f --mode reviewed --language=$(transifex_langs)
|
rm -rf src/i18n/messages
|
||||||
|
mkdir src/i18n/messages
|
||||||
|
cd src/i18n/messages \
|
||||||
|
&& atlas pull $(ATLAS_OPTIONS) \
|
||||||
|
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||||
|
translations/paragon/src/i18n/messages:paragon \
|
||||||
|
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||||
|
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||||
|
translations/frontend-app-profile/src/i18n/messages:frontend-app-profile
|
||||||
|
|
||||||
|
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-app-profile
|
||||||
|
|
||||||
# This target is used by Travis.
|
# This target is used by Travis.
|
||||||
validate-no-uncommitted-package-lock-changes:
|
validate-no-uncommitted-package-lock-changes:
|
||||||
|
|||||||
158
README.rst
158
README.rst
@@ -1,57 +1,153 @@
|
|||||||
|Build Status| |Codecov| |license|
|
#####################
|
||||||
|
|
||||||
frontend-app-profile
|
frontend-app-profile
|
||||||
====================
|
#####################
|
||||||
|
|
||||||
This is a micro-frontend application responsible for the display and updating of user profiles. Please tag **@edx/arch-fed** on any PRs or issues.
|
|license-badge| |status-badge| |ci-badge| |codecov-badge|
|
||||||
|
|
||||||
|
.. |license-badge| image:: https://img.shields.io/github/license/openedx/frontend-app-profile.svg
|
||||||
|
:target: https://github.com/openedx/frontend-app-profile/blob/main/LICENSE
|
||||||
|
:alt: License
|
||||||
|
|
||||||
|
.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen
|
||||||
|
|
||||||
|
.. |ci-badge| image:: https://github.com/openedx/frontend-app-profile/actions/workflows/ci.yml/badge.svg
|
||||||
|
:target: https://github.com/openedx/frontend-app-profile/actions/workflows/ci.yml
|
||||||
|
:alt: Continuous Integration
|
||||||
|
|
||||||
|
.. |codecov-badge| image:: https://codecov.io/github/openedx/frontend-app-profile/coverage.svg?branch=main
|
||||||
|
:target: https://codecov.io/github/openedx/frontend-app-profile?branch=main
|
||||||
|
:alt: Codecov
|
||||||
|
|
||||||
|
********
|
||||||
|
Purpose
|
||||||
|
********
|
||||||
|
|
||||||
|
This is a micro-frontend application responsible for the display and updating of user profiles.
|
||||||
|
|
||||||
When a user views their own profile, they're given fields to edit their full name, location, primary spoken language, education, social links, and bio. Each field also has a dropdown to select the visibility of that field - i.e., whether it can be viewed by other learners.
|
When a user views their own profile, they're given fields to edit their full name, location, primary spoken language, education, social links, and bio. Each field also has a dropdown to select the visibility of that field - i.e., whether it can be viewed by other learners.
|
||||||
|
|
||||||
When a user views someone else's profile, they see all those fields that that user set as public.
|
When a user views someone else's profile, they see all those fields that that user set as public.
|
||||||
|
|
||||||
----------
|
***************
|
||||||
|
Getting Started
|
||||||
|
***************
|
||||||
|
|
||||||
Development
|
Installation
|
||||||
-----------
|
============
|
||||||
|
|
||||||
Start Devstack
|
Follow these steps to provision, run, and enable an instance of the
|
||||||
^^^^^^^^^^^^^^
|
Profile MFE for local development via the `devstack`_.
|
||||||
|
|
||||||
To use this application `devstack <https://github.com/edx/devstack>`__ must be running and you must be logged into it.
|
.. _devstack: https://github.com/openedx/devstack#getting-started
|
||||||
|
|
||||||
- Start devstack
|
#. To use this application, `devstack <https://github.com/openedx/devstack>`__ must be running and you must be logged into it.
|
||||||
- Log in (http://localhost:18000/login)
|
|
||||||
|
|
||||||
Start the development server
|
* Start devstack
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
* Log in (http://localhost:18000/login)
|
||||||
|
|
||||||
In this project, install requirements and start the development server by running:
|
#. To run Profile, install requirements and start the development server by running:
|
||||||
|
|
||||||
.. code:: bash
|
.. code-block::
|
||||||
|
|
||||||
npm install
|
1. Clone your new repo:
|
||||||
npm start # The server will run on port 1995
|
|
||||||
|
|
||||||
Once the dev server is up visit http://localhost:1995/u/staff.
|
``git clone https://github.com/openedx/frontend-app-profile.git``
|
||||||
|
|
||||||
----------
|
2. Use node v18.x.
|
||||||
|
|
||||||
Configuration and Deployment
|
The current version of the micro-frontend build scripts support node 18.
|
||||||
----------------------------
|
Using other major versions of node *may* work, but this is unsupported. For
|
||||||
|
convenience, this repository includes an .nvmrc file to help in setting the
|
||||||
|
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
|
||||||
|
|
||||||
|
3. Install npm dependencies:
|
||||||
|
|
||||||
|
``cd frontend-app-profile && npm ci``
|
||||||
|
|
||||||
|
4. Start the dev server:
|
||||||
|
|
||||||
|
``npm start``
|
||||||
|
The server will run on port 1995
|
||||||
|
|
||||||
|
Once the dev server is up, visit http://localhost:1995/u/staff.
|
||||||
|
|
||||||
|
Plugins
|
||||||
|
=======
|
||||||
|
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
|
||||||
|
|
||||||
|
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
This MFE is configured via node environment variables supplied at build time. See the .env file for the list of required environment variables. Example build syntax with a single environment variable:
|
This MFE is configured via node environment variables supplied at build time. See the .env file for the list of required environment variables. Example build syntax with a single environment variable:
|
||||||
|
|
||||||
.. code:: bash
|
.. code-block::
|
||||||
|
|
||||||
NODE_ENV=production ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' npm run build
|
NODE_ENV=production ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' npm run build
|
||||||
|
|
||||||
|
Getting Help
|
||||||
|
============
|
||||||
|
|
||||||
For more information see the document: `Micro-frontend applications in Open
|
If you're having trouble, we have discussion forums at
|
||||||
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/micro-frontends-in-open-edx.html>`__.
|
https://discuss.openedx.org where you can connect with others in the community.
|
||||||
|
|
||||||
.. |Build Status| image:: https://api.travis-ci.org/edx/frontend-app-profile.svg?branch=master
|
Our real-time conversations are on Slack. You can request a `Slack
|
||||||
:target: https://travis-ci.org/edx/frontend-app-profile
|
invitation`_, then join our `community Slack workspace`_. Because this is a
|
||||||
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-app-profile
|
frontend repository, the best place to discuss it would be in the `#wg-frontend
|
||||||
:target: https://codecov.io/gh/edx/frontend-app-profile
|
channel`_.
|
||||||
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-profile.svg
|
|
||||||
:target: @edx/frontend-app-profile
|
For anything non-trivial, the best path is to open an issue in this repository
|
||||||
|
with as many details about the issue you are facing as you can provide. Please tag **@openedx/2u-infinity** on any PRs or issues.
|
||||||
|
|
||||||
|
https://github.com/openedx/frontend-app-profile/issues
|
||||||
|
|
||||||
|
For more information about these options, see the `Getting Help`_ page.
|
||||||
|
|
||||||
|
.. _Slack invitation: https://openedx.org/slack
|
||||||
|
.. _community Slack workspace: https://openedx.slack.com/
|
||||||
|
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
|
||||||
|
.. _Getting Help: https://openedx.org/getting-help
|
||||||
|
|
||||||
|
License
|
||||||
|
=======
|
||||||
|
|
||||||
|
The code in this repository is licensed under the AGPLv3 unless otherwise
|
||||||
|
noted.
|
||||||
|
|
||||||
|
Please see `LICENSE <LICENSE>`_ for details.
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
============
|
||||||
|
|
||||||
|
Contributions are very welcome. Please read `How To Contribute`_ for details.
|
||||||
|
|
||||||
|
.. _How To Contribute: https://openedx.org/r/how-to-contribute
|
||||||
|
|
||||||
|
This project is currently accepting all types of contributions, bug fixes,
|
||||||
|
security fixes, maintenance work, or new features. However, please make sure
|
||||||
|
to have a discussion about your new feature idea with the maintainers prior to
|
||||||
|
beginning development to maximize the chances of your change being accepted.
|
||||||
|
You can start a conversation by creating a new issue on this repo summarizing
|
||||||
|
your idea.
|
||||||
|
|
||||||
|
The Open edX Code of Conduct
|
||||||
|
============================
|
||||||
|
|
||||||
|
All community members are expected to follow the `Open edX Code of Conduct`_.
|
||||||
|
|
||||||
|
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
|
||||||
|
|
||||||
|
People
|
||||||
|
======
|
||||||
|
|
||||||
|
The assigned maintainers for this component and other project details may be
|
||||||
|
found in `Backstage`_. Backstage pulls this data from the ``catalog-info.yaml``
|
||||||
|
file in this repo.
|
||||||
|
|
||||||
|
.. _Backstage: https://backstage.herokuapp.com/catalog/default/component/frontend-app-profile
|
||||||
|
|
||||||
|
Reporting Security Issues
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Please do not report security issues in public. Email security@openedx.org instead.
|
||||||
|
|||||||
25
catalog-info.yaml
Normal file
25
catalog-info.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# This file records information about this repo. Its use is described in OEP-55:
|
||||||
|
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
|
||||||
|
apiVersion: backstage.io/v1alpha1
|
||||||
|
kind: Component
|
||||||
|
metadata:
|
||||||
|
name: 'frontend-app-profile'
|
||||||
|
description: 'This is a micro-frontend application responsible for displaying and updating the user profiles.'
|
||||||
|
links:
|
||||||
|
- url: 'https://github.com/openedx/frontend-app-profile/blob/master/README.rst'
|
||||||
|
title: 'Documentation'
|
||||||
|
icon: 'Article'
|
||||||
|
annotations:
|
||||||
|
# (Optional) Annotation keys and values can be whatever you want.
|
||||||
|
# We use it in Open edX repos to have a comma-separated list of GitHub user
|
||||||
|
# names that might be interested in changes to the architecture of this
|
||||||
|
# component.
|
||||||
|
openedx.org/arch-interest-groups: ""
|
||||||
|
# This can be multiple comma-separated projects.
|
||||||
|
openedx.org/add-to-projects: "openedx:23"
|
||||||
|
openedx.org/release: "master"
|
||||||
|
spec:
|
||||||
|
owner: group:2u-infinity
|
||||||
|
type: 'service'
|
||||||
|
lifecycle: 'production'
|
||||||
|
# (Optional) An array of different components or resources.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
const { createConfig } = require('@edx/frontend-build');
|
const { createConfig } = require('@openedx/frontend-build');
|
||||||
|
|
||||||
module.exports = createConfig('jest', {
|
module.exports = createConfig('jest', {
|
||||||
setupFiles: [
|
setupFilesAfterEnv: [
|
||||||
'<rootDir>/src/setupTest.js',
|
'<rootDir>/src/setupTest.js',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
# This file describes this Open edX repo, as described in OEP-2:
|
|
||||||
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0002-bp-repo-metadata.html#specification
|
|
||||||
|
|
||||||
nick: prof
|
|
||||||
oeps: {}
|
|
||||||
openedx-release: {ref: master}
|
|
||||||
44864
package-lock.json
generated
44864
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
89
package.json
89
package.json
@@ -6,71 +6,74 @@
|
|||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/edx/frontend-app-profile.git"
|
"url": "git+https://github.com/openedx/frontend-app-profile.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "fedx-scripts webpack",
|
"build": "fedx-scripts webpack",
|
||||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
"i18n_extract": "fedx-scripts formatjs extract",
|
||||||
"is-es5": "es-check es5 ./dist/*.js",
|
|
||||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||||
"start": "fedx-scripts webpack-dev-server --progress",
|
"start": "fedx-scripts webpack-dev-server --progress",
|
||||||
"test": "fedx-scripts jest --coverage --passWithNoTests"
|
"dev": "PUBLIC_PATH=/profile/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||||
|
"test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests",
|
||||||
|
"stubs": "pact-stub-service ./src/pacts/frontend-app-profile-edx-platform.json --port 18000"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/edx/frontend-app-profile/issues"
|
"url": "https://github.com/openedx/frontend-app-profile/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/edx/frontend-app-profile#readme",
|
"homepage": "https://github.com/openedx/frontend-app-profile#readme",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 2 versions",
|
"extends @edx/browserslist-config"
|
||||||
"ie 11"
|
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||||
"@edx/frontend-component-footer": "10.1.6",
|
"@edx/frontend-component-footer": "^14.6.0",
|
||||||
"@edx/frontend-component-header": "2.4.3",
|
"@edx/frontend-component-header": "^6.2.0",
|
||||||
"@edx/frontend-platform": "1.12.7",
|
"@edx/frontend-platform": "^8.3.1",
|
||||||
"@edx/paragon": "16.6.1",
|
"@edx/openedx-atlas": "^0.7.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
"@fortawesome/free-brands-svg-icons": "6.7.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||||
"@fortawesome/react-fontawesome": "0.1.15",
|
"@fortawesome/react-fontawesome": "0.2.2",
|
||||||
"classnames": "2.3.1",
|
"@openedx/paragon": "^23.4.5",
|
||||||
"core-js": "3.18.2",
|
"@pact-foundation/pact": "^11.0.2",
|
||||||
|
"@redux-devtools/extension": "3.3.0",
|
||||||
|
"classnames": "2.5.1",
|
||||||
|
"core-js": "3.43.0",
|
||||||
|
"history": "5.3.0",
|
||||||
"lodash.camelcase": "4.3.0",
|
"lodash.camelcase": "4.3.0",
|
||||||
"lodash.get": "4.4.2",
|
"lodash.get": "4.4.2",
|
||||||
"lodash.pick": "4.4.0",
|
"lodash.pick": "4.4.0",
|
||||||
"lodash.snakecase": "4.1.1",
|
"lodash.snakecase": "4.1.1",
|
||||||
"prop-types": "15.7.2",
|
"prop-types": "15.8.1",
|
||||||
"react": "16.14.0",
|
"react": "18.3.1",
|
||||||
"react-dom": "16.14.0",
|
"react-dom": "18.3.1",
|
||||||
"react-redux": "7.2.5",
|
"react-helmet": "6.1.0",
|
||||||
"react-router": "5.2.1",
|
"react-redux": "7.2.9",
|
||||||
"react-router-dom": "5.3.0",
|
"react-router": "6.30.1",
|
||||||
"redux": "4.1.1",
|
"react-router-dom": "6.30.1",
|
||||||
"redux-devtools-extension": "2.13.9",
|
"redux": "4.2.1",
|
||||||
"redux-logger": "3.0.6",
|
"redux-logger": "3.0.6",
|
||||||
"redux-saga": "1.1.3",
|
"redux-saga": "1.3.0",
|
||||||
"redux-thunk": "2.3.0",
|
"redux-thunk": "2.4.2",
|
||||||
"regenerator-runtime": "0.13.9",
|
"regenerator-runtime": "0.14.1",
|
||||||
"reselect": "4.0.0",
|
"reselect": "5.1.1",
|
||||||
"universal-cookie": "3.1.0"
|
"universal-cookie": "4.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "13.2.1",
|
"@commitlint/cli": "19.8.1",
|
||||||
"@commitlint/config-angular": "13.2.0",
|
"@commitlint/config-angular": "19.8.1",
|
||||||
"@edx/frontend-build": "9.0.5",
|
"@edx/browserslist-config": "^1.1.1",
|
||||||
"codecov": "3.8.3",
|
"@edx/reactifex": "2.2.0",
|
||||||
"enzyme": "3.11.0",
|
"@openedx/frontend-build": "^14.3.3",
|
||||||
"enzyme-adapter-react-16": "1.15.6",
|
"@testing-library/jest-dom": "6.6.3",
|
||||||
"es-check": "5.2.4",
|
"@testing-library/react": "14.3.1",
|
||||||
"glob": "7.2.0",
|
"glob": "11.0.3",
|
||||||
"react-test-renderer": "16.14.0",
|
|
||||||
"reactifex": "1.1.1",
|
"reactifex": "1.1.1",
|
||||||
"redux-mock-store": "1.5.4"
|
"redux-mock-store": "1.5.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,11 @@
|
|||||||
"pin"
|
"pin"
|
||||||
],
|
],
|
||||||
"automerge": true
|
"automerge": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchPackagePatterns": ["@edx", "@openedx"],
|
||||||
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
|
"automerge": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"timezone": "America/New_York"
|
"timezone": "America/New_York"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { applyMiddleware, createStore, compose } from 'redux';
|
import { applyMiddleware, createStore, compose } from 'redux';
|
||||||
import thunkMiddleware from 'redux-thunk';
|
import thunkMiddleware from 'redux-thunk';
|
||||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
import { composeWithDevTools } from '@redux-devtools/extension';
|
||||||
import { createLogger } from 'redux-logger';
|
import { createLogger } from 'redux-logger';
|
||||||
import createSagaMiddleware from 'redux-saga';
|
import createSagaMiddleware from 'redux-saga';
|
||||||
|
|
||||||
|
|||||||
21
src/head/Head.jsx
Normal file
21
src/head/Head.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const Head = ({ intl }) => (
|
||||||
|
<Helmet>
|
||||||
|
<title>
|
||||||
|
{intl.formatMessage(messages['profile.page.title'], { siteName: getConfig().SITE_NAME })}
|
||||||
|
</title>
|
||||||
|
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||||
|
</Helmet>
|
||||||
|
);
|
||||||
|
|
||||||
|
Head.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(Head);
|
||||||
17
src/head/Head.test.jsx
Normal file
17
src/head/Head.test.jsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import Head from './Head';
|
||||||
|
|
||||||
|
describe('Head', () => {
|
||||||
|
const props = {};
|
||||||
|
it('should match render title tag and favicon with the site configuration values', () => {
|
||||||
|
render(<IntlProvider locale="en"><Head {...props} /></IntlProvider>);
|
||||||
|
const helmet = Helmet.peek();
|
||||||
|
expect(helmet.title).toEqual(`Profile | ${getConfig().SITE_NAME}`);
|
||||||
|
expect(helmet.linkTags[0].rel).toEqual('shortcut icon');
|
||||||
|
expect(helmet.linkTags[0].href).toEqual(getConfig().FAVICON_URL);
|
||||||
|
});
|
||||||
|
});
|
||||||
11
src/head/messages.js
Normal file
11
src/head/messages.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
'profile.page.title': {
|
||||||
|
id: 'profile.page.title',
|
||||||
|
defaultMessage: 'Profile | {siteName}',
|
||||||
|
description: 'Title tag',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
1
src/i18n/index.js
Normal file
1
src/i18n/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export default [];
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import arMessages from './messages/ar.json';
|
|
||||||
import caMessages from './messages/ca.json';
|
|
||||||
// no need to import en messages-- they are in the defaultMessage field
|
|
||||||
import es419Messages from './messages/es_419.json';
|
|
||||||
import frMessages from './messages/fr.json';
|
|
||||||
import zhcnMessages from './messages/zh_CN.json';
|
|
||||||
import heMessages from './messages/he.json';
|
|
||||||
import idMessages from './messages/id.json';
|
|
||||||
import kokrMessages from './messages/ko_kr.json';
|
|
||||||
import plMessages from './messages/pl.json';
|
|
||||||
import ptbrMessages from './messages/pt_br.json';
|
|
||||||
import ruMessages from './messages/ru.json';
|
|
||||||
import thMessages from './messages/th.json';
|
|
||||||
import ukMessages from './messages/uk.json';
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
ar: arMessages,
|
|
||||||
'es-419': es419Messages,
|
|
||||||
fr: frMessages,
|
|
||||||
'zh-cn': zhcnMessages,
|
|
||||||
ca: caMessages,
|
|
||||||
he: heMessages,
|
|
||||||
id: idMessages,
|
|
||||||
'ko-kr': kokrMessages,
|
|
||||||
pl: plMessages,
|
|
||||||
'pt-br': ptbrMessages,
|
|
||||||
ru: ruMessages,
|
|
||||||
th: thMessages,
|
|
||||||
uk: ukMessages,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default messages;
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"profile.age.headline": "لا يمكن مشاركة ملفك الشخصي",
|
|
||||||
"profile.age.details": "لمشاركة ملفك الشخصي مع متعلمي edX الآخرين يجب التحقق من أن يكون عمرك أكثر من ١٣ سنة",
|
|
||||||
"profile.age.set.date": "اضبط تاريخ ميلاك",
|
|
||||||
"profile.datejoined.member.since": "عضو منذُ {year}",
|
|
||||||
"profile.bio.empty": "أضف نبذة قصيرة",
|
|
||||||
"profile.bio.about.me": "نبذة عنّي",
|
|
||||||
"profile.certificate.organization.label": "من",
|
|
||||||
"profile.certificate.completion.date.label": "مكتمل في",
|
|
||||||
"profile.no.certificates": "لم تحصل على أية شهادات حتى الآن.",
|
|
||||||
"profile.certificates.my.certificates": "شهاداتي",
|
|
||||||
"profile.certificates.view.certificate": "معاينة الشهادة",
|
|
||||||
"profile.certificates.types.verified": "شهادة موثقة",
|
|
||||||
"profile.certificates.types.professional": "شهادة مهنية",
|
|
||||||
"profile.certificates.types.unknown": "شهادة",
|
|
||||||
"profile.country.label": "الموقع",
|
|
||||||
"profile.country.empty": "أضف موقعًا",
|
|
||||||
"profile.education.empty": "أضف مؤهلًا تعليميًا",
|
|
||||||
"profile.education.education": "المستوى التعليمي",
|
|
||||||
"profile.education.levels.p": "شهادة دكتوراه",
|
|
||||||
"profile.education.levels.m": "ماجستير أو شهادة مهنيّة",
|
|
||||||
"profile.education.levels.b": "درجة البكالوريوس",
|
|
||||||
"profile.education.levels.a": "درجة الزمالة",
|
|
||||||
"profile.education.levels.hs": "شهادة الثانوية العامة",
|
|
||||||
"profile.education.levels.jhs": "شهادة الثانوية الصغرى/الإعدادية/المرحلة المتوسّطة",
|
|
||||||
"profile.education.levels.el": "شهادة المدرسة الابتدائية",
|
|
||||||
"profile.education.levels.none": "لا يوجد تعليم رسمي",
|
|
||||||
"profile.education.levels.o": "نوع آخر من التعليم",
|
|
||||||
"profile.editbutton.edit": "تحرير",
|
|
||||||
"profile.formcontrols.who.can.see": "من يمكنه مشاهدة هذا:",
|
|
||||||
"profile.formcontrols.button.cancel": "إلغاء",
|
|
||||||
"profile.formcontrols.button.save": "حفظ",
|
|
||||||
"profile.formcontrols.button.saving": "جاري الحفظ",
|
|
||||||
"profile.formcontrols.button.saved": "تم الحفظ",
|
|
||||||
"profile.visibility.who.just.me": "أنا فقط",
|
|
||||||
"profile.visibility.who.everyone": "جميع أعضاء edX",
|
|
||||||
"profile.name.full.name": "الاسم الكامل",
|
|
||||||
"profile.name.details": "هذا هو الاسم الذي سيظهر في حسابك وفي شهاداتك",
|
|
||||||
"profile.name.empty": "إضافة اسم",
|
|
||||||
"profile.preferredlanguage.empty": "إضافة اللغة",
|
|
||||||
"profile.preferredlanguage.label": "لغة التحدث الأم",
|
|
||||||
"profile.profileavatar.upload-button": "تحميل صورة",
|
|
||||||
"profile.profileavatar.remove.button": "حذف",
|
|
||||||
"profile.image.alt.attribute": "صورة عرض الملف الشخصي",
|
|
||||||
"profile.profileavatar.change-button": "تغيير",
|
|
||||||
"profile.sociallinks.add": "إضافة {network}",
|
|
||||||
"profile.sociallinks.social.links": "روابط قنوات التواصل الاجتماعي",
|
|
||||||
"profile.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في نص الرابط. الرجاء التحقق من الرابط والمحاولة مجددا.",
|
|
||||||
"profile.viewMyRecords": "عرض سجلّاتي",
|
|
||||||
"profile.loading": "جاري تحميل الملف الشخصي ..."
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"profile.age.headline": "Tu perfil no puede ser compartido.",
|
|
||||||
"profile.age.details": "Para compartir tu perfil con otros estudiantes de edX, debes confirmar que tienes más de 13 años.",
|
|
||||||
"profile.age.set.date": "Establece tu fecha de nacimiento",
|
|
||||||
"profile.datejoined.member.since": "Miembro desde {year}",
|
|
||||||
"profile.bio.empty": "Añade una breve biografía",
|
|
||||||
"profile.bio.about.me": "Sobre Mí",
|
|
||||||
"profile.certificate.organization.label": "Desde",
|
|
||||||
"profile.certificate.completion.date.label": "Completado el {date}",
|
|
||||||
"profile.no.certificates": "Todavía no ha obtenido ningún certificado.",
|
|
||||||
"profile.certificates.my.certificates": "Mis Certificados",
|
|
||||||
"profile.certificates.view.certificate": "Ver Certificado",
|
|
||||||
"profile.certificates.types.verified": "Certificado verificado",
|
|
||||||
"profile.certificates.types.professional": "Certificado profesional",
|
|
||||||
"profile.certificates.types.unknown": "Certificado",
|
|
||||||
"profile.country.label": "Ubicación",
|
|
||||||
"profile.country.empty": "Añade ubicación",
|
|
||||||
"profile.education.empty": "Añade Educación",
|
|
||||||
"profile.education.education": "Educación",
|
|
||||||
"profile.education.levels.p": "Doctorado",
|
|
||||||
"profile.education.levels.m": "Master o magíster",
|
|
||||||
"profile.education.levels.b": "Pregrado o Licenciatura",
|
|
||||||
"profile.education.levels.a": "Grado técnico - tecnológico",
|
|
||||||
"profile.education.levels.hs": "Enseñanza secundaria",
|
|
||||||
"profile.education.levels.jhs": "Formación media",
|
|
||||||
"profile.education.levels.el": "Enseñanza primaria",
|
|
||||||
"profile.education.levels.none": "Ninguna educación formal",
|
|
||||||
"profile.education.levels.o": "Otra educación",
|
|
||||||
"profile.editbutton.edit": "Editar",
|
|
||||||
"profile.formcontrols.who.can.see": "Quién puede ver esto:",
|
|
||||||
"profile.formcontrols.button.cancel": "Cancelar",
|
|
||||||
"profile.formcontrols.button.save": "Guardar",
|
|
||||||
"profile.formcontrols.button.saving": "Guardando",
|
|
||||||
"profile.formcontrols.button.saved": "Guardado",
|
|
||||||
"profile.visibility.who.just.me": "Solo yo",
|
|
||||||
"profile.visibility.who.everyone": "Todos en edX",
|
|
||||||
"profile.name.full.name": "Nombre completo",
|
|
||||||
"profile.name.details": "Este es el nombre que aparecerá en tu cuenta y en tus certificados.",
|
|
||||||
"profile.name.empty": "Añade nombre",
|
|
||||||
"profile.preferredlanguage.empty": "Añadir idioma",
|
|
||||||
"profile.preferredlanguage.label": "Idioma principal que hablas",
|
|
||||||
"profile.profileavatar.upload-button": "Subir foto",
|
|
||||||
"profile.profileavatar.remove.button": "Eliminar",
|
|
||||||
"profile.image.alt.attribute": "avatar del perfil",
|
|
||||||
"profile.profileavatar.change-button": "Cambiar",
|
|
||||||
"profile.sociallinks.add": "Añade {network}",
|
|
||||||
"profile.sociallinks.social.links": "Enlaces De Redes Sociales",
|
|
||||||
"profile.notfound.message": "La página que estas buscando no está disponible o hay un error en la URL. Por favor, comprueba la URL y vuelve a intentarlo.",
|
|
||||||
"profile.viewMyRecords": "Ver mis registros",
|
|
||||||
"profile.loading": "Cargando perfil..."
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"profile.age.headline": "Votre profil ne peut pas être partagé.",
|
|
||||||
"profile.age.details": "Pour partager votre profil avec d'autres apprenants edX, vous devez confirmer que vous avez plus de 13 ans.",
|
|
||||||
"profile.age.set.date": "Définissez votre date de naissance",
|
|
||||||
"profile.datejoined.member.since": "Membre depuis {year}",
|
|
||||||
"profile.bio.empty": "Ajouter une courte biographie",
|
|
||||||
"profile.bio.about.me": "À propos de moi",
|
|
||||||
"profile.certificate.organization.label": "De",
|
|
||||||
"profile.certificate.completion.date.label": "Terminé le {date}",
|
|
||||||
"profile.no.certificates": "Vous n'avez pas encore de certificats.",
|
|
||||||
"profile.certificates.my.certificates": "Mes certificats",
|
|
||||||
"profile.certificates.view.certificate": "Voir le certificat",
|
|
||||||
"profile.certificates.types.verified": "Certificat vérifié",
|
|
||||||
"profile.certificates.types.professional": "Certificat professionnel",
|
|
||||||
"profile.certificates.types.unknown": "Certificat",
|
|
||||||
"profile.country.label": "Localisation",
|
|
||||||
"profile.country.empty": "Ajouter localisation",
|
|
||||||
"profile.education.empty": "Ajouter une éducation",
|
|
||||||
"profile.education.education": "Education",
|
|
||||||
"profile.education.levels.p": "Doctorat",
|
|
||||||
"profile.education.levels.m": "Master ou diplôme professionnel",
|
|
||||||
"profile.education.levels.b": "Diplôme de licence",
|
|
||||||
"profile.education.levels.a": "Grade de l'associé",
|
|
||||||
"profile.education.levels.hs": "Lycée / enseignement secondaire",
|
|
||||||
"profile.education.levels.jhs": "Collège / enseignement secondaire inférieur",
|
|
||||||
"profile.education.levels.el": "Enseignement primaire",
|
|
||||||
"profile.education.levels.none": "Sans diplôme",
|
|
||||||
"profile.education.levels.o": "Autre niveau d'étude",
|
|
||||||
"profile.editbutton.edit": "Modifier",
|
|
||||||
"profile.formcontrols.who.can.see": "Qui peut voir ça :",
|
|
||||||
"profile.formcontrols.button.cancel": "Annuler",
|
|
||||||
"profile.formcontrols.button.save": "Enregistrer",
|
|
||||||
"profile.formcontrols.button.saving": "Enregistrement",
|
|
||||||
"profile.formcontrols.button.saved": "Enregistré",
|
|
||||||
"profile.visibility.who.just.me": "Juste moi",
|
|
||||||
"profile.visibility.who.everyone": "Tout le monde sur edX",
|
|
||||||
"profile.name.full.name": "Nom complet",
|
|
||||||
"profile.name.details": "C'est le nom qui apparaît dans votre compte et sur vos certificats.",
|
|
||||||
"profile.name.empty": "Ajouter un nom",
|
|
||||||
"profile.preferredlanguage.empty": "Ajouter une langue",
|
|
||||||
"profile.preferredlanguage.label": "Langue principale parlée",
|
|
||||||
"profile.profileavatar.upload-button": "Envoyer la photo",
|
|
||||||
"profile.profileavatar.remove.button": "Supprimer",
|
|
||||||
"profile.image.alt.attribute": "Profil avatar",
|
|
||||||
"profile.profileavatar.change-button": "Modifier",
|
|
||||||
"profile.sociallinks.add": "Ajouter {network}",
|
|
||||||
"profile.sociallinks.social.links": "Liens vers les réseaux sociaux",
|
|
||||||
"profile.notfound.message": "La page que vous recherchez n'est pas disponible ou il y a une erreur dans l'URL. Veuillez vérifier l'URL et réessayer.",
|
|
||||||
"profile.viewMyRecords": "Voir mes succès",
|
|
||||||
"profile.loading": "Chargement du profil...."
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"profile.age.headline": "Your profile cannot be shared.",
|
|
||||||
"profile.age.details": "To share your profile with other edX learners, you must confirm that you are over the age of 13.",
|
|
||||||
"profile.age.set.date": "Set your date of birth",
|
|
||||||
"profile.datejoined.member.since": "Member since {year}",
|
|
||||||
"profile.bio.empty": "Add a short bio",
|
|
||||||
"profile.bio.about.me": "About Me",
|
|
||||||
"profile.certificate.organization.label": "From",
|
|
||||||
"profile.certificate.completion.date.label": "Completed on {date}",
|
|
||||||
"profile.no.certificates": "You don't have any certificates yet.",
|
|
||||||
"profile.certificates.my.certificates": "My Certificates",
|
|
||||||
"profile.certificates.view.certificate": "View Certificate",
|
|
||||||
"profile.certificates.types.verified": "Verified Certificate",
|
|
||||||
"profile.certificates.types.professional": "Professional Certificate",
|
|
||||||
"profile.certificates.types.unknown": "Certificate",
|
|
||||||
"profile.country.label": "Location",
|
|
||||||
"profile.country.empty": "Add location",
|
|
||||||
"profile.education.empty": "Add education",
|
|
||||||
"profile.education.education": "Education",
|
|
||||||
"profile.education.levels.p": "Doctorate",
|
|
||||||
"profile.education.levels.m": "Master's or professional degree",
|
|
||||||
"profile.education.levels.b": "Bachelor's Degree",
|
|
||||||
"profile.education.levels.a": "Associate's degree",
|
|
||||||
"profile.education.levels.hs": "Secondary/high school",
|
|
||||||
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
|
|
||||||
"profile.education.levels.el": "Elementary/primary school",
|
|
||||||
"profile.education.levels.none": "No formal education",
|
|
||||||
"profile.education.levels.o": "Other education",
|
|
||||||
"profile.editbutton.edit": "Edit",
|
|
||||||
"profile.formcontrols.who.can.see": "Who can see this:",
|
|
||||||
"profile.formcontrols.button.cancel": "Cancel",
|
|
||||||
"profile.formcontrols.button.save": "Save",
|
|
||||||
"profile.formcontrols.button.saving": "Saving",
|
|
||||||
"profile.formcontrols.button.saved": "Saved",
|
|
||||||
"profile.visibility.who.just.me": "Just me",
|
|
||||||
"profile.visibility.who.everyone": "Everyone on edX",
|
|
||||||
"profile.name.full.name": "Full Name",
|
|
||||||
"profile.name.details": "This is the name that appears in your account and on your certificates.",
|
|
||||||
"profile.name.empty": "Add name",
|
|
||||||
"profile.preferredlanguage.empty": "Add language",
|
|
||||||
"profile.preferredlanguage.label": "Primary Language Spoken",
|
|
||||||
"profile.profileavatar.upload-button": "Upload Photo",
|
|
||||||
"profile.profileavatar.remove.button": "Remove",
|
|
||||||
"profile.image.alt.attribute": "profile avatar",
|
|
||||||
"profile.profileavatar.change-button": "Change",
|
|
||||||
"profile.sociallinks.add": "Add {network}",
|
|
||||||
"profile.sociallinks.social.links": "Social Links",
|
|
||||||
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
|
||||||
"profile.viewMyRecords": "View My Records",
|
|
||||||
"profile.loading": "Profile loading..."
|
|
||||||
}
|
|
||||||
@@ -13,53 +13,49 @@ import {
|
|||||||
ErrorPage,
|
ErrorPage,
|
||||||
} from '@edx/frontend-platform/react';
|
} from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
import React from 'react';
|
import React, { StrictMode } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { Route, Switch } from 'react-router-dom';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import Header, { messages as headerMessages } from '@edx/frontend-component-header';
|
import Header from '@edx/frontend-component-header';
|
||||||
import Footer, { messages as footerMessages } from '@edx/frontend-component-footer';
|
import { FooterSlot } from '@edx/frontend-component-footer';
|
||||||
|
|
||||||
import appMessages from './i18n';
|
import messages from './i18n';
|
||||||
import { ProfilePage, NotFoundPage } from './profile';
|
|
||||||
import configureStore from './data/configureStore';
|
import configureStore from './data/configureStore';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
import Head from './head/Head';
|
||||||
|
|
||||||
|
import AppRoutes from './routes/AppRoutes';
|
||||||
|
|
||||||
|
const rootNode = createRoot(document.getElementById('root'));
|
||||||
subscribe(APP_READY, () => {
|
subscribe(APP_READY, () => {
|
||||||
ReactDOM.render(
|
rootNode.render(
|
||||||
<AppProvider store={configureStore()}>
|
<StrictMode>
|
||||||
<Header />
|
<AppProvider store={configureStore()}>
|
||||||
<main>
|
<Head />
|
||||||
<Switch>
|
<Header />
|
||||||
<Route path="/u/:username" component={ProfilePage} />
|
<main id="main">
|
||||||
<Route path="/notfound" component={NotFoundPage} />
|
<AppRoutes />
|
||||||
<Route path="*" component={NotFoundPage} />
|
</main>
|
||||||
</Switch>
|
<FooterSlot />
|
||||||
</main>
|
</AppProvider>
|
||||||
<Footer />
|
</StrictMode>,
|
||||||
</AppProvider>,
|
|
||||||
document.getElementById('root'),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
subscribe(APP_INIT_ERROR, (error) => {
|
subscribe(APP_INIT_ERROR, (error) => {
|
||||||
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
|
rootNode.render(<ErrorPage message={error.message} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
initialize({
|
initialize({
|
||||||
messages: [
|
messages,
|
||||||
appMessages,
|
|
||||||
headerMessages,
|
|
||||||
footerMessages,
|
|
||||||
],
|
|
||||||
requireAuthenticatedUser: true,
|
|
||||||
hydrateAuthenticatedUser: true,
|
hydrateAuthenticatedUser: true,
|
||||||
handlers: {
|
handlers: {
|
||||||
config: () => {
|
config: () => {
|
||||||
mergeConfig({
|
mergeConfig({
|
||||||
ENABLE_LEARNER_RECORD_MFE: (process.env.ENABLE_LEARNER_RECORD_MFE || false),
|
COLLECT_YEAR_OF_BIRTH: process.env.COLLECT_YEAR_OF_BIRTH,
|
||||||
LEARNER_RECORD_MFE_BASE_URL: process.env.LEARNER_RECORD_MFE_BASE_URL,
|
ENABLE_SKILLS_BUILDER_PROFILE: process.env.ENABLE_SKILLS_BUILDER_PROFILE,
|
||||||
}, 'App loadConfig override handler');
|
}, 'App loadConfig override handler');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
@import "~@edx/brand/paragon/fonts";
|
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
|
||||||
@import "~@edx/brand/paragon/variables";
|
|
||||||
@import "~@edx/paragon/scss/core/core";
|
|
||||||
@import "~@edx/brand/paragon/overrides";
|
|
||||||
@import "~@edx/frontend-component-header/dist/index";
|
@import "~@edx/frontend-component-header/dist/index";
|
||||||
@import "~@edx/frontend-component-footer/dist/footer";
|
@import "~@edx/frontend-component-footer/dist/footer";
|
||||||
|
|
||||||
|
|||||||
77
src/pacts/frontend-app-profile-edx-platform.json
Normal file
77
src/pacts/frontend-app-profile-edx-platform.json
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"consumer": {
|
||||||
|
"name": "frontend-app-profile"
|
||||||
|
},
|
||||||
|
"interactions": [
|
||||||
|
{
|
||||||
|
"description": "A request for user's basic information",
|
||||||
|
"providerStates": [
|
||||||
|
{
|
||||||
|
"name": "Account and user's information does not exist"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/user/v1/accounts/staff_not_found"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 404
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "A request for user's basic information",
|
||||||
|
"providerStates": [
|
||||||
|
{
|
||||||
|
"name": "I have a user's basic information"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/user/v1/accounts/staff"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"body": {
|
||||||
|
"bio": "This is my bio",
|
||||||
|
"country": "ME",
|
||||||
|
"dateJoined": "2017-06-07T00:44:23Z",
|
||||||
|
"email": "staff@example.com",
|
||||||
|
"isActive": true,
|
||||||
|
"name": "Lemon Seltzer",
|
||||||
|
"username": "staff",
|
||||||
|
"yearOfBirth": 1901
|
||||||
|
},
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"matchingRules": {
|
||||||
|
"body": {
|
||||||
|
"$": {
|
||||||
|
"combine": "AND",
|
||||||
|
"matchers": [
|
||||||
|
{
|
||||||
|
"match": "type"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"pact-js": {
|
||||||
|
"version": "11.0.2"
|
||||||
|
},
|
||||||
|
"pactRust": {
|
||||||
|
"ffi": "0.4.0",
|
||||||
|
"models": "1.0.4"
|
||||||
|
},
|
||||||
|
"pactSpecification": {
|
||||||
|
"version": "3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"name": "edx-platform"
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/plugin-slots/FooterSlot/README.md
Normal file
53
src/plugin-slots/FooterSlot/README.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Footer Slot
|
||||||
|
|
||||||
|
### Slot ID: `org.openedx.frontend.layout.footer.v1`
|
||||||
|
|
||||||
|
### Slot ID Aliases
|
||||||
|
* `footer_slot`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This slot is used to replace/modify/hide the footer.
|
||||||
|
|
||||||
|
The implementation of the `FooterSlot` component lives in [the `frontend-component-footer` repository](https://github.com/openedx/frontend-component-footer/).
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
The following `env.config.jsx` will replace the default footer.
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
pluginSlots: {
|
||||||
|
'org.openedx.frontend.layout.footer.v1': {
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
// Hide the default footer
|
||||||
|
op: PLUGIN_OPERATIONS.Hide,
|
||||||
|
widgetId: 'default_contents',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Insert a custom footer
|
||||||
|
op: PLUGIN_OPERATIONS.Insert,
|
||||||
|
widget: {
|
||||||
|
id: 'custom_footer',
|
||||||
|
type: DIRECT_PLUGIN,
|
||||||
|
RenderWidget: () => (
|
||||||
|
<h1 style={{textAlign: 'center'}}>🦶</h1>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
BIN
src/plugin-slots/FooterSlot/images/custom_footer.png
Normal file
BIN
src/plugin-slots/FooterSlot/images/custom_footer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
src/plugin-slots/FooterSlot/images/default_footer.png
Normal file
BIN
src/plugin-slots/FooterSlot/images/default_footer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
3
src/plugin-slots/README.md
Normal file
3
src/plugin-slots/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# `frontend-app-profile` Plugin Slots
|
||||||
|
|
||||||
|
* [`org.openedx.frontend.layout.footer.v1`](./FooterSlot/)
|
||||||
@@ -1,40 +1,40 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { StatusAlert } from '@edx/paragon';
|
import { Alert } from '@openedx/paragon';
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
|
||||||
function AgeMessage({ accountSettingsUrl }) {
|
const AgeMessage = ({ accountSettingsUrl }) => (
|
||||||
return (
|
<Alert
|
||||||
<StatusAlert
|
variant="info"
|
||||||
alertType="info"
|
dismissible={false}
|
||||||
dialog={(
|
show
|
||||||
<>
|
>
|
||||||
<FormattedMessage
|
<Alert.Heading id="profile.age.headline">
|
||||||
id="profile.age.headline"
|
<FormattedMessage
|
||||||
defaultMessage="Your profile cannot be shared."
|
id="profile.age.cannotShare"
|
||||||
description="error message"
|
defaultMessage="Your profile cannot be shared."
|
||||||
tagName="h6"
|
description="Error message indicating that the user's profile cannot be shared"
|
||||||
/>
|
/>
|
||||||
<FormattedMessage
|
</Alert.Heading>
|
||||||
id="profile.age.details"
|
<FormattedMessage
|
||||||
defaultMessage="To share your profile with other edX learners, you must confirm that you are over the age of 13."
|
id="profile.age.details"
|
||||||
description="error message"
|
defaultMessage="To share your profile with other {siteName} learners, you must confirm that you are over the age of 13."
|
||||||
tagName="p"
|
description="Error message"
|
||||||
/>
|
tagName="p"
|
||||||
<a href={accountSettingsUrl}>
|
values={{
|
||||||
<FormattedMessage
|
siteName: getConfig().SITE_NAME,
|
||||||
id="profile.age.set.date"
|
}}
|
||||||
defaultMessage="Set your date of birth"
|
|
||||||
description="label on a link to set birthday"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
dismissible={false}
|
|
||||||
open
|
|
||||||
/>
|
/>
|
||||||
);
|
<Alert.Link href={accountSettingsUrl}>
|
||||||
}
|
<FormattedMessage
|
||||||
|
id="profile.age.set.date"
|
||||||
|
defaultMessage="Set your date of birth"
|
||||||
|
description="Label on a link to set birthday"
|
||||||
|
/>
|
||||||
|
</Alert.Link>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
|
||||||
AgeMessage.propTypes = {
|
AgeMessage.propTypes = {
|
||||||
accountSettingsUrl: PropTypes.string.isRequired,
|
accountSettingsUrl: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
function Banner() {
|
const Banner = () => <div className="profile-page-bg-banner bg-primary d-md-block p-relative" />;
|
||||||
return <div className="profile-page-bg-banner bg-primary d-none d-md-block p-relative" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Banner;
|
export default Banner;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
function DateJoined({ date }) {
|
const DateJoined = ({ date }) => {
|
||||||
if (date == null) {
|
if (date == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ function DateJoined({ date }) {
|
|||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
DateJoined.propTypes = {
|
DateJoined.propTypes = {
|
||||||
date: PropTypes.string,
|
date: PropTypes.string,
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
export default function NotFoundPage() {
|
const NotFoundPage = () => (
|
||||||
return (
|
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
<p className="my-0 py-5 text-muted" style={{ maxWidth: '32em' }}>
|
||||||
<p className="my-0 py-5 text-muted" style={{ maxWidth: '32em' }}>
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id="profile.notfound.message"
|
||||||
id="profile.notfound.message"
|
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
||||||
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
description="error message when a page does not exist"
|
||||||
description="error message when a page does not exist"
|
/>
|
||||||
/>
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
}
|
export default NotFoundPage;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
|||||||
import { ensureConfig, getConfig } from '@edx/frontend-platform';
|
import { ensureConfig, getConfig } from '@edx/frontend-platform';
|
||||||
import { AppContext } from '@edx/frontend-platform/react';
|
import { AppContext } from '@edx/frontend-platform/react';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { StatusAlert, Hyperlink } from '@edx/paragon';
|
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
import {
|
import {
|
||||||
@@ -30,8 +30,10 @@ import Bio from './forms/Bio';
|
|||||||
import Certificates from './forms/Certificates';
|
import Certificates from './forms/Certificates';
|
||||||
import AgeMessage from './AgeMessage';
|
import AgeMessage from './AgeMessage';
|
||||||
import DateJoined from './DateJoined';
|
import DateJoined from './DateJoined';
|
||||||
|
import UsernameDescription from './UsernameDescription';
|
||||||
import PageLoading from './PageLoading';
|
import PageLoading from './PageLoading';
|
||||||
import Banner from './Banner';
|
import Banner from './Banner';
|
||||||
|
import LearningGoal from './forms/LearningGoal';
|
||||||
|
|
||||||
// Selectors
|
// Selectors
|
||||||
import { profilePageSelector } from './data/selectors';
|
import { profilePageSelector } from './data/selectors';
|
||||||
@@ -39,17 +41,18 @@ import { profilePageSelector } from './data/selectors';
|
|||||||
// i18n
|
// i18n
|
||||||
import messages from './ProfilePage.messages';
|
import messages from './ProfilePage.messages';
|
||||||
|
|
||||||
|
import withParams from '../utils/hoc';
|
||||||
|
|
||||||
ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage');
|
ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage');
|
||||||
|
|
||||||
class ProfilePage extends React.Component {
|
class ProfilePage extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
const recordsUrl = this.getRecordsUrl(context);
|
const credentialsBaseUrl = context.config.CREDENTIALS_BASE_URL;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
viewMyRecordsUrl: recordsUrl,
|
viewMyRecordsUrl: credentialsBaseUrl ? `${credentialsBaseUrl}/records` : null,
|
||||||
accountSettingsUrl: `${context.config.LMS_BASE_URL}/account/settings`,
|
accountSettingsUrl: context.config.ACCOUNT_SETTINGS_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleSaveProfilePhoto = this.handleSaveProfilePhoto.bind(this);
|
this.handleSaveProfilePhoto = this.handleSaveProfilePhoto.bind(this);
|
||||||
@@ -61,29 +64,12 @@ class ProfilePage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchProfile(this.props.match.params.username);
|
this.props.fetchProfile(this.props.params.username);
|
||||||
sendTrackingLogEvent('edx.profile.viewed', {
|
sendTrackingLogEvent('edx.profile.viewed', {
|
||||||
username: this.props.match.params.username,
|
username: this.props.params.username,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getRecordsUrl(context) {
|
|
||||||
let recordsUrl = null;
|
|
||||||
|
|
||||||
if (getConfig().ENABLE_LEARNER_RECORD_MFE) {
|
|
||||||
recordsUrl = getConfig().LEARNER_RECORD_MFE_BASE_URL;
|
|
||||||
} else {
|
|
||||||
const credentialsBaseUrl = context.config.CREDENTIALS_BASE_URL;
|
|
||||||
recordsUrl = credentialsBaseUrl ? `${credentialsBaseUrl}/records` : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return recordsUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
isAuthenticatedUserProfile() {
|
|
||||||
return this.props.match.params.username === this.context.authenticatedUser.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSaveProfilePhoto(formData) {
|
handleSaveProfilePhoto(formData) {
|
||||||
this.props.saveProfilePhoto(this.context.authenticatedUser.username, formData);
|
this.props.saveProfilePhoto(this.context.authenticatedUser.username, formData);
|
||||||
}
|
}
|
||||||
@@ -108,6 +94,18 @@ class ProfilePage extends React.Component {
|
|||||||
this.props.updateDraft(name, value);
|
this.props.updateDraft(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isYOBDisabled() {
|
||||||
|
const { yearOfBirth } = this.props;
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const isAgeOrNotCompliant = !yearOfBirth || ((currentYear - yearOfBirth) < 13);
|
||||||
|
|
||||||
|
return isAgeOrNotCompliant && getConfig().COLLECT_YEAR_OF_BIRTH !== 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuthenticatedUserProfile() {
|
||||||
|
return this.props.params.username === this.context.authenticatedUser.username;
|
||||||
|
}
|
||||||
|
|
||||||
// Inserted into the DOM in two places (for responsive layout)
|
// Inserted into the DOM in two places (for responsive layout)
|
||||||
renderViewMyRecordsButton() {
|
renderViewMyRecordsButton() {
|
||||||
if (!(this.state.viewMyRecordsUrl && this.isAuthenticatedUserProfile())) {
|
if (!(this.state.viewMyRecordsUrl && this.isAuthenticatedUserProfile())) {
|
||||||
@@ -126,13 +124,12 @@ class ProfilePage extends React.Component {
|
|||||||
const { dateJoined } = this.props;
|
const { dateJoined } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<span data-hj-suppress>
|
||||||
<span data-hj-suppress>
|
<h1 className="h2 mb-0 font-weight-bold text-truncate">{this.props.params.username}</h1>
|
||||||
<h1 className="h2 mb-0 font-weight-bold">{this.props.match.params.username}</h1>
|
<DateJoined date={dateJoined} />
|
||||||
<DateJoined date={dateJoined} />
|
{this.isYOBDisabled() && <UsernameDescription />}
|
||||||
<hr className="d-none d-md-block" />
|
<hr className="d-none d-md-block" />
|
||||||
</span>
|
</span>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +143,9 @@ class ProfilePage extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-4 col-lg-3">
|
<div className="col-md-4 col-lg-3">
|
||||||
<StatusAlert alertType="danger" dialog={photoUploadError.userMessage} dismissible={false} open />
|
<Alert variant="danger" dismissible={false} show>
|
||||||
|
{photoUploadError.userMessage}
|
||||||
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -174,19 +173,29 @@ class ProfilePage extends React.Component {
|
|||||||
socialLinks,
|
socialLinks,
|
||||||
draftSocialLinksByPlatform,
|
draftSocialLinksByPlatform,
|
||||||
visibilitySocialLinks,
|
visibilitySocialLinks,
|
||||||
|
learningGoal,
|
||||||
|
visibilityLearningGoal,
|
||||||
languageProficiencies,
|
languageProficiencies,
|
||||||
visibilityLanguageProficiencies,
|
visibilityLanguageProficiencies,
|
||||||
|
courseCertificates,
|
||||||
visibilityCourseCertificates,
|
visibilityCourseCertificates,
|
||||||
bio,
|
bio,
|
||||||
visibilityBio,
|
visibilityBio,
|
||||||
requiresParentalConsent,
|
requiresParentalConsent,
|
||||||
isLoadingProfile,
|
isLoadingProfile,
|
||||||
|
username,
|
||||||
|
saveState,
|
||||||
|
navigate,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (isLoadingProfile) {
|
if (isLoadingProfile) {
|
||||||
return <PageLoading srMessage={this.props.intl.formatMessage(messages['profile.loading'])} />;
|
return <PageLoading srMessage={this.props.intl.formatMessage(messages['profile.loading'])} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!username && saveState === 'error' && navigate) {
|
||||||
|
navigate('/notfound');
|
||||||
|
}
|
||||||
|
|
||||||
const commonFormProps = {
|
const commonFormProps = {
|
||||||
openHandler: this.handleOpen,
|
openHandler: this.handleOpen,
|
||||||
closeHandler: this.handleClose,
|
closeHandler: this.handleClose,
|
||||||
@@ -194,6 +203,17 @@ class ProfilePage extends React.Component {
|
|||||||
changeHandler: this.handleChange,
|
changeHandler: this.handleChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isBlockVisible = (blockInfo) => this.isAuthenticatedUserProfile()
|
||||||
|
|| (!this.isAuthenticatedUserProfile() && Boolean(blockInfo));
|
||||||
|
|
||||||
|
const isLanguageBlockVisible = isBlockVisible(languageProficiencies.length);
|
||||||
|
const isEducationBlockVisible = isBlockVisible(levelOfEducation);
|
||||||
|
const isSocialLinksBLockVisible = isBlockVisible(socialLinks.some((link) => link.socialLink !== null));
|
||||||
|
const isBioBlockVisible = isBlockVisible(bio);
|
||||||
|
const isCertificatesBlockVisible = isBlockVisible(courseCertificates.length);
|
||||||
|
const isNameBlockVisible = isBlockVisible(name);
|
||||||
|
const isLocationBlockVisible = isBlockVisible(country);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
<div className="row align-items-center pt-4 mb-4 pt-md-0 mb-md-0">
|
<div className="row align-items-center pt-4 mb-4 pt-md-0 mb-md-0">
|
||||||
@@ -210,7 +230,7 @@ class ProfilePage extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col pl-0">
|
<div className="col">
|
||||||
<div className="d-md-none">
|
<div className="d-md-none">
|
||||||
{this.renderHeadingLockup()}
|
{this.renderHeadingLockup()}
|
||||||
</div>
|
</div>
|
||||||
@@ -228,51 +248,73 @@ class ProfilePage extends React.Component {
|
|||||||
<div className="d-md-none mb-4">
|
<div className="d-md-none mb-4">
|
||||||
{this.renderViewMyRecordsButton()}
|
{this.renderViewMyRecordsButton()}
|
||||||
</div>
|
</div>
|
||||||
<Name
|
{isNameBlockVisible && (
|
||||||
name={name}
|
<Name
|
||||||
visibilityName={visibilityName}
|
name={name}
|
||||||
formId="name"
|
visibilityName={visibilityName}
|
||||||
{...commonFormProps}
|
formId="name"
|
||||||
/>
|
{...commonFormProps}
|
||||||
<Country
|
/>
|
||||||
country={country}
|
)}
|
||||||
visibilityCountry={visibilityCountry}
|
{isLocationBlockVisible && (
|
||||||
formId="country"
|
<Country
|
||||||
{...commonFormProps}
|
country={country}
|
||||||
/>
|
visibilityCountry={visibilityCountry}
|
||||||
<PreferredLanguage
|
formId="country"
|
||||||
languageProficiencies={languageProficiencies}
|
{...commonFormProps}
|
||||||
visibilityLanguageProficiencies={visibilityLanguageProficiencies}
|
/>
|
||||||
formId="languageProficiencies"
|
)}
|
||||||
{...commonFormProps}
|
{isLanguageBlockVisible && (
|
||||||
/>
|
<PreferredLanguage
|
||||||
<Education
|
languageProficiencies={languageProficiencies}
|
||||||
levelOfEducation={levelOfEducation}
|
visibilityLanguageProficiencies={visibilityLanguageProficiencies}
|
||||||
visibilityLevelOfEducation={visibilityLevelOfEducation}
|
formId="languageProficiencies"
|
||||||
formId="levelOfEducation"
|
{...commonFormProps}
|
||||||
{...commonFormProps}
|
/>
|
||||||
/>
|
)}
|
||||||
<SocialLinks
|
{isEducationBlockVisible && (
|
||||||
socialLinks={socialLinks}
|
<Education
|
||||||
draftSocialLinksByPlatform={draftSocialLinksByPlatform}
|
levelOfEducation={levelOfEducation}
|
||||||
visibilitySocialLinks={visibilitySocialLinks}
|
visibilityLevelOfEducation={visibilityLevelOfEducation}
|
||||||
formId="socialLinks"
|
formId="levelOfEducation"
|
||||||
{...commonFormProps}
|
{...commonFormProps}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{isSocialLinksBLockVisible && (
|
||||||
|
<SocialLinks
|
||||||
|
socialLinks={socialLinks}
|
||||||
|
draftSocialLinksByPlatform={draftSocialLinksByPlatform}
|
||||||
|
visibilitySocialLinks={visibilitySocialLinks}
|
||||||
|
formId="socialLinks"
|
||||||
|
{...commonFormProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-md-3 col-md-8 col-lg-7 offset-lg-1">
|
<div className="pt-md-3 col-md-8 col-lg-7 offset-lg-1">
|
||||||
{this.renderAgeMessage()}
|
{!this.isYOBDisabled() && this.renderAgeMessage()}
|
||||||
<Bio
|
{isBioBlockVisible && (
|
||||||
bio={bio}
|
<Bio
|
||||||
visibilityBio={visibilityBio}
|
bio={bio}
|
||||||
formId="bio"
|
visibilityBio={visibilityBio}
|
||||||
{...commonFormProps}
|
formId="bio"
|
||||||
/>
|
{...commonFormProps}
|
||||||
<Certificates
|
/>
|
||||||
visibilityCourseCertificates={visibilityCourseCertificates}
|
)}
|
||||||
formId="certificates"
|
{getConfig().ENABLE_SKILLS_BUILDER_PROFILE && (
|
||||||
{...commonFormProps}
|
<LearningGoal
|
||||||
/>
|
learningGoal={learningGoal}
|
||||||
|
visibilityLearningGoal={visibilityLearningGoal}
|
||||||
|
formId="learningGoal"
|
||||||
|
{...commonFormProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isCertificatesBlockVisible && (
|
||||||
|
<Certificates
|
||||||
|
visibilityCourseCertificates={visibilityCourseCertificates}
|
||||||
|
formId="certificates"
|
||||||
|
{...commonFormProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -295,9 +337,11 @@ ProfilePage.propTypes = {
|
|||||||
// Account data
|
// Account data
|
||||||
requiresParentalConsent: PropTypes.bool,
|
requiresParentalConsent: PropTypes.bool,
|
||||||
dateJoined: PropTypes.string,
|
dateJoined: PropTypes.string,
|
||||||
|
username: PropTypes.string,
|
||||||
|
|
||||||
// Bio form data
|
// Bio form data
|
||||||
bio: PropTypes.string,
|
bio: PropTypes.string,
|
||||||
|
yearOfBirth: PropTypes.number,
|
||||||
visibilityBio: PropTypes.string.isRequired,
|
visibilityBio: PropTypes.string.isRequired,
|
||||||
|
|
||||||
// Certificates form data
|
// Certificates form data
|
||||||
@@ -335,6 +379,10 @@ ProfilePage.propTypes = {
|
|||||||
})),
|
})),
|
||||||
visibilitySocialLinks: PropTypes.string.isRequired,
|
visibilitySocialLinks: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
// Learning Goal form data
|
||||||
|
learningGoal: PropTypes.string,
|
||||||
|
visibilityLearningGoal: PropTypes.string.isRequired,
|
||||||
|
|
||||||
// Other data we need
|
// Other data we need
|
||||||
profileImage: PropTypes.shape({
|
profileImage: PropTypes.shape({
|
||||||
src: PropTypes.string,
|
src: PropTypes.string,
|
||||||
@@ -355,12 +403,11 @@ ProfilePage.propTypes = {
|
|||||||
openForm: PropTypes.func.isRequired,
|
openForm: PropTypes.func.isRequired,
|
||||||
closeForm: PropTypes.func.isRequired,
|
closeForm: PropTypes.func.isRequired,
|
||||||
updateDraft: PropTypes.func.isRequired,
|
updateDraft: PropTypes.func.isRequired,
|
||||||
|
navigate: PropTypes.func.isRequired,
|
||||||
|
|
||||||
// Router
|
// Router
|
||||||
match: PropTypes.shape({
|
params: PropTypes.shape({
|
||||||
params: PropTypes.shape({
|
username: PropTypes.string.isRequired,
|
||||||
username: PropTypes.string.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
|
||||||
// i18n
|
// i18n
|
||||||
@@ -369,15 +416,18 @@ ProfilePage.propTypes = {
|
|||||||
|
|
||||||
ProfilePage.defaultProps = {
|
ProfilePage.defaultProps = {
|
||||||
saveState: null,
|
saveState: null,
|
||||||
|
username: '',
|
||||||
savePhotoState: null,
|
savePhotoState: null,
|
||||||
photoUploadError: {},
|
photoUploadError: {},
|
||||||
profileImage: {},
|
profileImage: {},
|
||||||
name: null,
|
name: null,
|
||||||
|
yearOfBirth: null,
|
||||||
levelOfEducation: null,
|
levelOfEducation: null,
|
||||||
country: null,
|
country: null,
|
||||||
socialLinks: [],
|
socialLinks: [],
|
||||||
draftSocialLinksByPlatform: {},
|
draftSocialLinksByPlatform: {},
|
||||||
bio: null,
|
bio: null,
|
||||||
|
learningGoal: null,
|
||||||
languageProficiencies: [],
|
languageProficiencies: [],
|
||||||
courseCertificates: null,
|
courseCertificates: null,
|
||||||
requiresParentalConsent: null,
|
requiresParentalConsent: null,
|
||||||
@@ -395,4 +445,4 @@ export default connect(
|
|||||||
closeForm,
|
closeForm,
|
||||||
updateDraft,
|
updateDraft,
|
||||||
},
|
},
|
||||||
)(injectIntl(ProfilePage));
|
)(injectIntl(withParams(ProfilePage)));
|
||||||
|
|||||||
@@ -3,22 +3,24 @@ import { getConfig } from '@edx/frontend-platform';
|
|||||||
import * as analytics from '@edx/frontend-platform/analytics';
|
import * as analytics from '@edx/frontend-platform/analytics';
|
||||||
import { AppContext } from '@edx/frontend-platform/react';
|
import { AppContext } from '@edx/frontend-platform/react';
|
||||||
import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n';
|
import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import { mount } from 'enzyme';
|
import { render } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import renderer from 'react-test-renderer';
|
|
||||||
import configureMockStore from 'redux-mock-store';
|
import configureMockStore from 'redux-mock-store';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
|
import { BrowserRouter, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import messages from '../i18n';
|
import messages from '../i18n';
|
||||||
import ProfilePage from './ProfilePage';
|
import ProfilePage from './ProfilePage';
|
||||||
|
|
||||||
const mockStore = configureMockStore([thunk]);
|
const mockStore = configureMockStore([thunk]);
|
||||||
const storeMocks = {
|
const storeMocks = {
|
||||||
loadingApp: require('./__mocks__/loadingApp.mockStore.js'),
|
loadingApp: require('./__mocks__/loadingApp.mockStore'),
|
||||||
viewOwnProfile: require('./__mocks__/viewOwnProfile.mockStore.js'),
|
invalidUser: require('./__mocks__/invalidUser.mockStore'),
|
||||||
viewOtherProfile: require('./__mocks__/viewOtherProfile.mockStore.js'),
|
viewOwnProfile: require('./__mocks__/viewOwnProfile.mockStore'),
|
||||||
savingEditedBio: require('./__mocks__/savingEditedBio.mockStore.js'),
|
viewOtherProfile: require('./__mocks__/viewOtherProfile.mockStore'),
|
||||||
|
savingEditedBio: require('./__mocks__/savingEditedBio.mockStore'),
|
||||||
};
|
};
|
||||||
const requiredProfilePageProps = {
|
const requiredProfilePageProps = {
|
||||||
fetchUserAccount: () => {},
|
fetchUserAccount: () => {},
|
||||||
@@ -28,7 +30,7 @@ const requiredProfilePageProps = {
|
|||||||
deleteProfilePhoto: () => {},
|
deleteProfilePhoto: () => {},
|
||||||
openField: () => {},
|
openField: () => {},
|
||||||
closeField: () => {},
|
closeField: () => {},
|
||||||
match: { params: { username: 'staff' } },
|
params: { username: 'staff' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock language cookie
|
// Mock language cookie
|
||||||
@@ -65,84 +67,203 @@ beforeEach(() => {
|
|||||||
analytics.sendTrackingLogEvent.mockReset();
|
analytics.sendTrackingLogEvent.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ProfileWrapper = ({ params, requiresParentalConsent }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<ProfilePage
|
||||||
|
{...requiredProfilePageProps}
|
||||||
|
params={params}
|
||||||
|
requiresParentalConsent={requiresParentalConsent}
|
||||||
|
navigate={navigate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProfileWrapper.propTypes = {
|
||||||
|
params: PropTypes.shape({}).isRequired,
|
||||||
|
requiresParentalConsent: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProfilePageWrapper = ({
|
||||||
|
contextValue, store, params, requiresParentalConsent,
|
||||||
|
}) => (
|
||||||
|
<AppContext.Provider
|
||||||
|
value={contextValue}
|
||||||
|
>
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<Provider store={store}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<ProfileWrapper
|
||||||
|
params={params}
|
||||||
|
requiresParentalConsent={requiresParentalConsent}
|
||||||
|
/>
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
</IntlProvider>
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
ProfilePageWrapper.defaultProps = {
|
||||||
|
params: { username: 'staff' },
|
||||||
|
requiresParentalConsent: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
ProfilePageWrapper.propTypes = {
|
||||||
|
contextValue: PropTypes.shape({}).isRequired,
|
||||||
|
store: PropTypes.shape({}).isRequired,
|
||||||
|
params: PropTypes.shape({}),
|
||||||
|
requiresParentalConsent: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
describe('<ProfilePage />', () => {
|
describe('<ProfilePage />', () => {
|
||||||
describe('Renders correctly in various states', () => {
|
describe('Renders correctly in various states', () => {
|
||||||
it('app loading', () => {
|
it('app loading', () => {
|
||||||
const component = (
|
const contextValue = {
|
||||||
<AppContext.Provider
|
authenticatedUser: { userId: null, username: null, administrator: false },
|
||||||
value={{
|
config: getConfig(),
|
||||||
authenticatedUser: { userId: null, username: null, administrator: false },
|
};
|
||||||
config: getConfig(),
|
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeMocks.loadingApp)} />;
|
||||||
}}
|
const { container: tree } = render(component);
|
||||||
>
|
expect(tree).toMatchSnapshot();
|
||||||
<IntlProvider locale="en">
|
});
|
||||||
<Provider store={mockStore(storeMocks.loadingApp)}>
|
|
||||||
<ProfilePage {...requiredProfilePageProps} />
|
it('successfully redirected to not found page.', () => {
|
||||||
</Provider>
|
const contextValue = {
|
||||||
</IntlProvider>
|
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||||
</AppContext.Provider>
|
config: getConfig(),
|
||||||
);
|
};
|
||||||
const tree = renderer.create(component).toJSON();
|
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeMocks.invalidUser)} />;
|
||||||
|
const { container: tree } = render(component);
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('viewing own profile', () => {
|
it('viewing own profile', () => {
|
||||||
const component = (
|
const contextValue = {
|
||||||
<AppContext.Provider
|
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||||
value={{
|
config: getConfig(),
|
||||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
};
|
||||||
config: getConfig(),
|
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeMocks.viewOwnProfile)} />;
|
||||||
}}
|
const { container: tree } = render(component);
|
||||||
>
|
|
||||||
<IntlProvider locale="en">
|
|
||||||
<Provider store={mockStore(storeMocks.viewOwnProfile)}>
|
|
||||||
<ProfilePage {...requiredProfilePageProps} />
|
|
||||||
</Provider>
|
|
||||||
</IntlProvider>
|
|
||||||
</AppContext.Provider>
|
|
||||||
);
|
|
||||||
const tree = renderer.create(component).toJSON();
|
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('viewing other profile', () => {
|
it('viewing other profile with all fields', () => {
|
||||||
|
const contextValue = {
|
||||||
|
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||||
|
config: getConfig(),
|
||||||
|
};
|
||||||
|
|
||||||
const component = (
|
const component = (
|
||||||
<AppContext.Provider
|
<ProfilePageWrapper
|
||||||
value={{
|
contextValue={contextValue}
|
||||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
store={mockStore({
|
||||||
config: getConfig(),
|
...storeMocks.viewOtherProfile,
|
||||||
}}
|
profilePage: {
|
||||||
>
|
...storeMocks.viewOtherProfile.profilePage,
|
||||||
<IntlProvider locale="en">
|
account: {
|
||||||
<Provider store={mockStore(storeMocks.viewOtherProfile)}>
|
...storeMocks.viewOtherProfile.profilePage.account,
|
||||||
<ProfilePage
|
name: 'user',
|
||||||
{...requiredProfilePageProps}
|
country: 'EN',
|
||||||
match={{ params: { username: 'verified' } }} // Override default match
|
bio: 'bio',
|
||||||
/>
|
courseCertificates: ['course certificates'],
|
||||||
</Provider>
|
levelOfEducation: 'some level',
|
||||||
</IntlProvider>
|
languageProficiencies: ['some lang'],
|
||||||
</AppContext.Provider>
|
socialLinks: ['twitter'],
|
||||||
|
timeZone: 'time zone',
|
||||||
|
accountPrivacy: 'all_users',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
match={{ params: { username: 'verified' } }} // Override default match
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
const tree = renderer.create(component).toJSON();
|
const { container: tree } = render(component);
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('while saving an edited bio', () => {
|
it('while saving an edited bio', () => {
|
||||||
|
const contextValue = {
|
||||||
|
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||||
|
config: getConfig(),
|
||||||
|
};
|
||||||
const component = (
|
const component = (
|
||||||
<AppContext.Provider
|
<ProfilePageWrapper
|
||||||
value={{
|
contextValue={contextValue}
|
||||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
store={mockStore(storeMocks.savingEditedBio)}
|
||||||
config: getConfig(),
|
/>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IntlProvider locale="en">
|
|
||||||
<Provider store={mockStore(storeMocks.savingEditedBio)}>
|
|
||||||
<ProfilePage {...requiredProfilePageProps} />
|
|
||||||
</Provider>
|
|
||||||
</IntlProvider>
|
|
||||||
</AppContext.Provider>
|
|
||||||
);
|
);
|
||||||
const tree = renderer.create(component).toJSON();
|
const { container: tree } = render(component);
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('while saving an edited bio with error', () => {
|
||||||
|
const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio));
|
||||||
|
storeData.profilePage.errors.bio = { userMessage: 'bio error' };
|
||||||
|
const contextValue = {
|
||||||
|
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||||
|
config: getConfig(),
|
||||||
|
};
|
||||||
|
const component = (
|
||||||
|
<ProfilePageWrapper
|
||||||
|
contextValue={contextValue}
|
||||||
|
store={mockStore(storeData)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const { container: tree } = render(component);
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test country edit with error', () => {
|
||||||
|
const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio));
|
||||||
|
storeData.profilePage.errors.country = { userMessage: 'country error' };
|
||||||
|
storeData.profilePage.currentlyEditingField = 'country';
|
||||||
|
const contextValue = {
|
||||||
|
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||||
|
config: getConfig(),
|
||||||
|
};
|
||||||
|
const component = (
|
||||||
|
<ProfilePageWrapper
|
||||||
|
contextValue={contextValue}
|
||||||
|
store={mockStore(storeData)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const { container: tree } = render(component);
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test education edit with error', () => {
|
||||||
|
const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio));
|
||||||
|
storeData.profilePage.errors.levelOfEducation = { userMessage: 'education error' };
|
||||||
|
storeData.profilePage.currentlyEditingField = 'levelOfEducation';
|
||||||
|
const contextValue = {
|
||||||
|
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||||
|
config: getConfig(),
|
||||||
|
};
|
||||||
|
const component = (
|
||||||
|
<ProfilePageWrapper
|
||||||
|
contextValue={contextValue}
|
||||||
|
store={mockStore(storeData)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const { container: tree } = render(component);
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test preferreded language edit with error', () => {
|
||||||
|
const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio));
|
||||||
|
storeData.profilePage.errors.languageProficiencies = { userMessage: 'preferred language error' };
|
||||||
|
storeData.profilePage.currentlyEditingField = 'languageProficiencies';
|
||||||
|
const contextValue = {
|
||||||
|
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||||
|
config: getConfig(),
|
||||||
|
};
|
||||||
|
const component = (
|
||||||
|
<ProfilePageWrapper
|
||||||
|
contextValue={contextValue}
|
||||||
|
store={mockStore(storeData)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const { container: tree } = render(component);
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -150,46 +271,69 @@ describe('<ProfilePage />', () => {
|
|||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
config.CREDENTIALS_BASE_URL = '';
|
config.CREDENTIALS_BASE_URL = '';
|
||||||
|
|
||||||
|
const contextValue = {
|
||||||
|
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||||
|
config: getConfig(),
|
||||||
|
};
|
||||||
const component = (
|
const component = (
|
||||||
<AppContext.Provider
|
<ProfilePageWrapper
|
||||||
value={{
|
contextValue={contextValue}
|
||||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
store={mockStore(storeMocks.viewOwnProfile)}
|
||||||
config,
|
/>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IntlProvider locale="en">
|
|
||||||
<Provider store={mockStore(storeMocks.viewOwnProfile)}>
|
|
||||||
<ProfilePage {...requiredProfilePageProps} />
|
|
||||||
</Provider>
|
|
||||||
</IntlProvider>
|
|
||||||
</AppContext.Provider>
|
|
||||||
);
|
);
|
||||||
const tree = renderer.create(component).toJSON();
|
const { container: tree } = render(component);
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
it('test age message alert', () => {
|
||||||
|
const storeData = JSON.parse(JSON.stringify(storeMocks.viewOwnProfile));
|
||||||
|
storeData.userAccount.requiresParentalConsent = true;
|
||||||
|
storeData.profilePage.account.requiresParentalConsent = true;
|
||||||
|
const contextValue = {
|
||||||
|
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||||
|
config: { ...getConfig(), COLLECT_YEAR_OF_BIRTH: true },
|
||||||
|
};
|
||||||
|
const { container } = render(
|
||||||
|
<ProfilePageWrapper
|
||||||
|
contextValue={contextValue}
|
||||||
|
store={mockStore(storeData)}
|
||||||
|
requiresParentalConsent
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container.querySelector('.alert-info')).toHaveClass('show');
|
||||||
|
});
|
||||||
|
it('test photo error alert', () => {
|
||||||
|
const storeData = JSON.parse(JSON.stringify(storeMocks.viewOwnProfile));
|
||||||
|
storeData.profilePage.errors.photo = { userMessage: 'error' };
|
||||||
|
const contextValue = {
|
||||||
|
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||||
|
config: { ...getConfig(), COLLECT_YEAR_OF_BIRTH: true },
|
||||||
|
};
|
||||||
|
const { container } = render(
|
||||||
|
<ProfilePageWrapper
|
||||||
|
contextValue={contextValue}
|
||||||
|
store={mockStore(storeData)}
|
||||||
|
requiresParentalConsent
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container.querySelector('.alert-danger')).toHaveClass('show');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handles analytics', () => {
|
describe('handles analytics', () => {
|
||||||
it('calls sendTrackingLogEvent when mounting', () => {
|
it('calls sendTrackingLogEvent when mounting', () => {
|
||||||
const component = (
|
const contextValue = {
|
||||||
<AppContext.Provider
|
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||||
value={{
|
config: getConfig(),
|
||||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
};
|
||||||
config: getConfig(),
|
render(
|
||||||
}}
|
<ProfilePageWrapper
|
||||||
>
|
contextValue={contextValue}
|
||||||
<IntlProvider locale="en">
|
store={mockStore(storeMocks.loadingApp)}
|
||||||
<Provider store={mockStore(storeMocks.loadingApp)}>
|
params={{ username: 'test-username' }}
|
||||||
<ProfilePage
|
/>,
|
||||||
{...requiredProfilePageProps}
|
|
||||||
match={{ params: { username: 'test-username' } }}
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
</IntlProvider>
|
|
||||||
</AppContext.Provider>
|
|
||||||
);
|
);
|
||||||
const wrapper = mount(component);
|
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
expect(analytics.sendTrackingLogEvent.mock.calls.length).toBe(1);
|
expect(analytics.sendTrackingLogEvent.mock.calls.length).toBe(1);
|
||||||
expect(analytics.sendTrackingLogEvent.mock.calls[0][0]).toEqual('edx.profile.viewed');
|
expect(analytics.sendTrackingLogEvent.mock.calls[0][0]).toEqual('edx.profile.viewed');
|
||||||
|
|||||||
23
src/profile/UsernameDescription.jsx
Normal file
23
src/profile/UsernameDescription.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
|
import { VisibilityOff } from '@openedx/paragon/icons';
|
||||||
|
import { Icon } from '@openedx/paragon';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
|
||||||
|
const UsernameDescription = () => (
|
||||||
|
<div className="d-flex align-items-center mt-3 mb-2rem">
|
||||||
|
<Icon src={VisibilityOff} className="icon-visibility-off" />
|
||||||
|
<div className="username-description">
|
||||||
|
<FormattedMessage
|
||||||
|
id="profile.username.description"
|
||||||
|
defaultMessage="Your profile information is only visible to you. Only your username is visible to others on {siteName}."
|
||||||
|
description="A description of the username field"
|
||||||
|
values={{
|
||||||
|
siteName: getConfig().SITE_NAME,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default UsernameDescription;
|
||||||
41
src/profile/__mocks__/invalidUser.mockStore.js
Normal file
41
src/profile/__mocks__/invalidUser.mockStore.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
module.exports = {
|
||||||
|
userAccount: {
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
username: 'staff',
|
||||||
|
email: null,
|
||||||
|
bio: null,
|
||||||
|
name: null,
|
||||||
|
country: null,
|
||||||
|
socialLinks: null,
|
||||||
|
profileImage: {
|
||||||
|
imageUrlMedium: null,
|
||||||
|
imageUrlLarge: null
|
||||||
|
},
|
||||||
|
levelOfEducation: null,
|
||||||
|
learningGoal: null
|
||||||
|
},
|
||||||
|
profilePage: {
|
||||||
|
errors: {},
|
||||||
|
saveState: 'error',
|
||||||
|
savePhotoState: null,
|
||||||
|
currentlyEditingField: null,
|
||||||
|
account: {
|
||||||
|
username: '',
|
||||||
|
socialLinks: []
|
||||||
|
},
|
||||||
|
preferences: {},
|
||||||
|
courseCertificates: [],
|
||||||
|
drafts: {},
|
||||||
|
isLoadingProfile: false,
|
||||||
|
isAuthenticatedUserProfile: true,
|
||||||
|
},
|
||||||
|
router: {
|
||||||
|
location: {
|
||||||
|
pathname: '/u/staffTest',
|
||||||
|
search: '',
|
||||||
|
hash: ''
|
||||||
|
},
|
||||||
|
action: 'POP'
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -12,7 +12,8 @@ module.exports = {
|
|||||||
imageUrlMedium: null,
|
imageUrlMedium: null,
|
||||||
imageUrlLarge: null
|
imageUrlLarge: null
|
||||||
},
|
},
|
||||||
levelOfEducation: null
|
levelOfEducation: null,
|
||||||
|
learningGoal: null
|
||||||
},
|
},
|
||||||
profilePage: {
|
profilePage: {
|
||||||
errors: {},
|
errors: {},
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ module.exports = {
|
|||||||
secondaryEmail: null,
|
secondaryEmail: null,
|
||||||
timeZone: null,
|
timeZone: null,
|
||||||
gender: null,
|
gender: null,
|
||||||
accountPrivacy: 'custom'
|
accountPrivacy: 'custom',
|
||||||
|
learningGoal: null,
|
||||||
},
|
},
|
||||||
profilePage: {
|
profilePage: {
|
||||||
errors: {},
|
errors: {},
|
||||||
@@ -91,7 +92,8 @@ module.exports = {
|
|||||||
timeZone: null,
|
timeZone: null,
|
||||||
levelOfEducation: 'el',
|
levelOfEducation: 'el',
|
||||||
gender: null,
|
gender: null,
|
||||||
accountPrivacy: 'custom'
|
accountPrivacy: 'custom',
|
||||||
|
learningGoal: null,
|
||||||
},
|
},
|
||||||
preferences: {
|
preferences: {
|
||||||
visibilityUserLocation: 'all_users',
|
visibilityUserLocation: 'all_users',
|
||||||
@@ -104,7 +106,8 @@ module.exports = {
|
|||||||
visibilityName: 'private',
|
visibilityName: 'private',
|
||||||
visibilityLanguageProficiencies: 'all_users',
|
visibilityLanguageProficiencies: 'all_users',
|
||||||
visibilityCountry: 'all_users',
|
visibilityCountry: 'all_users',
|
||||||
accountPrivacy: 'custom'
|
accountPrivacy: 'custom',
|
||||||
|
visibilityLearningGoal: 'private',
|
||||||
},
|
},
|
||||||
courseCertificates: [
|
courseCertificates: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ module.exports = {
|
|||||||
secondaryEmail: null,
|
secondaryEmail: null,
|
||||||
timeZone: null,
|
timeZone: null,
|
||||||
gender: null,
|
gender: null,
|
||||||
accountPrivacy: 'custom'
|
accountPrivacy: 'custom',
|
||||||
|
learningGoal: 'advance_career',
|
||||||
},
|
},
|
||||||
profilePage: {
|
profilePage: {
|
||||||
errors: {},
|
errors: {},
|
||||||
@@ -83,7 +84,8 @@ module.exports = {
|
|||||||
preferences: {},
|
preferences: {},
|
||||||
courseCertificates: [],
|
courseCertificates: [],
|
||||||
drafts: {},
|
drafts: {},
|
||||||
isLoadingProfile: false
|
isLoadingProfile: false,
|
||||||
|
learningGoal: 'advance_career',
|
||||||
},
|
},
|
||||||
router: {
|
router: {
|
||||||
location: {
|
location: {
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ module.exports = {
|
|||||||
secondaryEmail: null,
|
secondaryEmail: null,
|
||||||
timeZone: null,
|
timeZone: null,
|
||||||
gender: null,
|
gender: null,
|
||||||
accountPrivacy: 'custom'
|
accountPrivacy: 'custom',
|
||||||
|
learningGoal: 'advance_career'
|
||||||
},
|
},
|
||||||
profilePage: {
|
profilePage: {
|
||||||
errors: {},
|
errors: {},
|
||||||
@@ -91,7 +92,8 @@ module.exports = {
|
|||||||
timeZone: null,
|
timeZone: null,
|
||||||
levelOfEducation: 'el',
|
levelOfEducation: 'el',
|
||||||
gender: null,
|
gender: null,
|
||||||
accountPrivacy: 'custom'
|
accountPrivacy: 'custom',
|
||||||
|
learningGoal: 'advance_career'
|
||||||
},
|
},
|
||||||
preferences: {
|
preferences: {
|
||||||
visibilityUserLocation: 'all_users',
|
visibilityUserLocation: 'all_users',
|
||||||
@@ -104,7 +106,8 @@ module.exports = {
|
|||||||
visibilityName: 'private',
|
visibilityName: 'private',
|
||||||
visibilityLanguageProficiencies: 'all_users',
|
visibilityLanguageProficiencies: 'all_users',
|
||||||
visibilityCountry: 'all_users',
|
visibilityCountry: 'all_users',
|
||||||
accountPrivacy: 'custom'
|
accountPrivacy: 'custom',
|
||||||
|
visibilityLearningGoal: 'private',
|
||||||
},
|
},
|
||||||
courseCertificates: [
|
courseCertificates: [
|
||||||
{
|
{
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ const EDUCATION_LEVELS = [
|
|||||||
'jhs',
|
'jhs',
|
||||||
'el',
|
'el',
|
||||||
'none',
|
'none',
|
||||||
'o',
|
'other',
|
||||||
];
|
];
|
||||||
|
|
||||||
const SOCIAL = {
|
const SOCIAL = {
|
||||||
|
|||||||
7
src/profile/data/mock_data.js
Normal file
7
src/profile/data/mock_data.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const mockData = {
|
||||||
|
learningGoal: 'advance_career',
|
||||||
|
editMode: 'static',
|
||||||
|
visibilityLearningGoal: 'private',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default mockData;
|
||||||
80
src/profile/data/pact-profile.test.js
Normal file
80
src/profile/data/pact-profile.test.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// This test file simply creates a contract that defines
|
||||||
|
// expectations and correct responses from the Pact stub server.
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
|
||||||
|
|
||||||
|
import { initializeMockApp, getConfig, setConfig } from '@edx/frontend-platform';
|
||||||
|
import { getAccount } from './services';
|
||||||
|
|
||||||
|
const expectedUserInfo200 = {
|
||||||
|
username: 'staff',
|
||||||
|
email: 'staff@example.com',
|
||||||
|
bio: 'This is my bio',
|
||||||
|
name: 'Lemon Seltzer',
|
||||||
|
country: 'ME',
|
||||||
|
dateJoined: '2017-06-07T00:44:23Z',
|
||||||
|
isActive: true,
|
||||||
|
yearOfBirth: 1901,
|
||||||
|
};
|
||||||
|
|
||||||
|
const provider = new PactV3({
|
||||||
|
log: path.resolve(process.cwd(), 'src/pact-logs/pact.log'),
|
||||||
|
dir: path.resolve(process.cwd(), 'src/pacts'),
|
||||||
|
consumer: 'frontend-app-profile',
|
||||||
|
provider: 'edx-platform',
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAccount for one username', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
initializeMockApp();
|
||||||
|
});
|
||||||
|
it('returns a HTTP 200 and user information', async () => {
|
||||||
|
const username200 = 'staff';
|
||||||
|
await provider.addInteraction({
|
||||||
|
states: [{ description: "I have a user's basic information" }],
|
||||||
|
uponReceiving: "A request for user's basic information",
|
||||||
|
withRequest: {
|
||||||
|
method: 'GET',
|
||||||
|
path: `/api/user/v1/accounts/${username200}`,
|
||||||
|
headers: {},
|
||||||
|
},
|
||||||
|
willRespondWith: {
|
||||||
|
status: 200,
|
||||||
|
headers: {},
|
||||||
|
body: MatchersV3.like(expectedUserInfo200),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return provider.executeTest(async (mockserver) => {
|
||||||
|
setConfig({
|
||||||
|
...getConfig(),
|
||||||
|
LMS_BASE_URL: mockserver.url,
|
||||||
|
});
|
||||||
|
const response = await getAccount(username200);
|
||||||
|
expect(response).toEqual(expectedUserInfo200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Account does not exist', async () => {
|
||||||
|
const username404 = 'staff_not_found';
|
||||||
|
await provider.addInteraction({
|
||||||
|
states: [{ description: "Account and user's information does not exist" }],
|
||||||
|
uponReceiving: "A request for user's basic information",
|
||||||
|
withRequest: {
|
||||||
|
method: 'GET',
|
||||||
|
path: `/api/user/v1/accounts/${username404}`,
|
||||||
|
},
|
||||||
|
willRespondWith: {
|
||||||
|
status: 404,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await provider.executeTest(async (mockserver) => {
|
||||||
|
setConfig({
|
||||||
|
...getConfig(),
|
||||||
|
LMS_BASE_URL: mockserver.url,
|
||||||
|
});
|
||||||
|
await expect(getAccount(username404).then((response) => response.data)).rejects.toThrow('Request failed with status code 404');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -24,7 +24,7 @@ export const initialState = {
|
|||||||
isAuthenticatedUserProfile: false,
|
isAuthenticatedUserProfile: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const profilePage = (state = initialState, action) => {
|
const profilePage = (state = initialState, action = {}) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case FETCH_PROFILE.BEGIN:
|
case FETCH_PROFILE.BEGIN:
|
||||||
return {
|
return {
|
||||||
@@ -63,12 +63,14 @@ const profilePage = (state = initialState, action) => {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
saveState: 'error',
|
saveState: 'error',
|
||||||
|
isLoadingProfile: false,
|
||||||
errors: { ...state.errors, ...action.payload.errors },
|
errors: { ...state.errors, ...action.payload.errors },
|
||||||
};
|
};
|
||||||
case SAVE_PROFILE.RESET:
|
case SAVE_PROFILE.RESET:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
saveState: null,
|
saveState: null,
|
||||||
|
isLoadingProfile: false,
|
||||||
errors: {},
|
errors: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { history } from '@edx/frontend-platform';
|
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import pick from 'lodash.pick';
|
import pick from 'lodash.pick';
|
||||||
import {
|
import {
|
||||||
@@ -66,6 +65,25 @@ export function* handleFetchProfile(action) {
|
|||||||
} else {
|
} else {
|
||||||
[account, courseCertificates] = result;
|
[account, courseCertificates] = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set initial visibility values for account
|
||||||
|
// Set account_privacy as custom is necessary so that when viewing another user's profile,
|
||||||
|
// their full name is displayed and change visibility forms are worked correctly
|
||||||
|
if (isAuthenticatedUserProfile && result[0].accountPrivacy === 'all_users') {
|
||||||
|
yield call(ProfileApiService.patchPreferences, action.payload.username, {
|
||||||
|
account_privacy: 'custom',
|
||||||
|
'visibility.name': 'all_users',
|
||||||
|
'visibility.bio': 'all_users',
|
||||||
|
'visibility.course_certificates': 'all_users',
|
||||||
|
'visibility.country': 'all_users',
|
||||||
|
'visibility.date_joined': 'all_users',
|
||||||
|
'visibility.level_of_education': 'all_users',
|
||||||
|
'visibility.language_proficiencies': 'all_users',
|
||||||
|
'visibility.social_links': 'all_users',
|
||||||
|
'visibility.time_zone': 'all_users',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
yield put(fetchProfileSuccess(
|
yield put(fetchProfileSuccess(
|
||||||
account,
|
account,
|
||||||
preferences,
|
preferences,
|
||||||
@@ -76,7 +94,11 @@ export function* handleFetchProfile(action) {
|
|||||||
yield put(fetchProfileReset());
|
yield put(fetchProfileReset());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.response.status === 404) {
|
if (e.response.status === 404) {
|
||||||
history.push('/notfound');
|
if (e.processedData && e.processedData.fieldErrors) {
|
||||||
|
yield put(saveProfileFailure(e.processedData.fieldErrors));
|
||||||
|
} else {
|
||||||
|
yield put(saveProfileFailure(e.customAttributes));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,12 @@ export const editableFormModeSelector = createSelector(
|
|||||||
// or is being hidden from us (for other users' profiles)
|
// or is being hidden from us (for other users' profiles)
|
||||||
let propExists = account[formId] != null && account[formId].length > 0;
|
let propExists = account[formId] != null && account[formId].length > 0;
|
||||||
propExists = formId === 'certificates' ? certificates.length > 0 : propExists; // overwrite for certificates
|
propExists = formId === 'certificates' ? certificates.length > 0 : propExists; // overwrite for certificates
|
||||||
// If this isn't the current user's profile or if
|
// If this isn't the current user's profile
|
||||||
|
if (!isAuthenticatedUserProfile) {
|
||||||
|
return 'static';
|
||||||
|
}
|
||||||
// the current user has no age set / under 13 ...
|
// the current user has no age set / under 13 ...
|
||||||
if (!isAuthenticatedUserProfile || account.requiresParentalConsent) {
|
if (account.requiresParentalConsent) {
|
||||||
// then there are only two options: static or nothing.
|
// then there are only two options: static or nothing.
|
||||||
// We use 'null' as a return value because the consumers of
|
// We use 'null' as a return value because the consumers of
|
||||||
// getMode render nothing at all on a mode of null.
|
// getMode render nothing at all on a mode of null.
|
||||||
@@ -228,13 +231,13 @@ export const visibilitiesSelector = createSelector(
|
|||||||
switch (accountPrivacy) {
|
switch (accountPrivacy) {
|
||||||
case 'custom':
|
case 'custom':
|
||||||
return {
|
return {
|
||||||
visibilityBio: preferences.visibilityBio || 'private',
|
visibilityBio: preferences.visibilityBio || 'all_users',
|
||||||
visibilityCourseCertificates: preferences.visibilityCourseCertificates || 'private',
|
visibilityCourseCertificates: preferences.visibilityCourseCertificates || 'all_users',
|
||||||
visibilityCountry: preferences.visibilityCountry || 'private',
|
visibilityCountry: preferences.visibilityCountry || 'all_users',
|
||||||
visibilityLevelOfEducation: preferences.visibilityLevelOfEducation || 'private',
|
visibilityLevelOfEducation: preferences.visibilityLevelOfEducation || 'all_users',
|
||||||
visibilityLanguageProficiencies: preferences.visibilityLanguageProficiencies || 'private',
|
visibilityLanguageProficiencies: preferences.visibilityLanguageProficiencies || 'all_users',
|
||||||
visibilityName: preferences.visibilityName || 'private',
|
visibilityName: preferences.visibilityName || 'all_users',
|
||||||
visibilitySocialLinks: preferences.visibilitySocialLinks || 'private',
|
visibilitySocialLinks: preferences.visibilitySocialLinks || 'all_users',
|
||||||
};
|
};
|
||||||
case 'private':
|
case 'private':
|
||||||
return {
|
return {
|
||||||
@@ -335,6 +338,7 @@ export const profilePageSelector = createSelector(
|
|||||||
profileImage,
|
profileImage,
|
||||||
requiresParentalConsent: account.requiresParentalConsent,
|
requiresParentalConsent: account.requiresParentalConsent,
|
||||||
dateJoined: account.dateJoined,
|
dateJoined: account.dateJoined,
|
||||||
|
yearOfBirth: account.yearOfBirth,
|
||||||
|
|
||||||
// Bio form data
|
// Bio form data
|
||||||
bio: formValues.bio,
|
bio: formValues.bio,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { ValidationFormGroup } from '@edx/paragon';
|
import { Form } from '@openedx/paragon';
|
||||||
|
|
||||||
import messages from './Bio.messages';
|
import messages from './Bio.messages';
|
||||||
|
|
||||||
@@ -56,10 +56,9 @@ class Bio extends React.Component {
|
|||||||
editing: (
|
editing: (
|
||||||
<div role="dialog" aria-labelledby={`${formId}-label`}>
|
<div role="dialog" aria-labelledby={`${formId}-label`}>
|
||||||
<form onSubmit={this.handleSubmit}>
|
<form onSubmit={this.handleSubmit}>
|
||||||
<ValidationFormGroup
|
<Form.Group
|
||||||
for={formId}
|
controlId={formId}
|
||||||
invalid={error !== null}
|
isInvalid={error !== null}
|
||||||
invalidMessage={error}
|
|
||||||
>
|
>
|
||||||
<label className="edit-section-header" htmlFor={formId}>
|
<label className="edit-section-header" htmlFor={formId}>
|
||||||
{intl.formatMessage(messages['profile.bio.about.me'])}
|
{intl.formatMessage(messages['profile.bio.about.me'])}
|
||||||
@@ -71,7 +70,12 @@ class Bio extends React.Component {
|
|||||||
value={bio}
|
value={bio}
|
||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
/>
|
/>
|
||||||
</ValidationFormGroup>
|
{error !== null && (
|
||||||
|
<Form.Control.Feedback hasIcon={false}>
|
||||||
|
{error}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
)}
|
||||||
|
</Form.Group>
|
||||||
<FormControls
|
<FormControls
|
||||||
visibilityId="visibilityBio"
|
visibilityId="visibilityBio"
|
||||||
saveState={saveState}
|
saveState={saveState}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import {
|
import {
|
||||||
FormattedDate, FormattedMessage, injectIntl, intlShape,
|
FormattedDate, FormattedMessage, injectIntl, intlShape,
|
||||||
} from '@edx/frontend-platform/i18n';
|
} from '@edx/frontend-platform/i18n';
|
||||||
import { Hyperlink } from '@edx/paragon';
|
import { Hyperlink } from '@openedx/paragon';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import get from 'lodash.get';
|
import get from 'lodash.get';
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ class Certificates extends React.Component {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={`${modifiedDate}-${courseId}`} className="col col-sm-6 d-flex align-items-stretch">
|
<div key={`${modifiedDate}-${courseId}`} className="col-12 col-sm-6 d-flex align-items-stretch">
|
||||||
<div className="card mb-4 certificate flex-grow-1">
|
<div className="card mb-4 certificate flex-grow-1">
|
||||||
<div
|
<div
|
||||||
className="certificate-type-illustration"
|
className="certificate-type-illustration"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { ValidationFormGroup } from '@edx/paragon';
|
import { Form } from '@openedx/paragon';
|
||||||
|
|
||||||
import messages from './Country.messages';
|
import messages from './Country.messages';
|
||||||
|
|
||||||
@@ -67,10 +67,9 @@ class Country extends React.Component {
|
|||||||
editing: (
|
editing: (
|
||||||
<div role="dialog" aria-labelledby={`${formId}-label`}>
|
<div role="dialog" aria-labelledby={`${formId}-label`}>
|
||||||
<form onSubmit={this.handleSubmit}>
|
<form onSubmit={this.handleSubmit}>
|
||||||
<ValidationFormGroup
|
<Form.Group
|
||||||
for={formId}
|
controlId={formId}
|
||||||
invalid={error !== null}
|
isInvalid={error !== null}
|
||||||
invalidMessage={error}
|
|
||||||
>
|
>
|
||||||
<label className="edit-section-header" htmlFor={formId}>
|
<label className="edit-section-header" htmlFor={formId}>
|
||||||
{intl.formatMessage(messages['profile.country.label'])}
|
{intl.formatMessage(messages['profile.country.label'])}
|
||||||
@@ -89,7 +88,12 @@ class Country extends React.Component {
|
|||||||
<option key={code} value={code}>{name}</option>
|
<option key={code} value={code}>{name}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</ValidationFormGroup>
|
{error !== null && (
|
||||||
|
<Form.Control.Feedback hasIcon={false}>
|
||||||
|
{error}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
)}
|
||||||
|
</Form.Group>
|
||||||
<FormControls
|
<FormControls
|
||||||
visibilityId="visibilityCountry"
|
visibilityId="visibilityCountry"
|
||||||
saveState={saveState}
|
saveState={saveState}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import get from 'lodash.get';
|
import get from 'lodash.get';
|
||||||
import { ValidationFormGroup } from '@edx/paragon';
|
import { Form } from '@openedx/paragon';
|
||||||
|
|
||||||
import messages from './Education.messages';
|
import messages from './Education.messages';
|
||||||
|
|
||||||
@@ -63,10 +63,9 @@ class Education extends React.Component {
|
|||||||
editing: (
|
editing: (
|
||||||
<div role="dialog" aria-labelledby={`${formId}-label`}>
|
<div role="dialog" aria-labelledby={`${formId}-label`}>
|
||||||
<form onSubmit={this.handleSubmit}>
|
<form onSubmit={this.handleSubmit}>
|
||||||
<ValidationFormGroup
|
<Form.Group
|
||||||
for={formId}
|
controlId={formId}
|
||||||
invalid={error !== null}
|
isInvalid={error !== null}
|
||||||
invalidMessage={error}
|
|
||||||
>
|
>
|
||||||
<label className="edit-section-header" htmlFor={formId}>
|
<label className="edit-section-header" htmlFor={formId}>
|
||||||
{intl.formatMessage(messages['profile.education.education'])}
|
{intl.formatMessage(messages['profile.education.education'])}
|
||||||
@@ -90,7 +89,12 @@ class Education extends React.Component {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</ValidationFormGroup>
|
{error !== null && (
|
||||||
|
<Form.Control.Feedback hasIcon={false}>
|
||||||
|
{error}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
)}
|
||||||
|
</Form.Group>
|
||||||
<FormControls
|
<FormControls
|
||||||
visibilityId="visibilityLevelOfEducation"
|
visibilityId="visibilityLevelOfEducation"
|
||||||
saveState={saveState}
|
saveState={saveState}
|
||||||
|
|||||||
92
src/profile/forms/LearningGoal.jsx
Normal file
92
src/profile/forms/LearningGoal.jsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import get from 'lodash.get';
|
||||||
|
|
||||||
|
// Mock Data
|
||||||
|
import mockData from '../data/mock_data';
|
||||||
|
|
||||||
|
import messages from './LearningGoal.messages';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import EditableItemHeader from './elements/EditableItemHeader';
|
||||||
|
import SwitchContent from './elements/SwitchContent';
|
||||||
|
|
||||||
|
// Selectors
|
||||||
|
import { editableFormSelector } from '../data/selectors';
|
||||||
|
|
||||||
|
const LearningGoal = (props) => {
|
||||||
|
let { learningGoal, editMode, visibilityLearningGoal } = props;
|
||||||
|
const { intl } = props;
|
||||||
|
|
||||||
|
if (!learningGoal) {
|
||||||
|
learningGoal = mockData.learningGoal;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!editMode || editMode === 'empty') { // editMode defaults to 'empty', not sure why yet
|
||||||
|
editMode = mockData.editMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!visibilityLearningGoal) {
|
||||||
|
visibilityLearningGoal = mockData.visibilityLearningGoal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SwitchContent
|
||||||
|
className="mb-5"
|
||||||
|
expression={editMode}
|
||||||
|
cases={{
|
||||||
|
editable: (
|
||||||
|
<>
|
||||||
|
<EditableItemHeader
|
||||||
|
content={intl.formatMessage(messages['profile.learningGoal.learningGoal'])}
|
||||||
|
showVisibility={visibilityLearningGoal !== null}
|
||||||
|
visibility={visibilityLearningGoal}
|
||||||
|
/>
|
||||||
|
<p data-hj-suppress className="lead">
|
||||||
|
{intl.formatMessage(get(
|
||||||
|
messages,
|
||||||
|
`profile.learningGoal.options.${learningGoal}`,
|
||||||
|
messages['profile.learningGoal.options.something_else'],
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
static: (
|
||||||
|
<>
|
||||||
|
<EditableItemHeader content={intl.formatMessage(messages['profile.learningGoal.learningGoal'])} />
|
||||||
|
<p data-hj-suppress className="lead">
|
||||||
|
{intl.formatMessage(get(
|
||||||
|
messages,
|
||||||
|
`profile.learningGoal.options.${learningGoal}`,
|
||||||
|
messages['profile.learningGoal.options.something_else'],
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LearningGoal.propTypes = {
|
||||||
|
// From Selector
|
||||||
|
learningGoal: PropTypes.oneOf(['advance_career', 'start_career', 'learn_something_new', 'something_else']),
|
||||||
|
visibilityLearningGoal: PropTypes.oneOf(['private', 'all_users']),
|
||||||
|
editMode: PropTypes.oneOf(['editable', 'static']),
|
||||||
|
|
||||||
|
// i18n
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
LearningGoal.defaultProps = {
|
||||||
|
editMode: 'static',
|
||||||
|
learningGoal: null,
|
||||||
|
visibilityLearningGoal: 'private',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
editableFormSelector,
|
||||||
|
{},
|
||||||
|
)(injectIntl(LearningGoal));
|
||||||
31
src/profile/forms/LearningGoal.messages.jsx
Normal file
31
src/profile/forms/LearningGoal.messages.jsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
'profile.learningGoal.learningGoal': {
|
||||||
|
id: 'profile.learningGoal.learningGoal',
|
||||||
|
defaultMessage: 'Learning Goal',
|
||||||
|
description: 'A section of a user profile that displays their current learning goal.',
|
||||||
|
},
|
||||||
|
'profile.learningGoal.options.start_career': {
|
||||||
|
id: 'profile.learningGoal.options.start_career',
|
||||||
|
defaultMessage: 'I want to start my career',
|
||||||
|
description: 'Selected by user if their goal is to start their career.',
|
||||||
|
},
|
||||||
|
'profile.learningGoal.options.advance_career': {
|
||||||
|
id: 'profile.learningGoal.options.advance_career',
|
||||||
|
defaultMessage: 'I want to advance my career',
|
||||||
|
description: 'Selected by user if their goal is to advance their career.',
|
||||||
|
},
|
||||||
|
'profile.learningGoal.options.learn_something_new': {
|
||||||
|
id: 'profile.learningGoal.options.learn_something_new',
|
||||||
|
defaultMessage: 'I want to learn something new',
|
||||||
|
description: 'Selected by user if their goal is to learn something new.',
|
||||||
|
},
|
||||||
|
'profile.learningGoal.options.something_else': {
|
||||||
|
id: 'profile.learningGoal.options.something_else',
|
||||||
|
defaultMessage: 'Something else',
|
||||||
|
description: 'Selected by user if their goal is not described by the other choices.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
116
src/profile/forms/LearningGoal.test.jsx
Normal file
116
src/profile/forms/LearningGoal.test.jsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import configureMockStore from 'redux-mock-store';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { AppContext } from '@edx/frontend-platform/react';
|
||||||
|
import messages from '../../i18n';
|
||||||
|
|
||||||
|
import viewOwnProfileMockStore from '../__mocks__/viewOwnProfile.mockStore';
|
||||||
|
import savingEditedBioMockStore from '../__mocks__/savingEditedBio.mockStore';
|
||||||
|
|
||||||
|
import LearningGoal from './LearningGoal';
|
||||||
|
|
||||||
|
const mockStore = configureMockStore([thunk]);
|
||||||
|
|
||||||
|
// props to be passed down to LearningGoal component
|
||||||
|
const requiredLearningGoalProps = {
|
||||||
|
formId: 'learningGoal',
|
||||||
|
learningGoal: 'advance_career',
|
||||||
|
drafts: {},
|
||||||
|
visibilityLearningGoal: 'private',
|
||||||
|
editMode: 'static',
|
||||||
|
saveState: null,
|
||||||
|
error: null,
|
||||||
|
openHandler: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
configureI18n({
|
||||||
|
loggingService: { logError: jest.fn() },
|
||||||
|
config: {
|
||||||
|
ENVIRONMENT: 'production',
|
||||||
|
LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
|
||||||
|
},
|
||||||
|
messages,
|
||||||
|
});
|
||||||
|
|
||||||
|
const LearningGoalWrapper = (props) => {
|
||||||
|
const contextValue = useMemo(() => ({
|
||||||
|
authenticatedUser: { userId: null, username: null, administrator: false },
|
||||||
|
config: getConfig(),
|
||||||
|
}), []);
|
||||||
|
return (
|
||||||
|
<AppContext.Provider
|
||||||
|
value={contextValue}
|
||||||
|
>
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<Provider store={props.store}>
|
||||||
|
<LearningGoal {...props} />
|
||||||
|
</Provider>
|
||||||
|
</IntlProvider>
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LearningGoalWrapper.defaultProps = {
|
||||||
|
store: mockStore(viewOwnProfileMockStore),
|
||||||
|
};
|
||||||
|
|
||||||
|
LearningGoalWrapper.propTypes = {
|
||||||
|
store: PropTypes.shape({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const LearningGoalWrapperWithStore = ({ store }) => {
|
||||||
|
const contextValue = useMemo(() => ({
|
||||||
|
authenticatedUser: { userId: null, username: null, administrator: false },
|
||||||
|
config: getConfig(),
|
||||||
|
}), []);
|
||||||
|
return (
|
||||||
|
<AppContext.Provider
|
||||||
|
value={contextValue}
|
||||||
|
>
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<Provider store={mockStore(store)}>
|
||||||
|
<LearningGoal {...requiredLearningGoalProps} formId="learningGoal" />
|
||||||
|
</Provider>
|
||||||
|
</IntlProvider>
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LearningGoalWrapperWithStore.defaultProps = {
|
||||||
|
store: mockStore(savingEditedBioMockStore),
|
||||||
|
};
|
||||||
|
|
||||||
|
LearningGoalWrapperWithStore.propTypes = {
|
||||||
|
store: PropTypes.shape({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<LearningGoal />', () => {
|
||||||
|
describe('renders the current learning goal', () => {
|
||||||
|
it('renders "I want to advance my career"', () => {
|
||||||
|
render(
|
||||||
|
<LearningGoalWrapper
|
||||||
|
{...requiredLearningGoalProps}
|
||||||
|
formId="learningGoal"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText('I want to advance my career')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders "Something else"', () => {
|
||||||
|
requiredLearningGoalProps.learningGoal = 'something_else';
|
||||||
|
|
||||||
|
render(
|
||||||
|
<LearningGoalWrapper
|
||||||
|
{...requiredLearningGoalProps}
|
||||||
|
formId="learningGoal"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Something else')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { ValidationFormGroup } from '@edx/paragon';
|
import { Form } from '@openedx/paragon';
|
||||||
|
|
||||||
import messages from './PreferredLanguage.messages';
|
import messages from './PreferredLanguage.messages';
|
||||||
|
|
||||||
@@ -77,10 +77,9 @@ class PreferredLanguage extends React.Component {
|
|||||||
editing: (
|
editing: (
|
||||||
<div role="dialog" aria-labelledby={`${formId}-label`}>
|
<div role="dialog" aria-labelledby={`${formId}-label`}>
|
||||||
<form onSubmit={this.handleSubmit}>
|
<form onSubmit={this.handleSubmit}>
|
||||||
<ValidationFormGroup
|
<Form.Group
|
||||||
for={formId}
|
controlId={formId}
|
||||||
invalid={error !== null}
|
isInvalid={error !== null}
|
||||||
invalidMessage={error}
|
|
||||||
>
|
>
|
||||||
<label className="edit-section-header" htmlFor={formId}>
|
<label className="edit-section-header" htmlFor={formId}>
|
||||||
{intl.formatMessage(messages['profile.preferredlanguage.label'])}
|
{intl.formatMessage(messages['profile.preferredlanguage.label'])}
|
||||||
@@ -98,7 +97,12 @@ class PreferredLanguage extends React.Component {
|
|||||||
<option key={code} value={code}>{name}</option>
|
<option key={code} value={code}>{name}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</ValidationFormGroup>
|
{error !== null && (
|
||||||
|
<Form.Control.Feedback hasIcon={false}>
|
||||||
|
{error}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
)}
|
||||||
|
</Form.Group>
|
||||||
<FormControls
|
<FormControls
|
||||||
visibilityId="visibilityLanguageProficiencies"
|
visibilityId="visibilityLanguageProficiencies"
|
||||||
saveState={saveState}
|
saveState={saveState}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, Dropdown } from '@edx/paragon';
|
import { Button, Dropdown } from '@openedx/paragon';
|
||||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { ReactComponent as DefaultAvatar } from '../assets/avatar.svg';
|
import { ReactComponent as DefaultAvatar } from '../assets/avatar.svg';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { StatusAlert } from '@edx/paragon';
|
import { Alert } from '@openedx/paragon';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faTwitter, faFacebook, faLinkedin } from '@fortawesome/free-brands-svg-icons';
|
import { faTwitter, faFacebook, faLinkedin } from '@fortawesome/free-brands-svg-icons';
|
||||||
@@ -33,6 +33,108 @@ const platformDisplayInfo = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SocialLink = ({ url, name, platform }) => (
|
||||||
|
<a href={url} className="font-weight-bold">
|
||||||
|
<FontAwesomeIcon className="mr-2" icon={platformDisplayInfo[platform].icon} />
|
||||||
|
{name}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
|
SocialLink.propTypes = {
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
platform: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditableListItem = ({
|
||||||
|
url, platform, onClickEmptyContent, name,
|
||||||
|
}) => {
|
||||||
|
const linkDisplay = url ? (
|
||||||
|
<SocialLink name={name} url={url} platform={platform} />
|
||||||
|
) : (
|
||||||
|
<EmptyContent onClick={onClickEmptyContent}>Add {name}</EmptyContent>
|
||||||
|
);
|
||||||
|
|
||||||
|
return <li className="form-group">{linkDisplay}</li>;
|
||||||
|
};
|
||||||
|
|
||||||
|
EditableListItem.propTypes = {
|
||||||
|
url: PropTypes.string,
|
||||||
|
platform: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
onClickEmptyContent: PropTypes.func,
|
||||||
|
};
|
||||||
|
EditableListItem.defaultProps = {
|
||||||
|
url: null,
|
||||||
|
onClickEmptyContent: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditingListItem = ({
|
||||||
|
platform, name, value, onChange, error,
|
||||||
|
}) => (
|
||||||
|
<li className="form-group">
|
||||||
|
<label htmlFor={`social-${platform}`}>{name}</label>
|
||||||
|
<input
|
||||||
|
className={classNames('form-control', { 'is-invalid': Boolean(error) })}
|
||||||
|
type="text"
|
||||||
|
id={`social-${platform}`}
|
||||||
|
name={platform}
|
||||||
|
value={value || ''}
|
||||||
|
onChange={onChange}
|
||||||
|
aria-describedby="social-error-feedback"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
EditingListItem.propTypes = {
|
||||||
|
platform: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.string,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
error: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
EditingListItem.defaultProps = {
|
||||||
|
value: null,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmptyListItem = ({ onClick, name }) => (
|
||||||
|
<li className="mb-4">
|
||||||
|
<EmptyContent onClick={onClick}>
|
||||||
|
<FormattedMessage
|
||||||
|
id="profile.sociallinks.add"
|
||||||
|
defaultMessage="Add {network}"
|
||||||
|
values={{
|
||||||
|
network: name,
|
||||||
|
}}
|
||||||
|
description="{network} is the name of a social network such as Facebook or Twitter"
|
||||||
|
/>
|
||||||
|
</EmptyContent>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
EmptyListItem.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const StaticListItem = ({ name, url, platform }) => (
|
||||||
|
<li className="mb-2">
|
||||||
|
<SocialLink name={name} url={url} platform={platform} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
StaticListItem.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string,
|
||||||
|
platform: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
StaticListItem.defaultProps = {
|
||||||
|
url: null,
|
||||||
|
};
|
||||||
|
|
||||||
class SocialLinks extends React.Component {
|
class SocialLinks extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -65,19 +167,6 @@ class SocialLinks extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeWithDrafts(newSocialLink) {
|
|
||||||
const knownPlatforms = ['twitter', 'facebook', 'linkedin'];
|
|
||||||
const updated = [];
|
|
||||||
knownPlatforms.forEach((platform) => {
|
|
||||||
if (newSocialLink.platform === platform) {
|
|
||||||
updated.push(newSocialLink);
|
|
||||||
} else if (this.props.draftSocialLinksByPlatform[platform] !== undefined) {
|
|
||||||
updated.push(this.props.draftSocialLinksByPlatform[platform]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit(e) {
|
handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.submitHandler(this.props.formId);
|
this.props.submitHandler(this.props.formId);
|
||||||
@@ -91,6 +180,19 @@ class SocialLinks extends React.Component {
|
|||||||
this.props.openHandler(this.props.formId);
|
this.props.openHandler(this.props.formId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mergeWithDrafts(newSocialLink) {
|
||||||
|
const knownPlatforms = ['twitter', 'facebook', 'linkedin'];
|
||||||
|
const updated = [];
|
||||||
|
knownPlatforms.forEach((platform) => {
|
||||||
|
if (newSocialLink.platform === platform) {
|
||||||
|
updated.push(newSocialLink);
|
||||||
|
} else if (this.props.draftSocialLinksByPlatform[platform] !== undefined) {
|
||||||
|
updated.push(this.props.draftSocialLinksByPlatform[platform]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
socialLinks, visibilitySocialLinks, editMode, saveState, error, intl,
|
socialLinks, visibilitySocialLinks, editMode, saveState, error, intl,
|
||||||
@@ -158,14 +260,19 @@ class SocialLinks extends React.Component {
|
|||||||
),
|
),
|
||||||
editing: (
|
editing: (
|
||||||
<div role="dialog" aria-labelledby="social-links-label">
|
<div role="dialog" aria-labelledby="social-links-label">
|
||||||
<form onSubmit={this.handleSubmit}>
|
<form aria-labelledby="editing-form" onSubmit={this.handleSubmit}>
|
||||||
<EditableItemHeader
|
<EditableItemHeader
|
||||||
headingId="social-links-label"
|
headingId="social-links-label"
|
||||||
content={intl.formatMessage(messages['profile.sociallinks.social.links'])}
|
content={intl.formatMessage(messages['profile.sociallinks.social.links'])}
|
||||||
/>
|
/>
|
||||||
{/* TODO: Replace this alert with per-field errors. Needs API update. */}
|
{/* TODO: Replace this alert with per-field errors. Needs API update. */}
|
||||||
<div id="social-error-feedback">
|
<div id="social-error-feedback">
|
||||||
{error !== null ? <StatusAlert alertType="danger" dialog={error} dismissible={false} open /> : null}
|
{error !== null
|
||||||
|
? (
|
||||||
|
<Alert variant="danger" dismissible={false} show>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<ul className="list-unstyled">
|
<ul className="list-unstyled">
|
||||||
{socialLinks.map(({ platform, socialLink }) => (
|
{socialLinks.map(({ platform, socialLink }) => (
|
||||||
@@ -238,113 +345,3 @@ export default connect(
|
|||||||
editableFormSelector,
|
editableFormSelector,
|
||||||
{},
|
{},
|
||||||
)(injectIntl(SocialLinks));
|
)(injectIntl(SocialLinks));
|
||||||
|
|
||||||
function SocialLink({ url, name, platform }) {
|
|
||||||
return (
|
|
||||||
<a href={url} className="font-weight-bold">
|
|
||||||
<FontAwesomeIcon className="mr-2" icon={platformDisplayInfo[platform].icon} />
|
|
||||||
{name}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SocialLink.propTypes = {
|
|
||||||
url: PropTypes.string.isRequired,
|
|
||||||
platform: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
function EditableListItem({
|
|
||||||
url, platform, onClickEmptyContent, name,
|
|
||||||
}) {
|
|
||||||
const linkDisplay = url ? (
|
|
||||||
<SocialLink name={name} url={url} platform={platform} />
|
|
||||||
) : (
|
|
||||||
<EmptyContent onClick={onClickEmptyContent}>Add {name}</EmptyContent>
|
|
||||||
);
|
|
||||||
|
|
||||||
return <li className="form-group">{linkDisplay}</li>;
|
|
||||||
}
|
|
||||||
|
|
||||||
EditableListItem.propTypes = {
|
|
||||||
url: PropTypes.string,
|
|
||||||
platform: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
onClickEmptyContent: PropTypes.func,
|
|
||||||
};
|
|
||||||
EditableListItem.defaultProps = {
|
|
||||||
url: null,
|
|
||||||
onClickEmptyContent: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
function EditingListItem({
|
|
||||||
platform, name, value, onChange, error,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<li className="form-group">
|
|
||||||
<label htmlFor={`social-${platform}`}>{name}</label>
|
|
||||||
<input
|
|
||||||
className={classNames('form-control', { 'is-invalid': Boolean(error) })}
|
|
||||||
type="text"
|
|
||||||
id={`social-${platform}`}
|
|
||||||
name={platform}
|
|
||||||
value={value || ''}
|
|
||||||
onChange={onChange}
|
|
||||||
aria-describedby="social-error-feedback"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditingListItem.propTypes = {
|
|
||||||
platform: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
error: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
EditingListItem.defaultProps = {
|
|
||||||
value: null,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
function EmptyListItem({ onClick, name }) {
|
|
||||||
return (
|
|
||||||
<li className="mb-4">
|
|
||||||
<EmptyContent onClick={onClick}>
|
|
||||||
<FormattedMessage
|
|
||||||
id="profile.sociallinks.add"
|
|
||||||
defaultMessage="Add {network}"
|
|
||||||
values={{
|
|
||||||
network: name,
|
|
||||||
}}
|
|
||||||
description="{network} is the name of a social network such as Facebook or Twitter"
|
|
||||||
/>
|
|
||||||
</EmptyContent>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EmptyListItem.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
onClick: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
function StaticListItem({ name, url, platform }) {
|
|
||||||
return (
|
|
||||||
<li className="mb-2">
|
|
||||||
<SocialLink name={name} url={url} platform={platform} />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
StaticListItem.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
url: PropTypes.string,
|
|
||||||
platform: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
StaticListItem.defaultProps = {
|
|
||||||
url: null,
|
|
||||||
};
|
|
||||||
|
|||||||
165
src/profile/forms/SocialLinks.test.jsx
Normal file
165
src/profile/forms/SocialLinks.test.jsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { render, fireEvent, screen } from '@testing-library/react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import configureMockStore from 'redux-mock-store';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { AppContext } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
|
import SocialLinks from './SocialLinks';
|
||||||
|
import * as savingEditedBio from '../__mocks__/savingEditedBio.mockStore';
|
||||||
|
import messages from '../../i18n';
|
||||||
|
|
||||||
|
const mockStore = configureMockStore([thunk]);
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
formId: 'socialLinks',
|
||||||
|
socialLinks: [
|
||||||
|
{
|
||||||
|
platform: 'facebook',
|
||||||
|
socialLink: 'https://www.facebook.com/aloha',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: 'twitter',
|
||||||
|
socialLink: 'https://www.twitter.com/ALOHA',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
drafts: {},
|
||||||
|
visibilitySocialLinks: 'private',
|
||||||
|
editMode: 'static',
|
||||||
|
saveState: null,
|
||||||
|
error: null,
|
||||||
|
changeHandler: jest.fn(),
|
||||||
|
submitHandler: jest.fn(),
|
||||||
|
closeHandler: jest.fn(),
|
||||||
|
openHandler: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
configureI18n({
|
||||||
|
loggingService: { logError: jest.fn() },
|
||||||
|
config: {
|
||||||
|
ENVIRONMENT: 'production',
|
||||||
|
LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
|
||||||
|
},
|
||||||
|
messages,
|
||||||
|
});
|
||||||
|
|
||||||
|
const SocialLinksWrapper = (props) => {
|
||||||
|
const contextValue = useMemo(() => ({
|
||||||
|
authenticatedUser: { userId: null, username: null, administrator: false },
|
||||||
|
config: getConfig(),
|
||||||
|
}), []);
|
||||||
|
return (
|
||||||
|
<AppContext.Provider
|
||||||
|
value={contextValue}
|
||||||
|
>
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<Provider store={props.store}>
|
||||||
|
<SocialLinks {...props} />
|
||||||
|
</Provider>
|
||||||
|
</IntlProvider>
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SocialLinksWrapper.defaultProps = {
|
||||||
|
store: mockStore(savingEditedBio),
|
||||||
|
};
|
||||||
|
|
||||||
|
SocialLinksWrapper.propTypes = {
|
||||||
|
store: PropTypes.shape({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const SocialLinksWrapperWithStore = ({ store }) => {
|
||||||
|
const contextValue = useMemo(() => ({
|
||||||
|
authenticatedUser: { userId: null, username: null, administrator: false },
|
||||||
|
config: getConfig(),
|
||||||
|
}), []);
|
||||||
|
return (
|
||||||
|
<AppContext.Provider
|
||||||
|
value={contextValue}
|
||||||
|
>
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<Provider store={mockStore(store)}>
|
||||||
|
<SocialLinks {...defaultProps} formId="bio" />
|
||||||
|
</Provider>
|
||||||
|
</IntlProvider>
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SocialLinksWrapperWithStore.defaultProps = {
|
||||||
|
store: mockStore(savingEditedBio),
|
||||||
|
};
|
||||||
|
|
||||||
|
SocialLinksWrapperWithStore.propTypes = {
|
||||||
|
store: PropTypes.shape({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<SocialLinks />', () => {
|
||||||
|
['certificates', 'bio', 'goals', 'socialLinks'].forEach(editMode => (
|
||||||
|
it(`calls social links with edit mode ${editMode}`, () => {
|
||||||
|
const component = <SocialLinksWrapper {...defaultProps} formId={editMode} />;
|
||||||
|
const { container: tree } = render(component);
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
|
it('calls social links with editing', () => {
|
||||||
|
const changeHandler = jest.fn();
|
||||||
|
const submitHandler = jest.fn();
|
||||||
|
const closeHandler = jest.fn();
|
||||||
|
const { container } = render(
|
||||||
|
<SocialLinksWrapper
|
||||||
|
{...defaultProps}
|
||||||
|
formId="bio"
|
||||||
|
changeHandler={changeHandler}
|
||||||
|
submitHandler={submitHandler}
|
||||||
|
closeHandler={closeHandler}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { platform } = defaultProps.socialLinks[0];
|
||||||
|
const inputField = container.querySelector(`#social-${platform}`);
|
||||||
|
fireEvent.change(inputField, { target: { value: 'test', name: platform } });
|
||||||
|
expect(changeHandler).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const selectElement = container.querySelector('#visibilitySocialLinks');
|
||||||
|
expect(selectElement.value).toBe('private');
|
||||||
|
fireEvent.change(selectElement, { target: { value: 'all_users', name: 'visibilitySocialLinks' } });
|
||||||
|
expect(changeHandler).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
fireEvent.submit(container.querySelector('[aria-labelledby="editing-form"]'));
|
||||||
|
expect(submitHandler).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
||||||
|
expect(closeHandler).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls social links with static', () => {
|
||||||
|
const openHandler = jest.fn();
|
||||||
|
render(
|
||||||
|
<SocialLinksWrapper
|
||||||
|
{...defaultProps}
|
||||||
|
formId="goals"
|
||||||
|
openHandler={openHandler}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const addFacebookButton = screen.getByRole('button', { name: 'Add Facebook' });
|
||||||
|
fireEvent.click(addFacebookButton);
|
||||||
|
|
||||||
|
expect(openHandler).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls social links with error', () => {
|
||||||
|
const newStore = JSON.parse(JSON.stringify(savingEditedBio));
|
||||||
|
newStore.profilePage.errors.bio = { userMessage: 'error' };
|
||||||
|
|
||||||
|
const { container } = render(<SocialLinksWrapperWithStore store={newStore} />);
|
||||||
|
|
||||||
|
const alertDanger = container.querySelector('.alert-danger');
|
||||||
|
expect(alertDanger).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
504
src/profile/forms/__snapshots__/SocialLinks.test.jsx.snap
Normal file
504
src/profile/forms/__snapshots__/SocialLinks.test.jsx.snap
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<SocialLinks /> calls social links with edit mode bio 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="pgn-transition-replace-group position-relative mb-5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="padding: .1px 0px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-labelledby="social-links-label"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
aria-labelledby="editing-form"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="editable-item-header mb-2"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="edit-section-header"
|
||||||
|
id="social-links-label"
|
||||||
|
>
|
||||||
|
Social Links
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="social-error-feedback"
|
||||||
|
/>
|
||||||
|
<ul
|
||||||
|
class="list-unstyled"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="form-group"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
for="social-facebook"
|
||||||
|
>
|
||||||
|
Facebook
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
aria-describedby="social-error-feedback"
|
||||||
|
class="form-control"
|
||||||
|
id="social-facebook"
|
||||||
|
name="facebook"
|
||||||
|
type="text"
|
||||||
|
value="https://www.facebook.com/aloha"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="form-group"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
for="social-twitter"
|
||||||
|
>
|
||||||
|
Twitter
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
aria-describedby="social-error-feedback"
|
||||||
|
class="form-control"
|
||||||
|
id="social-twitter"
|
||||||
|
name="twitter"
|
||||||
|
type="text"
|
||||||
|
value="https://www.twitter.com/ALOHA"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div
|
||||||
|
class="d-flex flex-row-reverse flex-wrap justify-content-end align-items-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="form-group d-flex flex-wrap"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="col-form-label"
|
||||||
|
for="visibilitySocialLinks"
|
||||||
|
>
|
||||||
|
Who can see this:
|
||||||
|
</label>
|
||||||
|
<span
|
||||||
|
class="d-flex align-items-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="d-inline-block ml-1 mr-2"
|
||||||
|
style="width: 1.5rem;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="svg-inline--fa fa-eye-slash "
|
||||||
|
data-icon="eye-slash"
|
||||||
|
data-prefix="far"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 640 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zm151 118.3C226 97.7 269.5 80 320 80c65.2 0 118.8 29.6 159.9 67.7C518.4 183.5 545 226 558.6 256c-12.6 28-36.6 66.8-70.9 100.9l-53.8-42.2c9.1-17.6 14.2-37.5 14.2-58.7c0-70.7-57.3-128-128-128c-32.2 0-61.7 11.9-84.2 31.5l-46.1-36.1zM394.9 284.2l-81.5-63.9c4.2-8.5 6.6-18.2 6.6-28.3c0-5.5-.7-10.9-2-16c.7 0 1.3 0 2 0c44.2 0 80 35.8 80 80c0 9.9-1.8 19.4-5.1 28.2zm9.4 130.3C378.8 425.4 350.7 432 320 432c-65.2 0-118.8-29.6-159.9-67.7C121.6 328.5 95 286 81.4 256c8.3-18.4 21.5-41.5 39.4-64.8L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5l-41.9-33zM192 256c0 70.7 57.3 128 128 128c13.3 0 26.1-2 38.2-5.8L302 334c-23.5-5.4-43.1-21.2-53.7-42.3l-56.1-44.2c-.2 2.8-.3 5.6-.3 8.5z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
class="d-inline-block form-control"
|
||||||
|
id="visibilitySocialLinks"
|
||||||
|
name="visibilitySocialLinks"
|
||||||
|
type="select"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="private"
|
||||||
|
>
|
||||||
|
Just me
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="all_users"
|
||||||
|
>
|
||||||
|
Everyone on localhost
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="form-group flex-shrink-0 flex-grow-1"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-disabled="false"
|
||||||
|
aria-live="assertive"
|
||||||
|
class="pgn__stateful-btn pgn__stateful-btn-state-pending btn btn-primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="d-flex align-items-center justify-content-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__stateful-btn-icon"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pgn__icon icon-spin"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
height="24"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M22 12A10 10 0 1 1 6.122 3.91l1.176 1.618A8 8 0 1 0 20 12h2Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Saving
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-link"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<SocialLinks /> calls social links with edit mode certificates 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="pgn-transition-replace-group position-relative mb-5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="padding: .1px 0px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="editable-item-header mb-2"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="edit-section-header"
|
||||||
|
>
|
||||||
|
Social Links
|
||||||
|
<button
|
||||||
|
class="float-right px-0 btn btn-link btn-sm"
|
||||||
|
style="margin-top: -.35rem;"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="svg-inline--fa fa-pencil mr-1"
|
||||||
|
data-icon="pencil"
|
||||||
|
data-prefix="fas"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1 0 32c0 8.8 7.2 16 16 16l32 0zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
class="mb-0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="ml-auto small text-muted"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="svg-inline--fa fa-eye-slash "
|
||||||
|
data-icon="eye-slash"
|
||||||
|
data-prefix="far"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 640 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zm151 118.3C226 97.7 269.5 80 320 80c65.2 0 118.8 29.6 159.9 67.7C518.4 183.5 545 226 558.6 256c-12.6 28-36.6 66.8-70.9 100.9l-53.8-42.2c9.1-17.6 14.2-37.5 14.2-58.7c0-70.7-57.3-128-128-128c-32.2 0-61.7 11.9-84.2 31.5l-46.1-36.1zM394.9 284.2l-81.5-63.9c4.2-8.5 6.6-18.2 6.6-28.3c0-5.5-.7-10.9-2-16c.7 0 1.3 0 2 0c44.2 0 80 35.8 80 80c0 9.9-1.8 19.4-5.1 28.2zm9.4 130.3C378.8 425.4 350.7 432 320 432c-65.2 0-118.8-29.6-159.9-67.7C121.6 328.5 95 286 81.4 256c8.3-18.4 21.5-41.5 39.4-64.8L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5l-41.9-33zM192 256c0 70.7 57.3 128 128 128c13.3 0 26.1-2 38.2-5.8L302 334c-23.5-5.4-43.1-21.2-53.7-42.3l-56.1-44.2c-.2 2.8-.3 5.6-.3 8.5z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
Just me
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="list-unstyled"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="form-group"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="font-weight-bold"
|
||||||
|
href="https://www.facebook.com/aloha"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="svg-inline--fa fa-facebook mr-2"
|
||||||
|
data-icon="facebook"
|
||||||
|
data-prefix="fab"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 256C512 114.6 397.4 0 256 0S0 114.6 0 256C0 376 82.7 476.8 194.2 504.5V334.2H141.4V256h52.8V222.3c0-87.1 39.4-127.5 125-127.5c16.2 0 44.2 3.2 55.7 6.4V172c-6-.6-16.5-1-29.6-1c-42 0-58.2 15.9-58.2 57.2V256h83.6l-14.4 78.2H287V510.1C413.8 494.8 512 386.9 512 256h0z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Facebook
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="form-group"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="font-weight-bold"
|
||||||
|
href="https://www.twitter.com/ALOHA"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="svg-inline--fa fa-twitter mr-2"
|
||||||
|
data-icon="twitter"
|
||||||
|
data-prefix="fab"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Twitter
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<SocialLinks /> calls social links with edit mode goals 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="pgn-transition-replace-group position-relative mb-5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="padding: .1px 0px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="editable-item-header mb-2"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="edit-section-header"
|
||||||
|
>
|
||||||
|
Social Links
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="list-unstyled"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="pl-0 text-left btn btn-link"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="svg-inline--fa fa-plus fa-xs mr-2"
|
||||||
|
data-icon="plus"
|
||||||
|
data-prefix="fas"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Add Facebook
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="pl-0 text-left btn btn-link"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="svg-inline--fa fa-plus fa-xs mr-2"
|
||||||
|
data-icon="plus"
|
||||||
|
data-prefix="fas"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Add Twitter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<SocialLinks /> calls social links with edit mode socialLinks 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="pgn-transition-replace-group position-relative mb-5"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="padding: .1px 0px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="editable-item-header mb-2"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="edit-section-header"
|
||||||
|
>
|
||||||
|
Social Links
|
||||||
|
<button
|
||||||
|
class="float-right px-0 btn btn-link btn-sm"
|
||||||
|
style="margin-top: -.35rem;"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="svg-inline--fa fa-pencil mr-1"
|
||||||
|
data-icon="pencil"
|
||||||
|
data-prefix="fas"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1 0 32c0 8.8 7.2 16 16 16l32 0zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
class="mb-0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="ml-auto small text-muted"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="svg-inline--fa fa-eye-slash "
|
||||||
|
data-icon="eye-slash"
|
||||||
|
data-prefix="far"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 640 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zm151 118.3C226 97.7 269.5 80 320 80c65.2 0 118.8 29.6 159.9 67.7C518.4 183.5 545 226 558.6 256c-12.6 28-36.6 66.8-70.9 100.9l-53.8-42.2c9.1-17.6 14.2-37.5 14.2-58.7c0-70.7-57.3-128-128-128c-32.2 0-61.7 11.9-84.2 31.5l-46.1-36.1zM394.9 284.2l-81.5-63.9c4.2-8.5 6.6-18.2 6.6-28.3c0-5.5-.7-10.9-2-16c.7 0 1.3 0 2 0c44.2 0 80 35.8 80 80c0 9.9-1.8 19.4-5.1 28.2zm9.4 130.3C378.8 425.4 350.7 432 320 432c-65.2 0-118.8-29.6-159.9-67.7C121.6 328.5 95 286 81.4 256c8.3-18.4 21.5-41.5 39.4-64.8L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5l-41.9-33zM192 256c0 70.7 57.3 128 128 128c13.3 0 26.1-2 38.2-5.8L302 334c-23.5-5.4-43.1-21.2-53.7-42.3l-56.1-44.2c-.2 2.8-.3 5.6-.3 8.5z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
Just me
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="list-unstyled"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="form-group"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="font-weight-bold"
|
||||||
|
href="https://www.facebook.com/aloha"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="svg-inline--fa fa-facebook mr-2"
|
||||||
|
data-icon="facebook"
|
||||||
|
data-prefix="fab"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 256C512 114.6 397.4 0 256 0S0 114.6 0 256C0 376 82.7 476.8 194.2 504.5V334.2H141.4V256h52.8V222.3c0-87.1 39.4-127.5 125-127.5c16.2 0 44.2 3.2 55.7 6.4V172c-6-.6-16.5-1-29.6-1c-42 0-58.2 15.9-58.2 57.2V256h83.6l-14.4 78.2H287V510.1C413.8 494.8 512 386.9 512 256h0z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Facebook
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="form-group"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="font-weight-bold"
|
||||||
|
href="https://www.twitter.com/ALOHA"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="svg-inline--fa fa-twitter mr-2"
|
||||||
|
data-icon="twitter"
|
||||||
|
data-prefix="fab"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Twitter
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -3,26 +3,24 @@ import PropTypes from 'prop-types';
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { Button } from '@edx/paragon';
|
import { Button } from '@openedx/paragon';
|
||||||
|
|
||||||
import messages from './EditButton.messages';
|
import messages from './EditButton.messages';
|
||||||
|
|
||||||
function EditButton({
|
const EditButton = ({
|
||||||
onClick, className, style, intl,
|
onClick, className, style, intl,
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<Button
|
||||||
<Button
|
variant="link"
|
||||||
variant="link"
|
size="sm"
|
||||||
size="sm"
|
className={className}
|
||||||
className={className}
|
onClick={onClick}
|
||||||
onClick={onClick}
|
style={style}
|
||||||
style={style}
|
>
|
||||||
>
|
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />
|
||||||
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />
|
{intl.formatMessage(messages['profile.editbutton.edit'])}
|
||||||
{intl.formatMessage(messages['profile.editbutton.edit'])}
|
</Button>
|
||||||
</Button>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(EditButton);
|
export default injectIntl(EditButton);
|
||||||
|
|
||||||
|
|||||||
@@ -4,26 +4,22 @@ import PropTypes from 'prop-types';
|
|||||||
import EditButton from './EditButton';
|
import EditButton from './EditButton';
|
||||||
import { Visibility } from './Visibility';
|
import { Visibility } from './Visibility';
|
||||||
|
|
||||||
function EditableItemHeader({
|
const EditableItemHeader = ({
|
||||||
content,
|
content,
|
||||||
showVisibility,
|
showVisibility,
|
||||||
visibility,
|
visibility,
|
||||||
showEditButton,
|
showEditButton,
|
||||||
onClickEdit,
|
onClickEdit,
|
||||||
headingId,
|
headingId,
|
||||||
}) {
|
}) => (
|
||||||
return (
|
<div className="editable-item-header mb-2">
|
||||||
<>
|
<h2 className="edit-section-header" id={headingId}>
|
||||||
<div className="editable-item-header mb-2">
|
{content}
|
||||||
<h2 className="edit-section-header" id={headingId}>
|
{showEditButton ? <EditButton style={{ marginTop: '-.35rem' }} className="float-right px-0" onClick={onClickEdit} /> : null}
|
||||||
{content}
|
</h2>
|
||||||
{showEditButton ? <EditButton style={{ marginTop: '-.35rem' }} className="float-right px-0" onClick={onClickEdit} /> : null}
|
{showVisibility ? <p className="mb-0"><Visibility to={visibility} /></p> : null}
|
||||||
</h2>
|
</div>
|
||||||
{showVisibility ? <p className="mb-0"><Visibility to={visibility} /></p> : null}
|
);
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EditableItemHeader;
|
export default EditableItemHeader;
|
||||||
|
|
||||||
|
|||||||
@@ -3,24 +3,22 @@ import PropTypes from 'prop-types';
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
function EmptyContent({ children, onClick, showPlusIcon }) {
|
const EmptyContent = ({ children, onClick, showPlusIcon }) => (
|
||||||
return (
|
<div>
|
||||||
<div>
|
{onClick ? (
|
||||||
{onClick ? (
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className="pl-0 text-left btn btn-link"
|
||||||
className="pl-0 text-left btn btn-link"
|
onClick={onClick}
|
||||||
onClick={onClick}
|
onKeyDown={(e) => { if (e.key === 'Enter') { onClick(); } }}
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter') { onClick(); } }}
|
tabIndex={0}
|
||||||
tabIndex={0}
|
>
|
||||||
>
|
{showPlusIcon ? <FontAwesomeIcon size="xs" className="mr-2" icon={faPlus} /> : null}
|
||||||
{showPlusIcon ? <FontAwesomeIcon size="xs" className="mr-2" icon={faPlus} /> : null}
|
{children}
|
||||||
{children}
|
</button>
|
||||||
</button>
|
) : children}
|
||||||
) : children}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EmptyContent;
|
export default EmptyContent;
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, StatefulButton } from '@edx/paragon';
|
import { Button, StatefulButton } from '@openedx/paragon';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import messages from './FormControls.messages';
|
import messages from './FormControls.messages';
|
||||||
|
|
||||||
import { VisibilitySelect } from './Visibility';
|
import { VisibilitySelect } from './Visibility';
|
||||||
|
|
||||||
function FormControls({
|
const FormControls = ({
|
||||||
cancelHandler, changeHandler, visibility, visibilityId, saveState, intl,
|
cancelHandler, changeHandler, visibility, visibilityId, saveState, intl,
|
||||||
}) {
|
}) => {
|
||||||
// Eliminate error/failed state for save button
|
// Eliminate error/failed state for save button
|
||||||
const buttonState = saveState === 'error' ? null : saveState;
|
const buttonState = saveState === 'error' ? null : saveState;
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ function FormControls({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default injectIntl(FormControls);
|
export default injectIntl(FormControls);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { TransitionReplace } from '@edx/paragon';
|
import { TransitionReplace } from '@openedx/paragon';
|
||||||
|
|
||||||
const onChildExit = (htmlNode) => {
|
const onChildExit = (htmlNode) => {
|
||||||
// If the leaving child has focus, take control and redirect it
|
// If the leaving child has focus, take control and redirect it
|
||||||
@@ -22,7 +22,7 @@ const onChildExit = (htmlNode) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function SwitchContent({ expression, cases, className }) {
|
const SwitchContent = ({ expression, cases, className }) => {
|
||||||
const getContent = (caseKey) => {
|
const getContent = (caseKey) => {
|
||||||
if (cases[caseKey]) {
|
if (cases[caseKey]) {
|
||||||
if (typeof cases[caseKey] === 'string') {
|
if (typeof cases[caseKey] === 'string') {
|
||||||
@@ -48,7 +48,7 @@ function SwitchContent({ expression, cases, className }) {
|
|||||||
{getContent(expression)}
|
{getContent(expression)}
|
||||||
</TransitionReplace>
|
</TransitionReplace>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
SwitchContent.propTypes = {
|
SwitchContent.propTypes = {
|
||||||
expression: PropTypes.string,
|
expression: PropTypes.string,
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faEyeSlash, faEye } from '@fortawesome/free-regular-svg-icons';
|
import { faEyeSlash, faEye } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
|
||||||
import messages from './Visibility.messages';
|
import messages from './Visibility.messages';
|
||||||
|
|
||||||
function Visibility({ to, intl }) {
|
const Visibility = ({ to, intl }) => {
|
||||||
const icon = to === 'private' ? faEyeSlash : faEye;
|
const icon = to === 'private' ? faEyeSlash : faEye;
|
||||||
const label = to === 'private'
|
const label = to === 'private'
|
||||||
? intl.formatMessage(messages['profile.visibility.who.just.me'])
|
? intl.formatMessage(messages['profile.visibility.who.just.me'])
|
||||||
: intl.formatMessage(messages['profile.visibility.who.everyone']);
|
: intl.formatMessage(messages['profile.visibility.who.everyone'], { siteName: getConfig().SITE_NAME });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="ml-auto small text-muted">
|
<span className="ml-auto small text-muted">
|
||||||
<FontAwesomeIcon icon={icon} /> {label}
|
<FontAwesomeIcon icon={icon} /> {label}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
Visibility.propTypes = {
|
Visibility.propTypes = {
|
||||||
to: PropTypes.oneOf(['private', 'all_users']),
|
to: PropTypes.oneOf(['private', 'all_users']),
|
||||||
@@ -29,7 +30,7 @@ Visibility.defaultProps = {
|
|||||||
to: 'private',
|
to: 'private',
|
||||||
};
|
};
|
||||||
|
|
||||||
function VisibilitySelect({ intl, className, ...props }) {
|
const VisibilitySelect = ({ intl, className, ...props }) => {
|
||||||
const { value } = props;
|
const { value } = props;
|
||||||
const icon = value === 'private' ? faEyeSlash : faEye;
|
const icon = value === 'private' ? faEyeSlash : faEye;
|
||||||
|
|
||||||
@@ -38,17 +39,17 @@ function VisibilitySelect({ intl, className, ...props }) {
|
|||||||
<span className="d-inline-block ml-1 mr-2" style={{ width: '1.5rem' }}>
|
<span className="d-inline-block ml-1 mr-2" style={{ width: '1.5rem' }}>
|
||||||
<FontAwesomeIcon icon={icon} />
|
<FontAwesomeIcon icon={icon} />
|
||||||
</span>
|
</span>
|
||||||
<select className="d-inline-block w-auto form-control" {...props}>
|
<select className="d-inline-block form-control" {...props}>
|
||||||
<option key="private" value="private">
|
<option key="private" value="private">
|
||||||
{intl.formatMessage(messages['profile.visibility.who.just.me'])}
|
{intl.formatMessage(messages['profile.visibility.who.just.me'])}
|
||||||
</option>
|
</option>
|
||||||
<option key="all_users" value="all_users">
|
<option key="all_users" value="all_users">
|
||||||
{intl.formatMessage(messages['profile.visibility.who.everyone'])}
|
{intl.formatMessage(messages['profile.visibility.who.everyone'], { siteName: getConfig().SITE_NAME })}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
VisibilitySelect.propTypes = {
|
VisibilitySelect.propTypes = {
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
'profile.visibility.who.everyone': {
|
'profile.visibility.who.everyone': {
|
||||||
id: 'profile.visibility.who.everyone',
|
id: 'profile.visibility.who.everyone',
|
||||||
defaultMessage: 'Everyone on edX',
|
defaultMessage: 'Everyone on {siteName}',
|
||||||
description: 'What users can see this area?',
|
description: 'What users can see this area?',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,9 +23,31 @@
|
|||||||
background-size: auto 85%;
|
background-size: auto 85%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.username-description {
|
||||||
|
width: auto;
|
||||||
|
position: absolute;
|
||||||
|
left: 1.5rem;
|
||||||
|
top: 5.25rem;
|
||||||
|
color: var(--pgn-color-gray-500);
|
||||||
|
line-height: 0.9rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-left: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-2rem {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-visibility-off {
|
||||||
|
height: 1rem;
|
||||||
|
color: var(--pgn-color-gray-500);
|
||||||
|
}
|
||||||
|
|
||||||
.profile-page {
|
.profile-page {
|
||||||
.edit-section-header {
|
.edit-section-header {
|
||||||
@extend .h6;
|
font-size: var(--pgn-typography-font-size-h6-base);
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
@@ -33,11 +55,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
label.edit-section-header {
|
label.edit-section-header {
|
||||||
margin-bottom: $spacer * .5;
|
margin-bottom: calc(var(--pgn-spacing-spacer-base) * .5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar-wrap {
|
.profile-avatar-wrap {
|
||||||
@include media-breakpoint-up(md) {
|
@media (--pgn-size-breakpoint-min-width-md) {
|
||||||
max-width: 12rem;
|
max-width: 12rem;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
margin-top: -8rem;
|
margin-top: -8rem;
|
||||||
@@ -55,25 +77,25 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@media (--pgn-size-breakpoint-min-width-md) {
|
||||||
background: linear-gradient(to top, rgba(0,0,0,.65) 4rem, rgba(0,0,0,0) 4rem);
|
background: linear-gradient(to top, rgba(0,0,0,.65) 4rem, rgba(0,0,0,0) 4rem);
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@include media-breakpoint-up(md) {
|
@media (--pgn-size-breakpoint-min-width-md) {
|
||||||
margin-bottom: 1.2rem;
|
margin-bottom: 1.2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
@include media-breakpoint-up(md) {
|
@media (--pgn-size-breakpoint-min-width-md) {
|
||||||
margin-bottom: 1.2rem;
|
margin-bottom: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
color: $white;
|
color: var(--pgn-color-white);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -86,7 +108,7 @@
|
|||||||
height: 5rem;
|
height: 5rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@media (--pgn-size-breakpoint-min-width-md) {
|
||||||
width: 12rem;
|
width: 12rem;
|
||||||
height: 12rem;
|
height: 12rem;
|
||||||
}
|
}
|
||||||
@@ -106,7 +128,7 @@
|
|||||||
border-radius:0;
|
border-radius:0;
|
||||||
transition: opacity 200ms ease;
|
transition: opacity 200ms ease;
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@media (--pgn-size-breakpoint-min-width-md) {
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +142,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.certificate-title {
|
.certificate-title {
|
||||||
font-family: $font-family-serif;
|
font-family: var(--pgn-typography-font-family-serif);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
src/routes/AppRoutes.jsx
Normal file
21
src/routes/AppRoutes.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
AuthenticatedPageRoute,
|
||||||
|
PageWrap,
|
||||||
|
} from '@edx/frontend-platform/react';
|
||||||
|
import { Routes, Route, useNavigate } from 'react-router-dom';
|
||||||
|
import { ProfilePage, NotFoundPage } from '../profile';
|
||||||
|
|
||||||
|
const AppRoutes = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/u/:username" element={<AuthenticatedPageRoute><ProfilePage navigate={navigate} /></AuthenticatedPageRoute>} />
|
||||||
|
<Route path="/notfound" element={<PageWrap><NotFoundPage /></PageWrap>} />
|
||||||
|
<Route path="*" element={<PageWrap><NotFoundPage /></PageWrap>} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppRoutes;
|
||||||
63
src/routes/routes.test.jsx
Normal file
63
src/routes/routes.test.jsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { AppContext } from '@edx/frontend-platform/react';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { MemoryRouter as Router } from 'react-router-dom';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||||
|
import AppRoutes from './AppRoutes';
|
||||||
|
|
||||||
|
jest.mock('@edx/frontend-platform/analytics');
|
||||||
|
|
||||||
|
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||||
|
getLoginRedirectUrl: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../profile', () => ({
|
||||||
|
ProfilePage: () => (<div>Profile page</div>),
|
||||||
|
NotFoundPage: () => (<div>Not found page</div>),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const RoutesWithProvider = (context, path) => (
|
||||||
|
<AppContext.Provider value={context}>
|
||||||
|
<Router initialEntries={[`${path}`]}>
|
||||||
|
<AppRoutes />
|
||||||
|
</Router>
|
||||||
|
</AppContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const unauthenticatedUser = {
|
||||||
|
authenticatedUser: null,
|
||||||
|
config: getConfig(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('routes', () => {
|
||||||
|
test('Profile page should redirect for unauthenticated users', () => {
|
||||||
|
render(
|
||||||
|
RoutesWithProvider(unauthenticatedUser, '/u/edx'),
|
||||||
|
);
|
||||||
|
expect(getLoginRedirectUrl).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Profile page should be accessible for authenticated users', () => {
|
||||||
|
render(
|
||||||
|
RoutesWithProvider(
|
||||||
|
{
|
||||||
|
authenticatedUser: {
|
||||||
|
username: 'edx',
|
||||||
|
email: 'edx@example.com',
|
||||||
|
},
|
||||||
|
config: getConfig(),
|
||||||
|
},
|
||||||
|
'/u/edx',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Profile page')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show NotFound page for a bad route', () => {
|
||||||
|
render(
|
||||||
|
RoutesWithProvider(unauthenticatedUser, '/nonMatchingRoute'),
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Not found page')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
import 'core-js/stable';
|
import 'core-js/stable';
|
||||||
import 'regenerator-runtime/runtime';
|
import 'regenerator-runtime/runtime';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
import Enzyme from 'enzyme';
|
|
||||||
import Adapter from 'enzyme-adapter-react-16';
|
|
||||||
|
|
||||||
Enzyme.configure({ adapter: new Adapter() });
|
|
||||||
|
|||||||
10
src/utils/hoc.jsx
Normal file
10
src/utils/hoc.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
const withParams = (WrappedComponent) => {
|
||||||
|
const WithParamsComponent = (props) => <WrappedComponent params={useParams()} {...props} />;
|
||||||
|
return WithParamsComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withParams;
|
||||||
Reference in New Issue
Block a user