Compare commits
660 Commits
v1.6.6
...
release/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7cddb60f7 | ||
|
|
9b6e928260 | ||
|
|
0e676ffff3 | ||
|
|
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 | ||
|
|
b378967023 | ||
|
|
180af03af8 | ||
|
|
17ca164085 | ||
|
|
7cfc973c72 | ||
|
|
912c06caee | ||
|
|
58c654d8f1 | ||
|
|
fdb5de902d | ||
|
|
84360e6f42 | ||
|
|
2fad316329 | ||
|
|
c820dedbfc | ||
|
|
5199d88114 | ||
|
|
0383167754 | ||
|
|
17f42c8f97 | ||
|
|
13e5a684e9 | ||
|
|
56f5d655c6 |
9
.env
9
.env
@@ -10,6 +10,8 @@ LOGIN_URL=''
|
||||
LOGOUT_URL=''
|
||||
MARKETING_SITE_BASE_URL=''
|
||||
ORDER_HISTORY_URL=''
|
||||
ACCOUNT_SETTINGS_URL=''
|
||||
ACCOUNT_PROFILE_URL=''
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME=''
|
||||
@@ -22,5 +24,8 @@ LOGO_URL=''
|
||||
LOGO_TRADEMARK_URL=''
|
||||
LOGO_WHITE_URL=''
|
||||
FAVICON_URL=''
|
||||
ENABLE_LEARNER_RECORD_MFE=''
|
||||
LEARNER_RECORD_MFE_BASE_URL=''
|
||||
COLLECT_YEAR_OF_BIRTH=true
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
SEARCH_CATALOG_URL=''
|
||||
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||
|
||||
@@ -3,14 +3,16 @@ PORT=1995
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:1995'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
ACCOUNT_SETTINGS_URL=http://localhost:1997
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
ACCOUNT_PROFILE_URL=http://localhost:1995
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
ORDER_HISTORY_URL='localhost:1996/orders'
|
||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME=localhost
|
||||
@@ -23,5 +25,8 @@ LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
ENABLE_LEARNER_RECORD_MFE=''
|
||||
LEARNER_RECORD_MFE_BASE_URL='http://localhost:1990'
|
||||
COLLECT_YEAR_OF_BIRTH=true
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
SEARCH_CATALOG_URL='http://localhost:18000/courses'
|
||||
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||
|
||||
@@ -5,10 +5,12 @@ CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
ACCOUNT_SETTINGS_URL='http://localhost:1997'
|
||||
ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
ORDER_HISTORY_URL='localhost:1996/orders'
|
||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME=localhost
|
||||
@@ -18,4 +20,8 @@ LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
ENABLE_LEARNER_RECORD_MFE=''
|
||||
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||
LEARNER_RECORD_MFE_BASE_URL='http://localhost:1990'
|
||||
COLLECT_YEAR_OF_BIRTH=true
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -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
|
||||
|
||||
29
.github/workflows/ci.yml
vendored
Normal file
29
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
npm-test:
|
||||
- i18n_extract
|
||||
- lint
|
||||
- test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: make requirements
|
||||
- run: make test NPM_TESTS=build
|
||||
- run: make test NPM_TESTS=${{ matrix.npm-test }}
|
||||
- name: Coverage
|
||||
if: ${{ matrix.npm-test == 'test' }}
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
10
.github/workflows/commitlint.yml
vendored
Normal file
10
.github/workflows/commitlint.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Run commitlint on the commit messages in a pull request.
|
||||
|
||||
name: Lint Commit Messages
|
||||
|
||||
on:
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
uses: openedx/.github/.github/workflows/commitlint.yml@master
|
||||
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 }}
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -14,7 +14,7 @@ temp/babel-plugin-react-intl
|
||||
|
||||
### Emacs ###
|
||||
*~
|
||||
/.vscode
|
||||
/temp
|
||||
/npm-dist
|
||||
/.vscode
|
||||
/module.config.js
|
||||
src/i18n/messages
|
||||
27
.releaserc
27
.releaserc
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"branch": "master",
|
||||
"tagFormat": "v${version}",
|
||||
"verifyConditions": [
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"path": "@semantic-release/github",
|
||||
"assets": {
|
||||
"path": "npm-dist/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"analyzeCommits": "@semantic-release/commit-analyzer",
|
||||
"generateNotes": "@semantic-release/release-notes-generator",
|
||||
"prepare": "@semantic-release/npm",
|
||||
"publish": [
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"path": "@semantic-release/github",
|
||||
"assets": {
|
||||
"path": "npm-dist/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"success": [],
|
||||
"fail": []
|
||||
}
|
||||
23
.travis.yml
23
.travis.yml
@@ -1,23 +0,0 @@
|
||||
language: node_js
|
||||
node_js: 12
|
||||
before_install:
|
||||
- npm install -g npm@6
|
||||
install:
|
||||
- npm ci
|
||||
script:
|
||||
- make validate-no-uncommitted-package-lock-changes
|
||||
- npm run i18n_extract
|
||||
- npm run lint
|
||||
- npm run test
|
||||
- npm run build
|
||||
- npm run npm-build
|
||||
- npm run is-es5
|
||||
after_success:
|
||||
- npx semantic-release
|
||||
- codecov
|
||||
env:
|
||||
global:
|
||||
# GH_TOKEN
|
||||
- secure: qvCsyn5Ioj+b/gvX95yaQ+Vha+r60pGxuKFm6HzVbnWJyKAbR1NN8+RpIo0HUD9S5RPEFjLae82bLa2g/syS+D1DU9qF8NYderjy58bjibgrPdC1YOhi2SEY5u6TW6/T88+w9DyfZ6MF883lJi/6uzs24bHE13RguARlO79OeGvHk6SecoJSWe9yq2IHA0wAScKrvS64+AcUWyTDUvkrxHEvupVJs6aJ6EsdPALODLJ7JnXX7Rh+68nyqbezh+ZnumxBPkdCzUNdtO9C0o7JjV6/P1xg9rNre/huWGx4SYndPaPEmoOpWEfyCZw1ub83LHC1HBnE7eRJku/CmiaiaLGSfeU2sCRteBLlSyI5fEOQ4Su2xLu2k+Yuczj4BlIbjr+xC+bNg+WbOF8vj7TL4/YCxOG/8V4nLfDcHFsZDsPoYZyBe9Z5HsS77S5ZrutQ3i/qR+4zfs4X/QfPXfW3MqI7+oGo+PVNPgxI24i2Kwq5+ZUjnmkVnuGC5ZOZMN8aYojEvqdF+u5VALL/YCmXsXysL6BUsqv4kiZJpQEVBG0WPbh0FJH8qiB8OjLGogxkoSrL4Yd++o4fnSAvW1UgARIlJ0iHVf5r/wVei1KCQiFS6mh0Pwq1lM0Vo2SlTCF5UylnV80Hv7XY6Sp9dYi/EaVG1dv4DyJOHnPti1JAM/U=
|
||||
# NPM_TOKEN
|
||||
- secure: S6spzx4jJGWqucSwjaSdPEaNWlwzA75FisK+NoLFDY6uU7KIi2dayHiFlSW1C1vl5xQkkosHXJAInSwcnQMCUomCEuD1FFzydXA1/Dn8NmPc35coKR4CX9yJiArRbjZZjboCulrTPUVWHWoPfA7JTnqCDgOnXspgRKy7emzFLXkdn8vuUQcyXmmT/vDfQQjgq/T+DdBbMiradMvlTzxmKydAVeHAZ03A1Z5O6ePnR6IsFv4LgJ/RfGn501jHeTww6HsxfQX27tWLo5AyQ00YjMl9oXfs57n5u+em/ph3Xoc8Pz6wjkMLJ+eYo+xkYjoZ3d+/f+WXPLMBUNz0LpIBs7LcBHhAc2z4N+dce42CTXur/v8ra0BZEnZQsvOakHvox6KisDDlipPqc0yF7MgThbd31fc8QjdVxdh5w7tLH4PJ7QpBUtZwPskDLABKjnrxkxH+BysHADvVaNOBcZMwIA9yHY3GqzpyQ2upcS07h5a+SHx07viuA7hEn14XCtYrM1AHwS0n2sNdinb0qWOCcGOXzl39FU3N6xNueKoH/eIXuqJ7oiUkKBiSP9zApTApPL6nrng0InZhGjzZHOMNePPdin/uIrxk/fp/wqneGTQemCGRkfHdPT0yuIP22bUv/OmDtgNEG9t9hbIZGYgQmG4FTeTQQ7lC5RjmLrn+uWI=
|
||||
@@ -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
|
||||
55
Makefile
Executable file → Normal file
55
Makefile
Executable file → Normal file
@@ -1,17 +1,23 @@
|
||||
transifex_resource = frontend-app-profile
|
||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
|
||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
|
||||
requirements:
|
||||
npm install
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
.PHONY: test
|
||||
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
|
||||
|
||||
.PHONY: test.npm.*
|
||||
test.npm.%: validate-no-uncommitted-package-lock-changes
|
||||
test -d node_modules || $(MAKE) requirements
|
||||
npm run $(*)
|
||||
|
||||
.PHONY: requirements
|
||||
requirements: ## install ci requirements
|
||||
npm ci
|
||||
|
||||
i18n.extract:
|
||||
# Pulling display strings from .jsx files into .json files...
|
||||
@@ -29,29 +35,20 @@ detect_changed_source_translations:
|
||||
# Checking for changed translations...
|
||||
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:
|
||||
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.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
# Checking for package-lock.json changes...
|
||||
git diff --exit-code package-lock.json
|
||||
|
||||
npm-build:
|
||||
rm -rf ./npm-dist
|
||||
./node_modules/.bin/fedx-scripts babel src/profile --out-dir npm-dist --source-maps --ignore **/*.test.jsx,**/*.test.js,**/setupTest.js --copy-files
|
||||
@# --copy-files will bring in everything else that wasn't processed by babel. Remove what we don't want.
|
||||
@find npm-dist -name '*.test.js*' -delete
|
||||
@rm -rf ./npm-dist/__mocks__
|
||||
|
||||
164
README.rst
164
README.rst
@@ -1,63 +1,153 @@
|
||||
|Build Status| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
|
||||
|
||||
#####################
|
||||
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 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
|
||||
- Log in (http://localhost:18000/login)
|
||||
#. To use this application, `devstack <https://github.com/openedx/devstack>`__ must be running and you must be logged into it.
|
||||
|
||||
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
|
||||
npm start # The server will run on port 1995
|
||||
1. Clone your new repo:
|
||||
|
||||
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:
|
||||
|
||||
.. code:: bash
|
||||
.. code-block::
|
||||
|
||||
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
|
||||
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/micro-frontends-in-open-edx.html>`__.
|
||||
If you're having trouble, we have discussion forums at
|
||||
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
|
||||
:target: https://travis-ci.org/edx/frontend-app-profile
|
||||
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-app-profile
|
||||
:target: https://codecov.io/gh/edx/frontend-app-profile
|
||||
.. |npm_version| image:: https://img.shields.io/npm/v/@edx/frontend-app-profile.svg
|
||||
:target: https://www.npmjs.com/package/@edx/frontend-app-profile
|
||||
.. |npm_downloads| image:: https://img.shields.io/npm/dt/@edx/frontend-app-profile.svg
|
||||
:target: https://www.npmjs.com/package/@edx/frontend-app-profile
|
||||
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-profile.svg
|
||||
:target: @edx/frontend-app-profile
|
||||
.. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
|
||||
:target: https://github.com/semantic-release/semantic-release
|
||||
Our real-time conversations are on Slack. You can request a `Slack
|
||||
invitation`_, then join our `community Slack workspace`_. Because this is a
|
||||
frontend repository, the best place to discuss it would be in the `#wg-frontend
|
||||
channel`_.
|
||||
|
||||
For anything non-trivial, the best path is to open an issue in this repository
|
||||
with as many details about the issue you are facing as you can provide. 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,3 +0,0 @@
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-angular'],
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFiles: [
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
|
||||
|
||||
nick: prof
|
||||
oeps: {}
|
||||
owner: edx/arch-team
|
||||
openedx-release: {ref: master}
|
||||
37264
package-lock.json
generated
37264
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
102
package.json
102
package.json
@@ -4,85 +4,77 @@
|
||||
"description": "User profile micro-frontend for Open edX",
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "npm-dist/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-profile.git"
|
||||
"url": "git+https://github.com/openedx/frontend-app-profile.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"npm-build": "make npm-build",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests"
|
||||
},
|
||||
"files": [
|
||||
"/npm-dist"
|
||||
],
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint",
|
||||
"commit-msg": "commitlint -e $GIT_PARAMS"
|
||||
}
|
||||
"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": {
|
||||
"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": {
|
||||
"access": "public"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
"ie 11"
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "10.1.6",
|
||||
"@edx/frontend-component-header": "2.3.0",
|
||||
"@edx/frontend-platform": "1.12.4",
|
||||
"@edx/paragon": "16.6.1",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.1.15",
|
||||
"classnames": "2.3.1",
|
||||
"core-js": "3.16.4",
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/openedx-atlas": "^0.7.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/paragon": "^22.17.0",
|
||||
"@pact-foundation/pact": "^11.0.2",
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.41.0",
|
||||
"history": "5.3.0",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"lodash.get": "4.4.2",
|
||||
"lodash.pick": "4.4.0",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"prop-types": "15.7.2",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-redux": "7.2.4",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"redux": "4.1.1",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "6.30.0",
|
||||
"react-router-dom": "6.30.0",
|
||||
"redux": "4.2.1",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-saga": "1.1.3",
|
||||
"redux-thunk": "2.3.0",
|
||||
"regenerator-runtime": "0.13.9",
|
||||
"reselect": "4.0.0",
|
||||
"universal-cookie": "3.1.0"
|
||||
"redux-saga": "1.3.0",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.14.1",
|
||||
"reselect": "5.1.1",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "13.1.0",
|
||||
"@commitlint/config-angular": "13.1.0",
|
||||
"@edx/frontend-build": "8.0.4",
|
||||
"codecov": "3.8.3",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.6",
|
||||
"es-check": "5.2.4",
|
||||
"glob": "7.1.7",
|
||||
"husky": "7.0.2",
|
||||
"react-test-renderer": "16.14.0",
|
||||
"@commitlint/cli": "19.8.0",
|
||||
"@commitlint/config-angular": "19.8.0",
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/reactifex": "2.2.0",
|
||||
"@openedx/frontend-build": "^14.3.3",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "14.3.1",
|
||||
"glob": "11.0.1",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "1.5.4"
|
||||
"redux-mock-store": "1.5.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
<title>Learner Profile | <%= process.env.SITE_NAME %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700" rel="stylesheet">
|
||||
<link rel="shortcut icon" href="<%=webpackConfig.output.publicPath%>favicon.ico" type="image/x-icon" />
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
"pin"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["@edx", "@openedx"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"timezone": "America/New_York"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
@@ -1,7 +1,7 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { applyMiddleware, createStore, compose } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||
import { composeWithDevTools } from '@redux-devtools/extension';
|
||||
import { createLogger } from 'redux-logger';
|
||||
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": "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..."
|
||||
}
|
||||
@@ -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,54 +13,49 @@ import {
|
||||
ErrorPage,
|
||||
} from '@edx/frontend-platform/react';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import React, { StrictMode } from 'react';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import Header, { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
import Footer, { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
import Header from '@edx/frontend-component-header';
|
||||
import { FooterSlot } from '@edx/frontend-component-footer';
|
||||
|
||||
import appMessages from './i18n';
|
||||
import { ProfilePage, NotFoundPage } from './profile';
|
||||
import messages from './i18n';
|
||||
import configureStore from './data/configureStore';
|
||||
|
||||
import './index.scss';
|
||||
import './assets/favicon.ico';
|
||||
import Head from './head/Head';
|
||||
|
||||
import AppRoutes from './routes/AppRoutes';
|
||||
|
||||
const rootNode = createRoot(document.getElementById('root'));
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<AppProvider store={configureStore()}>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route path="/u/:username" component={ProfilePage} />
|
||||
<Route path="/notfound" component={NotFoundPage} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</AppProvider>,
|
||||
document.getElementById('root'),
|
||||
rootNode.render(
|
||||
<StrictMode>
|
||||
<AppProvider store={configureStore()}>
|
||||
<Head />
|
||||
<Header />
|
||||
<main id="main">
|
||||
<AppRoutes />
|
||||
</main>
|
||||
<FooterSlot />
|
||||
</AppProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
subscribe(APP_INIT_ERROR, (error) => {
|
||||
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
|
||||
rootNode.render(<ErrorPage message={error.message} />);
|
||||
});
|
||||
|
||||
initialize({
|
||||
messages: [
|
||||
appMessages,
|
||||
headerMessages,
|
||||
footerMessages,
|
||||
],
|
||||
requireAuthenticatedUser: true,
|
||||
messages,
|
||||
hydrateAuthenticatedUser: true,
|
||||
handlers: {
|
||||
config: () => {
|
||||
mergeConfig({
|
||||
ENABLE_LEARNER_RECORD_MFE: (process.env.ENABLE_LEARNER_RECORD_MFE || false),
|
||||
LEARNER_RECORD_MFE_BASE_URL: process.env.LEARNER_RECORD_MFE_BASE_URL,
|
||||
COLLECT_YEAR_OF_BIRTH: process.env.COLLECT_YEAR_OF_BIRTH,
|
||||
ENABLE_SKILLS_BUILDER_PROFILE: process.env.ENABLE_SKILLS_BUILDER_PROFILE,
|
||||
}, 'App loadConfig override handler');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@edx/paragon/scss/core/core";
|
||||
@import "~@openedx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
@import "~@edx/frontend-component-header/dist/index";
|
||||
@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"
|
||||
}
|
||||
}
|
||||
97
src/plugin-slots/AdditionalProfileFieldsSlot/README.md
Normal file
97
src/plugin-slots/AdditionalProfileFieldsSlot/README.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Additional Profile Fields
|
||||
|
||||
### Slot ID: `org.openedx.frontend.profile.additional_profile_fields.v1`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to replace/modify/hide the additional profile fields in the profile page.
|
||||
|
||||
## Example
|
||||
The following `env.config.jsx` will extend the default fields with a additional custom fields through a simple example component.
|
||||
|
||||

|
||||
|
||||
### Using the Additional Fields Component
|
||||
Create a file named `env.config.jsx` at the MFE root with this:
|
||||
|
||||
```jsx
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
import Example from './src/plugin-slots/AdditionalProfileFieldsSlot/example';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.profile.additional_profile_fields.v1': {
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'additional_profile_fields',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: Example,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
## Plugin Props
|
||||
|
||||
When implementing a plugin for this slot, the following props are available:
|
||||
|
||||
### `updateUserProfile`
|
||||
- **Type**: Function
|
||||
- **Description**: A function for updating the user's profile with new field values. This handles the API call to persist changes to the backend.
|
||||
- **Usage**: Pass an object containing the field updates to be saved to the user's profile. The function automatically handles the persistence and UI updates.
|
||||
|
||||
#### Example
|
||||
```javascript
|
||||
updateUserProfile({ extendedProfile: [{ fieldName: 'favorite_color', fieldValue: value }] });
|
||||
```
|
||||
|
||||
### `profileFieldValues`
|
||||
- **Type**: Array of Objects
|
||||
- **Description**: Contains the current values of all additional profile fields as an array of objects. Each object has a `fieldName` property (string) and a `fieldValue` property (which can be string, boolean, number, or other data types depending on the field type).
|
||||
- **Usage**: Access specific field values by finding the object with the matching `fieldName` and reading its `fieldValue` property. Use array methods like `find()` to locate specific fields.
|
||||
|
||||
#### Example
|
||||
```javascript
|
||||
// Finding a specific field value
|
||||
const nifField = profileFieldValues.find(field => field.fieldName === 'nif');
|
||||
const nifValue = nifField ? nifField.fieldValue : null;
|
||||
|
||||
// Example data structure:
|
||||
[
|
||||
{
|
||||
"fieldName": "favorite_color",
|
||||
"fieldValue": "red"
|
||||
},
|
||||
{
|
||||
"fieldName": "employment_situation",
|
||||
"fieldValue": "Unemployed"
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
### `profileFieldErrors`
|
||||
- **Type**: Object
|
||||
- **Description**: Contains validation errors for profile fields. Each key corresponds to a field name, and the value is the error message.
|
||||
- **Usage**: Check for field-specific errors to display validation feedback to users.
|
||||
|
||||
### `formComponents`
|
||||
- **Type**: Object
|
||||
- **Description**: Provides access to reusable form components that are consistent with the rest of the profile page styling and behavior. These components follow the platform's design system and include proper validation and accessibility features.
|
||||
- **Usage**: Use these components in your custom fields implementation to maintain UI consistency. Available components include `SwitchContent` for managing different UI states, `EmptyContent` for empty states, and `EditableItemHeader` for consistent headers.
|
||||
|
||||
### `refreshUserProfile`
|
||||
- **Type**: Function
|
||||
- **Description**: A function that triggers a refresh of the user's profile data. This can be used after updating profile fields to ensure the UI reflects the latest data from the server.
|
||||
- **Usage**: Call this function with the username parameter when you need to manually reload the user profile information. Note that `updateUserProfile` typically handles data refresh automatically.
|
||||
|
||||
#### Example
|
||||
```javascript
|
||||
refreshUserProfile(username);
|
||||
```
|
||||
129
src/plugin-slots/AdditionalProfileFieldsSlot/example/index.jsx
Normal file
129
src/plugin-slots/AdditionalProfileFieldsSlot/example/index.jsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
/**
|
||||
* Straightforward example of how you could use the pluginProps provided by
|
||||
* the AdditionalProfileFieldsSlot to create a custom profile field.
|
||||
*
|
||||
* Here you can set a 'favorite_color' field with radio buttons and
|
||||
* save it to the user's profile, especifically to their `meta` in
|
||||
* the user's model. For more information, see the documentation:
|
||||
*
|
||||
* https://github.com/openedx/edx-platform/blob/master/openedx/core/djangoapps/user_api/README.rst#persisting-optional-user-metadata
|
||||
*/
|
||||
const Example = ({
|
||||
updateUserProfile,
|
||||
profileFieldValues,
|
||||
profileFieldErrors,
|
||||
formComponents: { SwitchContent, EditableItemHeader, EmptyContent } = {},
|
||||
}) => {
|
||||
const authenticatedUser = getAuthenticatedUser();
|
||||
const [formMode, setFormMode] = useState('editable');
|
||||
|
||||
// Get current favorite color from profileFieldValues
|
||||
const currentColorField = profileFieldValues?.find(field => field.fieldName === 'favorite_color');
|
||||
const currentColor = currentColorField ? currentColorField.fieldValue : '';
|
||||
|
||||
const [value, setValue] = useState(currentColor);
|
||||
const handleChange = e => setValue(e.target.value);
|
||||
|
||||
// Get any validation errors for the favorite_color field
|
||||
const colorFieldError = profileFieldErrors?.favorite_color;
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) { setFormMode('empty'); }
|
||||
if (colorFieldError) {
|
||||
setFormMode('editing');
|
||||
}
|
||||
}, [colorFieldError, value]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
try {
|
||||
updateUserProfile(authenticatedUser.username, { extendedProfile: [{ fieldName: 'favorite_color', fieldValue: value }] });
|
||||
setFormMode('editable');
|
||||
} catch (error) {
|
||||
setFormMode('editing');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border border-accent-500 p-3 mt-5">
|
||||
<h3 className="h3">Example Additional Profile Fields Slot</h3>
|
||||
|
||||
<SwitchContent
|
||||
className="pt-40px"
|
||||
expression={formMode}
|
||||
cases={{
|
||||
editing: (
|
||||
<>
|
||||
<label className="edit-section-header" htmlFor="favorite_color">
|
||||
Favorite Color
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="favorite_color"
|
||||
name="favorite_color"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Button type="button" className="mt-2" onClick={handleSubmit}>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
editable: (
|
||||
<>
|
||||
<div className="row m-0 pb-1.5 align-items-center">
|
||||
<p data-hj-suppress className="h5 font-weight-bold m-0">
|
||||
Favorite Color
|
||||
</p>
|
||||
</div>
|
||||
<EditableItemHeader
|
||||
content={value}
|
||||
showEditButton
|
||||
onClickEdit={() => setFormMode('editing')}
|
||||
showVisibility={false}
|
||||
visibility="private"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
empty: (
|
||||
<>
|
||||
<div className="row m-0 pb-1.5 align-items-center">
|
||||
<p data-hj-suppress className="h5 font-weight-bold m-0">
|
||||
Favorite Color
|
||||
</p>
|
||||
</div>
|
||||
<EmptyContent onClick={() => setFormMode('editing')}>
|
||||
<p className="mb-0">Click to add your favorite color</p>
|
||||
</EmptyContent>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Example.propTypes = {
|
||||
updateUserProfile: PropTypes.func.isRequired,
|
||||
profileFieldValues: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
fieldName: PropTypes.string.isRequired,
|
||||
fieldValue: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
PropTypes.number,
|
||||
]).isRequired,
|
||||
}),
|
||||
),
|
||||
profileFieldErrors: PropTypes.objectOf(PropTypes.string),
|
||||
formComponents: PropTypes.shape({
|
||||
SwitchContent: PropTypes.elementType.isRequired,
|
||||
}),
|
||||
};
|
||||
|
||||
export default Example;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
37
src/plugin-slots/AdditionalProfileFieldsSlot/index.jsx
Normal file
37
src/plugin-slots/AdditionalProfileFieldsSlot/index.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { patchProfile } from '../../profile/data/services';
|
||||
import { fetchProfile } from '../../profile/data/actions';
|
||||
|
||||
import SwitchContent from '../../profile/forms/elements/SwitchContent';
|
||||
import EmptyContent from '../../profile/forms/elements/EmptyContent';
|
||||
import EditableItemHeader from '../../profile/forms/elements/EditableItemHeader';
|
||||
|
||||
const AdditionalProfileFieldsSlot = () => {
|
||||
const dispatch = useDispatch();
|
||||
const extendedProfileValues = useSelector((state) => state.profilePage.account.extendedProfile);
|
||||
const errors = useSelector((state) => state.profilePage.errors);
|
||||
|
||||
const pluginProps = {
|
||||
refreshUserProfile: useCallback((username) => dispatch(fetchProfile(username)), [dispatch]),
|
||||
updateUserProfile: patchProfile,
|
||||
profileFieldValues: extendedProfileValues,
|
||||
profileFieldErrors: errors,
|
||||
formComponents: {
|
||||
SwitchContent,
|
||||
EmptyContent,
|
||||
EditableItemHeader,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<PluginSlot
|
||||
id="org.openedx.frontend.profile.additional_profile_fields.v1"
|
||||
pluginProps={pluginProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdditionalProfileFieldsSlot;
|
||||
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 PropTypes from 'prop-types';
|
||||
import { StatusAlert } from '@edx/paragon';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
function AgeMessage({ accountSettingsUrl }) {
|
||||
return (
|
||||
<StatusAlert
|
||||
alertType="info"
|
||||
dialog={(
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="profile.age.headline"
|
||||
defaultMessage="Your profile cannot be shared."
|
||||
description="error message"
|
||||
tagName="h6"
|
||||
/>
|
||||
<FormattedMessage
|
||||
id="profile.age.details"
|
||||
defaultMessage="To share your profile with other edX learners, you must confirm that you are over the age of 13."
|
||||
description="error message"
|
||||
tagName="p"
|
||||
/>
|
||||
<a href={accountSettingsUrl}>
|
||||
<FormattedMessage
|
||||
id="profile.age.set.date"
|
||||
defaultMessage="Set your date of birth"
|
||||
description="label on a link to set birthday"
|
||||
/>
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
dismissible={false}
|
||||
open
|
||||
const AgeMessage = ({ accountSettingsUrl }) => (
|
||||
<Alert
|
||||
variant="info"
|
||||
dismissible={false}
|
||||
show
|
||||
>
|
||||
<Alert.Heading id="profile.age.headline">
|
||||
<FormattedMessage
|
||||
id="profile.age.cannotShare"
|
||||
defaultMessage="Your profile cannot be shared."
|
||||
description="Error message indicating that the user's profile cannot be shared"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<FormattedMessage
|
||||
id="profile.age.details"
|
||||
defaultMessage="To share your profile with other {siteName} learners, you must confirm that you are over the age of 13."
|
||||
description="Error message"
|
||||
tagName="p"
|
||||
values={{
|
||||
siteName: getConfig().SITE_NAME,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
<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 = {
|
||||
accountSettingsUrl: PropTypes.string.isRequired,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
function Banner() {
|
||||
return <div className="profile-page-bg-banner bg-primary d-none d-md-block p-relative" />;
|
||||
}
|
||||
const Banner = () => <div className="profile-page-bg-banner bg-primary d-md-block p-relative" />;
|
||||
|
||||
export default Banner;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
|
||||
|
||||
function DateJoined({ date }) {
|
||||
const DateJoined = ({ date }) => {
|
||||
if (date == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -19,7 +19,7 @@ function DateJoined({ date }) {
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
DateJoined.propTypes = {
|
||||
date: PropTypes.string,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||
<p className="my-0 py-5 text-muted" style={{ maxWidth: '32em' }}>
|
||||
<FormattedMessage
|
||||
id="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."
|
||||
description="error message when a page does not exist"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const NotFoundPage = () => (
|
||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||
<p className="my-0 py-5 text-muted" style={{ maxWidth: '32em' }}>
|
||||
<FormattedMessage
|
||||
id="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."
|
||||
description="error message when a page does not exist"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NotFoundPage;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { ensureConfig, getConfig } from '@edx/frontend-platform';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { StatusAlert, Hyperlink } from '@edx/paragon';
|
||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
@@ -30,8 +30,10 @@ import Bio from './forms/Bio';
|
||||
import Certificates from './forms/Certificates';
|
||||
import AgeMessage from './AgeMessage';
|
||||
import DateJoined from './DateJoined';
|
||||
import UsernameDescription from './UsernameDescription';
|
||||
import PageLoading from './PageLoading';
|
||||
import Banner from './Banner';
|
||||
import LearningGoal from './forms/LearningGoal';
|
||||
|
||||
// Selectors
|
||||
import { profilePageSelector } from './data/selectors';
|
||||
@@ -39,17 +41,20 @@ import { profilePageSelector } from './data/selectors';
|
||||
// i18n
|
||||
import messages from './ProfilePage.messages';
|
||||
|
||||
import withParams from '../utils/hoc';
|
||||
|
||||
import AdditionalProfileFieldsSlot from '../plugin-slots/AdditionalProfileFieldsSlot';
|
||||
|
||||
ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage');
|
||||
|
||||
class ProfilePage extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const recordsUrl = this.getRecordsUrl(context);
|
||||
|
||||
const credentialsBaseUrl = context.config.CREDENTIALS_BASE_URL;
|
||||
this.state = {
|
||||
viewMyRecordsUrl: recordsUrl,
|
||||
accountSettingsUrl: `${context.config.LMS_BASE_URL}/account/settings`,
|
||||
viewMyRecordsUrl: credentialsBaseUrl ? `${credentialsBaseUrl}/records` : null,
|
||||
accountSettingsUrl: context.config.ACCOUNT_SETTINGS_URL,
|
||||
};
|
||||
|
||||
this.handleSaveProfilePhoto = this.handleSaveProfilePhoto.bind(this);
|
||||
@@ -61,29 +66,12 @@ class ProfilePage extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchProfile(this.props.match.params.username);
|
||||
this.props.fetchProfile(this.props.params.username);
|
||||
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) {
|
||||
this.props.saveProfilePhoto(this.context.authenticatedUser.username, formData);
|
||||
}
|
||||
@@ -108,6 +96,18 @@ class ProfilePage extends React.Component {
|
||||
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)
|
||||
renderViewMyRecordsButton() {
|
||||
if (!(this.state.viewMyRecordsUrl && this.isAuthenticatedUserProfile())) {
|
||||
@@ -126,13 +126,12 @@ class ProfilePage extends React.Component {
|
||||
const { dateJoined } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span data-hj-suppress>
|
||||
<h1 className="h2 mb-0 font-weight-bold">{this.props.match.params.username}</h1>
|
||||
<DateJoined date={dateJoined} />
|
||||
<hr className="d-none d-md-block" />
|
||||
</span>
|
||||
</>
|
||||
<span data-hj-suppress>
|
||||
<h1 className="h2 mb-0 font-weight-bold text-truncate">{this.props.params.username}</h1>
|
||||
<DateJoined date={dateJoined} />
|
||||
{this.isYOBDisabled() && <UsernameDescription />}
|
||||
<hr className="d-none d-md-block" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,7 +145,9 @@ class ProfilePage extends React.Component {
|
||||
return (
|
||||
<div className="row">
|
||||
<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>
|
||||
);
|
||||
@@ -174,19 +175,29 @@ class ProfilePage extends React.Component {
|
||||
socialLinks,
|
||||
draftSocialLinksByPlatform,
|
||||
visibilitySocialLinks,
|
||||
learningGoal,
|
||||
visibilityLearningGoal,
|
||||
languageProficiencies,
|
||||
visibilityLanguageProficiencies,
|
||||
courseCertificates,
|
||||
visibilityCourseCertificates,
|
||||
bio,
|
||||
visibilityBio,
|
||||
requiresParentalConsent,
|
||||
isLoadingProfile,
|
||||
username,
|
||||
saveState,
|
||||
navigate,
|
||||
} = this.props;
|
||||
|
||||
if (isLoadingProfile) {
|
||||
return <PageLoading srMessage={this.props.intl.formatMessage(messages['profile.loading'])} />;
|
||||
}
|
||||
|
||||
if (!username && saveState === 'error' && navigate) {
|
||||
navigate('/notfound');
|
||||
}
|
||||
|
||||
const commonFormProps = {
|
||||
openHandler: this.handleOpen,
|
||||
closeHandler: this.handleClose,
|
||||
@@ -194,6 +205,17 @@ class ProfilePage extends React.Component {
|
||||
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 (
|
||||
<div className="container-fluid">
|
||||
<div className="row align-items-center pt-4 mb-4 pt-md-0 mb-md-0">
|
||||
@@ -210,7 +232,7 @@ class ProfilePage extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col pl-0">
|
||||
<div className="col">
|
||||
<div className="d-md-none">
|
||||
{this.renderHeadingLockup()}
|
||||
</div>
|
||||
@@ -228,51 +250,76 @@ class ProfilePage extends React.Component {
|
||||
<div className="d-md-none mb-4">
|
||||
{this.renderViewMyRecordsButton()}
|
||||
</div>
|
||||
<Name
|
||||
name={name}
|
||||
visibilityName={visibilityName}
|
||||
formId="name"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<Country
|
||||
country={country}
|
||||
visibilityCountry={visibilityCountry}
|
||||
formId="country"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<PreferredLanguage
|
||||
languageProficiencies={languageProficiencies}
|
||||
visibilityLanguageProficiencies={visibilityLanguageProficiencies}
|
||||
formId="languageProficiencies"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<Education
|
||||
levelOfEducation={levelOfEducation}
|
||||
visibilityLevelOfEducation={visibilityLevelOfEducation}
|
||||
formId="levelOfEducation"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<SocialLinks
|
||||
socialLinks={socialLinks}
|
||||
draftSocialLinksByPlatform={draftSocialLinksByPlatform}
|
||||
visibilitySocialLinks={visibilitySocialLinks}
|
||||
formId="socialLinks"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
{isNameBlockVisible && (
|
||||
<Name
|
||||
name={name}
|
||||
visibilityName={visibilityName}
|
||||
formId="name"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{isLocationBlockVisible && (
|
||||
<Country
|
||||
country={country}
|
||||
visibilityCountry={visibilityCountry}
|
||||
formId="country"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{isLanguageBlockVisible && (
|
||||
<PreferredLanguage
|
||||
languageProficiencies={languageProficiencies}
|
||||
visibilityLanguageProficiencies={visibilityLanguageProficiencies}
|
||||
formId="languageProficiencies"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{isEducationBlockVisible && (
|
||||
<Education
|
||||
levelOfEducation={levelOfEducation}
|
||||
visibilityLevelOfEducation={visibilityLevelOfEducation}
|
||||
formId="levelOfEducation"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{isSocialLinksBLockVisible && (
|
||||
<SocialLinks
|
||||
socialLinks={socialLinks}
|
||||
draftSocialLinksByPlatform={draftSocialLinksByPlatform}
|
||||
visibilitySocialLinks={visibilitySocialLinks}
|
||||
formId="socialLinks"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
<div className="mb-4">
|
||||
<AdditionalProfileFieldsSlot />
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-md-3 col-md-8 col-lg-7 offset-lg-1">
|
||||
{this.renderAgeMessage()}
|
||||
<Bio
|
||||
bio={bio}
|
||||
visibilityBio={visibilityBio}
|
||||
formId="bio"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<Certificates
|
||||
visibilityCourseCertificates={visibilityCourseCertificates}
|
||||
formId="certificates"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
{!this.isYOBDisabled() && this.renderAgeMessage()}
|
||||
{isBioBlockVisible && (
|
||||
<Bio
|
||||
bio={bio}
|
||||
visibilityBio={visibilityBio}
|
||||
formId="bio"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{getConfig().ENABLE_SKILLS_BUILDER_PROFILE && (
|
||||
<LearningGoal
|
||||
learningGoal={learningGoal}
|
||||
visibilityLearningGoal={visibilityLearningGoal}
|
||||
formId="learningGoal"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
{isCertificatesBlockVisible && (
|
||||
<Certificates
|
||||
visibilityCourseCertificates={visibilityCourseCertificates}
|
||||
formId="certificates"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -295,9 +342,11 @@ ProfilePage.propTypes = {
|
||||
// Account data
|
||||
requiresParentalConsent: PropTypes.bool,
|
||||
dateJoined: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
|
||||
// Bio form data
|
||||
bio: PropTypes.string,
|
||||
yearOfBirth: PropTypes.number,
|
||||
visibilityBio: PropTypes.string.isRequired,
|
||||
|
||||
// Certificates form data
|
||||
@@ -335,6 +384,10 @@ ProfilePage.propTypes = {
|
||||
})),
|
||||
visibilitySocialLinks: PropTypes.string.isRequired,
|
||||
|
||||
// Learning Goal form data
|
||||
learningGoal: PropTypes.string,
|
||||
visibilityLearningGoal: PropTypes.string.isRequired,
|
||||
|
||||
// Other data we need
|
||||
profileImage: PropTypes.shape({
|
||||
src: PropTypes.string,
|
||||
@@ -355,12 +408,11 @@ ProfilePage.propTypes = {
|
||||
openForm: PropTypes.func.isRequired,
|
||||
closeForm: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
navigate: PropTypes.func.isRequired,
|
||||
|
||||
// Router
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
username: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
params: PropTypes.shape({
|
||||
username: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
// i18n
|
||||
@@ -369,15 +421,18 @@ ProfilePage.propTypes = {
|
||||
|
||||
ProfilePage.defaultProps = {
|
||||
saveState: null,
|
||||
username: '',
|
||||
savePhotoState: null,
|
||||
photoUploadError: {},
|
||||
profileImage: {},
|
||||
name: null,
|
||||
yearOfBirth: null,
|
||||
levelOfEducation: null,
|
||||
country: null,
|
||||
socialLinks: [],
|
||||
draftSocialLinksByPlatform: {},
|
||||
bio: null,
|
||||
learningGoal: null,
|
||||
languageProficiencies: [],
|
||||
courseCertificates: null,
|
||||
requiresParentalConsent: null,
|
||||
@@ -395,4 +450,4 @@ export default connect(
|
||||
closeForm,
|
||||
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 { AppContext } from '@edx/frontend-platform/react';
|
||||
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 PropTypes from 'prop-types';
|
||||
import { Provider } from 'react-redux';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { BrowserRouter, useNavigate } from 'react-router-dom';
|
||||
|
||||
import messages from '../i18n';
|
||||
import ProfilePage from './ProfilePage';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const storeMocks = {
|
||||
loadingApp: require('./__mocks__/loadingApp.mockStore.js'),
|
||||
viewOwnProfile: require('./__mocks__/viewOwnProfile.mockStore.js'),
|
||||
viewOtherProfile: require('./__mocks__/viewOtherProfile.mockStore.js'),
|
||||
savingEditedBio: require('./__mocks__/savingEditedBio.mockStore.js'),
|
||||
loadingApp: require('./__mocks__/loadingApp.mockStore'),
|
||||
invalidUser: require('./__mocks__/invalidUser.mockStore'),
|
||||
viewOwnProfile: require('./__mocks__/viewOwnProfile.mockStore'),
|
||||
viewOtherProfile: require('./__mocks__/viewOtherProfile.mockStore'),
|
||||
savingEditedBio: require('./__mocks__/savingEditedBio.mockStore'),
|
||||
};
|
||||
const requiredProfilePageProps = {
|
||||
fetchUserAccount: () => {},
|
||||
@@ -28,7 +30,7 @@ const requiredProfilePageProps = {
|
||||
deleteProfilePhoto: () => {},
|
||||
openField: () => {},
|
||||
closeField: () => {},
|
||||
match: { params: { username: 'staff' } },
|
||||
params: { username: 'staff' },
|
||||
};
|
||||
|
||||
// Mock language cookie
|
||||
@@ -65,84 +67,203 @@ beforeEach(() => {
|
||||
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('Renders correctly in various states', () => {
|
||||
it('app loading', () => {
|
||||
const component = (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
authenticatedUser: { userId: null, username: null, administrator: false },
|
||||
config: getConfig(),
|
||||
}}
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={mockStore(storeMocks.loadingApp)}>
|
||||
<ProfilePage {...requiredProfilePageProps} />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
const tree = renderer.create(component).toJSON();
|
||||
const contextValue = {
|
||||
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();
|
||||
});
|
||||
|
||||
it('successfully redirected to not found page.', () => {
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeMocks.invalidUser)} />;
|
||||
const { container: tree } = render(component);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('viewing own profile', () => {
|
||||
const component = (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
}}
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={mockStore(storeMocks.viewOwnProfile)}>
|
||||
<ProfilePage {...requiredProfilePageProps} />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
const tree = renderer.create(component).toJSON();
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeMocks.viewOwnProfile)} />;
|
||||
const { container: tree } = render(component);
|
||||
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 = (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
}}
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={mockStore(storeMocks.viewOtherProfile)}>
|
||||
<ProfilePage
|
||||
{...requiredProfilePageProps}
|
||||
match={{ params: { username: 'verified' } }} // Override default match
|
||||
/>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore({
|
||||
...storeMocks.viewOtherProfile,
|
||||
profilePage: {
|
||||
...storeMocks.viewOtherProfile.profilePage,
|
||||
account: {
|
||||
...storeMocks.viewOtherProfile.profilePage.account,
|
||||
name: 'user',
|
||||
country: 'EN',
|
||||
bio: 'bio',
|
||||
courseCertificates: ['course certificates'],
|
||||
levelOfEducation: 'some level',
|
||||
languageProficiencies: ['some lang'],
|
||||
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();
|
||||
});
|
||||
|
||||
it('while saving an edited bio', () => {
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
}}
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={mockStore(storeMocks.savingEditedBio)}>
|
||||
<ProfilePage {...requiredProfilePageProps} />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeMocks.savingEditedBio)}
|
||||
/>
|
||||
);
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -150,46 +271,69 @@ describe('<ProfilePage />', () => {
|
||||
const config = getConfig();
|
||||
config.CREDENTIALS_BASE_URL = '';
|
||||
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
const component = (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config,
|
||||
}}
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={mockStore(storeMocks.viewOwnProfile)}>
|
||||
<ProfilePage {...requiredProfilePageProps} />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeMocks.viewOwnProfile)}
|
||||
/>
|
||||
);
|
||||
const tree = renderer.create(component).toJSON();
|
||||
const { container: tree } = render(component);
|
||||
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', () => {
|
||||
it('calls sendTrackingLogEvent when mounting', () => {
|
||||
const component = (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
}}
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={mockStore(storeMocks.loadingApp)}>
|
||||
<ProfilePage
|
||||
{...requiredProfilePageProps}
|
||||
match={{ params: { username: 'test-username' } }}
|
||||
/>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</AppContext.Provider>
|
||||
const contextValue = {
|
||||
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
|
||||
config: getConfig(),
|
||||
};
|
||||
render(
|
||||
<ProfilePageWrapper
|
||||
contextValue={contextValue}
|
||||
store={mockStore(storeMocks.loadingApp)}
|
||||
params={{ username: 'test-username' }}
|
||||
/>,
|
||||
);
|
||||
const wrapper = mount(component);
|
||||
wrapper.update();
|
||||
|
||||
expect(analytics.sendTrackingLogEvent.mock.calls.length).toBe(1);
|
||||
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,
|
||||
imageUrlLarge: null
|
||||
},
|
||||
levelOfEducation: null
|
||||
levelOfEducation: null,
|
||||
learningGoal: null
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
|
||||
@@ -42,7 +42,8 @@ module.exports = {
|
||||
secondaryEmail: null,
|
||||
timeZone: null,
|
||||
gender: null,
|
||||
accountPrivacy: 'custom'
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: null,
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
@@ -91,7 +92,8 @@ module.exports = {
|
||||
timeZone: null,
|
||||
levelOfEducation: 'el',
|
||||
gender: null,
|
||||
accountPrivacy: 'custom'
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: null,
|
||||
},
|
||||
preferences: {
|
||||
visibilityUserLocation: 'all_users',
|
||||
@@ -104,7 +106,8 @@ module.exports = {
|
||||
visibilityName: 'private',
|
||||
visibilityLanguageProficiencies: 'all_users',
|
||||
visibilityCountry: 'all_users',
|
||||
accountPrivacy: 'custom'
|
||||
accountPrivacy: 'custom',
|
||||
visibilityLearningGoal: 'private',
|
||||
},
|
||||
courseCertificates: [
|
||||
{
|
||||
|
||||
@@ -42,7 +42,8 @@ module.exports = {
|
||||
secondaryEmail: null,
|
||||
timeZone: null,
|
||||
gender: null,
|
||||
accountPrivacy: 'custom'
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: 'advance_career',
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
@@ -83,7 +84,8 @@ module.exports = {
|
||||
preferences: {},
|
||||
courseCertificates: [],
|
||||
drafts: {},
|
||||
isLoadingProfile: false
|
||||
isLoadingProfile: false,
|
||||
learningGoal: 'advance_career',
|
||||
},
|
||||
router: {
|
||||
location: {
|
||||
|
||||
@@ -42,7 +42,8 @@ module.exports = {
|
||||
secondaryEmail: null,
|
||||
timeZone: null,
|
||||
gender: null,
|
||||
accountPrivacy: 'custom'
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: 'advance_career'
|
||||
},
|
||||
profilePage: {
|
||||
errors: {},
|
||||
@@ -91,7 +92,8 @@ module.exports = {
|
||||
timeZone: null,
|
||||
levelOfEducation: 'el',
|
||||
gender: null,
|
||||
accountPrivacy: 'custom'
|
||||
accountPrivacy: 'custom',
|
||||
learningGoal: 'advance_career'
|
||||
},
|
||||
preferences: {
|
||||
visibilityUserLocation: 'all_users',
|
||||
@@ -104,7 +106,8 @@ module.exports = {
|
||||
visibilityName: 'private',
|
||||
visibilityLanguageProficiencies: 'all_users',
|
||||
visibilityCountry: 'all_users',
|
||||
accountPrivacy: 'custom'
|
||||
accountPrivacy: 'custom',
|
||||
visibilityLearningGoal: 'private',
|
||||
},
|
||||
courseCertificates: [
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ const EDUCATION_LEVELS = [
|
||||
'jhs',
|
||||
'el',
|
||||
'none',
|
||||
'o',
|
||||
'other',
|
||||
];
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
const profilePage = (state = initialState, action) => {
|
||||
const profilePage = (state = initialState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case FETCH_PROFILE.BEGIN:
|
||||
return {
|
||||
@@ -63,12 +63,14 @@ const profilePage = (state = initialState, action) => {
|
||||
return {
|
||||
...state,
|
||||
saveState: 'error',
|
||||
isLoadingProfile: false,
|
||||
errors: { ...state.errors, ...action.payload.errors },
|
||||
};
|
||||
case SAVE_PROFILE.RESET:
|
||||
return {
|
||||
...state,
|
||||
saveState: null,
|
||||
isLoadingProfile: false,
|
||||
errors: {},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import pick from 'lodash.pick';
|
||||
import {
|
||||
@@ -66,6 +65,25 @@ export function* handleFetchProfile(action) {
|
||||
} else {
|
||||
[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(
|
||||
account,
|
||||
preferences,
|
||||
@@ -76,7 +94,11 @@ export function* handleFetchProfile(action) {
|
||||
yield put(fetchProfileReset());
|
||||
} catch (e) {
|
||||
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 {
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -35,9 +35,12 @@ export const editableFormModeSelector = createSelector(
|
||||
// or is being hidden from us (for other users' profiles)
|
||||
let propExists = account[formId] != null && account[formId].length > 0;
|
||||
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 ...
|
||||
if (!isAuthenticatedUserProfile || account.requiresParentalConsent) {
|
||||
if (account.requiresParentalConsent) {
|
||||
// then there are only two options: static or nothing.
|
||||
// We use 'null' as a return value because the consumers of
|
||||
// getMode render nothing at all on a mode of null.
|
||||
@@ -228,13 +231,13 @@ export const visibilitiesSelector = createSelector(
|
||||
switch (accountPrivacy) {
|
||||
case 'custom':
|
||||
return {
|
||||
visibilityBio: preferences.visibilityBio || 'private',
|
||||
visibilityCourseCertificates: preferences.visibilityCourseCertificates || 'private',
|
||||
visibilityCountry: preferences.visibilityCountry || 'private',
|
||||
visibilityLevelOfEducation: preferences.visibilityLevelOfEducation || 'private',
|
||||
visibilityLanguageProficiencies: preferences.visibilityLanguageProficiencies || 'private',
|
||||
visibilityName: preferences.visibilityName || 'private',
|
||||
visibilitySocialLinks: preferences.visibilitySocialLinks || 'private',
|
||||
visibilityBio: preferences.visibilityBio || 'all_users',
|
||||
visibilityCourseCertificates: preferences.visibilityCourseCertificates || 'all_users',
|
||||
visibilityCountry: preferences.visibilityCountry || 'all_users',
|
||||
visibilityLevelOfEducation: preferences.visibilityLevelOfEducation || 'all_users',
|
||||
visibilityLanguageProficiencies: preferences.visibilityLanguageProficiencies || 'all_users',
|
||||
visibilityName: preferences.visibilityName || 'all_users',
|
||||
visibilitySocialLinks: preferences.visibilitySocialLinks || 'all_users',
|
||||
};
|
||||
case 'private':
|
||||
return {
|
||||
@@ -335,6 +338,7 @@ export const profilePageSelector = createSelector(
|
||||
profileImage,
|
||||
requiresParentalConsent: account.requiresParentalConsent,
|
||||
dateJoined: account.dateJoined,
|
||||
yearOfBirth: account.yearOfBirth,
|
||||
|
||||
// Bio form data
|
||||
bio: formValues.bio,
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { ValidationFormGroup } from '@edx/paragon';
|
||||
import { Form } from '@openedx/paragon';
|
||||
|
||||
import messages from './Bio.messages';
|
||||
|
||||
@@ -56,10 +56,9 @@ class Bio extends React.Component {
|
||||
editing: (
|
||||
<div role="dialog" aria-labelledby={`${formId}-label`}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<ValidationFormGroup
|
||||
for={formId}
|
||||
invalid={error !== null}
|
||||
invalidMessage={error}
|
||||
<Form.Group
|
||||
controlId={formId}
|
||||
isInvalid={error !== null}
|
||||
>
|
||||
<label className="edit-section-header" htmlFor={formId}>
|
||||
{intl.formatMessage(messages['profile.bio.about.me'])}
|
||||
@@ -71,7 +70,12 @@ class Bio extends React.Component {
|
||||
value={bio}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
{error !== null && (
|
||||
<Form.Control.Feedback hasIcon={false}>
|
||||
{error}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
<FormControls
|
||||
visibilityId="visibilityBio"
|
||||
saveState={saveState}
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
FormattedDate, FormattedMessage, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { connect } from 'react-redux';
|
||||
import get from 'lodash.get';
|
||||
|
||||
@@ -68,7 +68,7 @@ class Certificates extends React.Component {
|
||||
})();
|
||||
|
||||
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="certificate-type-illustration"
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { ValidationFormGroup } from '@edx/paragon';
|
||||
import { Form } from '@openedx/paragon';
|
||||
|
||||
import messages from './Country.messages';
|
||||
|
||||
@@ -67,10 +67,9 @@ class Country extends React.Component {
|
||||
editing: (
|
||||
<div role="dialog" aria-labelledby={`${formId}-label`}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<ValidationFormGroup
|
||||
for={formId}
|
||||
invalid={error !== null}
|
||||
invalidMessage={error}
|
||||
<Form.Group
|
||||
controlId={formId}
|
||||
isInvalid={error !== null}
|
||||
>
|
||||
<label className="edit-section-header" htmlFor={formId}>
|
||||
{intl.formatMessage(messages['profile.country.label'])}
|
||||
@@ -89,7 +88,12 @@ class Country extends React.Component {
|
||||
<option key={code} value={code}>{name}</option>
|
||||
))}
|
||||
</select>
|
||||
</ValidationFormGroup>
|
||||
{error !== null && (
|
||||
<Form.Control.Feedback hasIcon={false}>
|
||||
{error}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
<FormControls
|
||||
visibilityId="visibilityCountry"
|
||||
saveState={saveState}
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import get from 'lodash.get';
|
||||
import { ValidationFormGroup } from '@edx/paragon';
|
||||
import { Form } from '@openedx/paragon';
|
||||
|
||||
import messages from './Education.messages';
|
||||
|
||||
@@ -63,10 +63,9 @@ class Education extends React.Component {
|
||||
editing: (
|
||||
<div role="dialog" aria-labelledby={`${formId}-label`}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<ValidationFormGroup
|
||||
for={formId}
|
||||
invalid={error !== null}
|
||||
invalidMessage={error}
|
||||
<Form.Group
|
||||
controlId={formId}
|
||||
isInvalid={error !== null}
|
||||
>
|
||||
<label className="edit-section-header" htmlFor={formId}>
|
||||
{intl.formatMessage(messages['profile.education.education'])}
|
||||
@@ -90,7 +89,12 @@ class Education extends React.Component {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</ValidationFormGroup>
|
||||
{error !== null && (
|
||||
<Form.Control.Feedback hasIcon={false}>
|
||||
{error}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
<FormControls
|
||||
visibilityId="visibilityLevelOfEducation"
|
||||
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 { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { ValidationFormGroup } from '@edx/paragon';
|
||||
import { Form } from '@openedx/paragon';
|
||||
|
||||
import messages from './PreferredLanguage.messages';
|
||||
|
||||
@@ -77,10 +77,9 @@ class PreferredLanguage extends React.Component {
|
||||
editing: (
|
||||
<div role="dialog" aria-labelledby={`${formId}-label`}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<ValidationFormGroup
|
||||
for={formId}
|
||||
invalid={error !== null}
|
||||
invalidMessage={error}
|
||||
<Form.Group
|
||||
controlId={formId}
|
||||
isInvalid={error !== null}
|
||||
>
|
||||
<label className="edit-section-header" htmlFor={formId}>
|
||||
{intl.formatMessage(messages['profile.preferredlanguage.label'])}
|
||||
@@ -98,7 +97,12 @@ class PreferredLanguage extends React.Component {
|
||||
<option key={code} value={code}>{name}</option>
|
||||
))}
|
||||
</select>
|
||||
</ValidationFormGroup>
|
||||
{error !== null && (
|
||||
<Form.Control.Feedback hasIcon={false}>
|
||||
{error}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
<FormControls
|
||||
visibilityId="visibilityLanguageProficiencies"
|
||||
saveState={saveState}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
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 { ReactComponent as DefaultAvatar } from '../assets/avatar.svg';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StatusAlert } from '@edx/paragon';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import { connect } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
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 {
|
||||
constructor(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) {
|
||||
e.preventDefault();
|
||||
this.props.submitHandler(this.props.formId);
|
||||
@@ -91,6 +180,19 @@ class SocialLinks extends React.Component {
|
||||
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() {
|
||||
const {
|
||||
socialLinks, visibilitySocialLinks, editMode, saveState, error, intl,
|
||||
@@ -158,14 +260,19 @@ class SocialLinks extends React.Component {
|
||||
),
|
||||
editing: (
|
||||
<div role="dialog" aria-labelledby="social-links-label">
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<form aria-labelledby="editing-form" onSubmit={this.handleSubmit}>
|
||||
<EditableItemHeader
|
||||
headingId="social-links-label"
|
||||
content={intl.formatMessage(messages['profile.sociallinks.social.links'])}
|
||||
/>
|
||||
{/* TODO: Replace this alert with per-field errors. Needs API update. */}
|
||||
<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>
|
||||
<ul className="list-unstyled">
|
||||
{socialLinks.map(({ platform, socialLink }) => (
|
||||
@@ -238,113 +345,3 @@ export default connect(
|
||||
editableFormSelector,
|
||||
{},
|
||||
)(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 { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
import { Button } from '@openedx/paragon';
|
||||
|
||||
import messages from './EditButton.messages';
|
||||
|
||||
function EditButton({
|
||||
const EditButton = ({
|
||||
onClick, className, style, intl,
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
>
|
||||
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />
|
||||
{intl.formatMessage(messages['profile.editbutton.edit'])}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}) => (
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
>
|
||||
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />
|
||||
{intl.formatMessage(messages['profile.editbutton.edit'])}
|
||||
</Button>
|
||||
);
|
||||
|
||||
export default injectIntl(EditButton);
|
||||
|
||||
|
||||
@@ -4,26 +4,22 @@ import PropTypes from 'prop-types';
|
||||
import EditButton from './EditButton';
|
||||
import { Visibility } from './Visibility';
|
||||
|
||||
function EditableItemHeader({
|
||||
const EditableItemHeader = ({
|
||||
content,
|
||||
showVisibility,
|
||||
visibility,
|
||||
showEditButton,
|
||||
onClickEdit,
|
||||
headingId,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="editable-item-header mb-2">
|
||||
<h2 className="edit-section-header" id={headingId}>
|
||||
{content}
|
||||
{showEditButton ? <EditButton style={{ marginTop: '-.35rem' }} className="float-right px-0" onClick={onClickEdit} /> : null}
|
||||
</h2>
|
||||
{showVisibility ? <p className="mb-0"><Visibility to={visibility} /></p> : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}) => (
|
||||
<div className="editable-item-header mb-2">
|
||||
<h2 className="edit-section-header" id={headingId}>
|
||||
{content}
|
||||
{showEditButton ? <EditButton style={{ marginTop: '-.35rem' }} className="float-right px-0" onClick={onClickEdit} /> : null}
|
||||
</h2>
|
||||
{showVisibility ? <p className="mb-0"><Visibility to={visibility} /></p> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default EditableItemHeader;
|
||||
|
||||
|
||||
@@ -3,24 +3,22 @@ import PropTypes from 'prop-types';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
function EmptyContent({ children, onClick, showPlusIcon }) {
|
||||
return (
|
||||
<div>
|
||||
{onClick ? (
|
||||
<button
|
||||
type="button"
|
||||
className="pl-0 text-left btn btn-link"
|
||||
onClick={onClick}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { onClick(); } }}
|
||||
tabIndex={0}
|
||||
>
|
||||
{showPlusIcon ? <FontAwesomeIcon size="xs" className="mr-2" icon={faPlus} /> : null}
|
||||
{children}
|
||||
</button>
|
||||
) : children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const EmptyContent = ({ children, onClick, showPlusIcon }) => (
|
||||
<div>
|
||||
{onClick ? (
|
||||
<button
|
||||
type="button"
|
||||
className="pl-0 text-left btn btn-link"
|
||||
onClick={onClick}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { onClick(); } }}
|
||||
tabIndex={0}
|
||||
>
|
||||
{showPlusIcon ? <FontAwesomeIcon size="xs" className="mr-2" icon={faPlus} /> : null}
|
||||
{children}
|
||||
</button>
|
||||
) : children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default EmptyContent;
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React from 'react';
|
||||
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 messages from './FormControls.messages';
|
||||
|
||||
import { VisibilitySelect } from './Visibility';
|
||||
|
||||
function FormControls({
|
||||
const FormControls = ({
|
||||
cancelHandler, changeHandler, visibility, visibilityId, saveState, intl,
|
||||
}) {
|
||||
}) => {
|
||||
// Eliminate error/failed state for save button
|
||||
const buttonState = saveState === 'error' ? null : saveState;
|
||||
|
||||
@@ -57,7 +57,7 @@ function FormControls({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default injectIntl(FormControls);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TransitionReplace } from '@edx/paragon';
|
||||
import { TransitionReplace } from '@openedx/paragon';
|
||||
|
||||
const onChildExit = (htmlNode) => {
|
||||
// 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) => {
|
||||
if (cases[caseKey]) {
|
||||
if (typeof cases[caseKey] === 'string') {
|
||||
@@ -48,7 +48,7 @@ function SwitchContent({ expression, cases, className }) {
|
||||
{getContent(expression)}
|
||||
</TransitionReplace>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SwitchContent.propTypes = {
|
||||
expression: PropTypes.string,
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faEyeSlash, faEye } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
import messages from './Visibility.messages';
|
||||
|
||||
function Visibility({ to, intl }) {
|
||||
const Visibility = ({ to, intl }) => {
|
||||
const icon = to === 'private' ? faEyeSlash : faEye;
|
||||
const label = to === 'private'
|
||||
? 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 (
|
||||
<span className="ml-auto small text-muted">
|
||||
<FontAwesomeIcon icon={icon} /> {label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Visibility.propTypes = {
|
||||
to: PropTypes.oneOf(['private', 'all_users']),
|
||||
@@ -29,7 +30,7 @@ Visibility.defaultProps = {
|
||||
to: 'private',
|
||||
};
|
||||
|
||||
function VisibilitySelect({ intl, className, ...props }) {
|
||||
const VisibilitySelect = ({ intl, className, ...props }) => {
|
||||
const { value } = props;
|
||||
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' }}>
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
</span>
|
||||
<select className="d-inline-block w-auto form-control" {...props}>
|
||||
<select className="d-inline-block form-control" {...props}>
|
||||
<option key="private" value="private">
|
||||
{intl.formatMessage(messages['profile.visibility.who.just.me'])}
|
||||
</option>
|
||||
<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>
|
||||
</select>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
VisibilitySelect.propTypes = {
|
||||
id: PropTypes.string,
|
||||
|
||||
@@ -8,7 +8,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'profile.visibility.who.everyone': {
|
||||
id: 'profile.visibility.who.everyone',
|
||||
defaultMessage: 'Everyone on edX',
|
||||
defaultMessage: 'Everyone on {siteName}',
|
||||
description: 'What users can see this area?',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -23,6 +23,28 @@
|
||||
background-size: auto 85%;
|
||||
}
|
||||
|
||||
.username-description {
|
||||
width: auto;
|
||||
position: absolute;
|
||||
left: 1.5rem;
|
||||
top: 5.25rem;
|
||||
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: $gray-500;
|
||||
}
|
||||
|
||||
.profile-page {
|
||||
.edit-section-header {
|
||||
@extend .h6;
|
||||
|
||||
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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user