mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b2cc29b4a | ||
|
|
0efa419b35 | ||
|
|
cfd52144f4 | ||
|
|
239377dd7f | ||
|
|
f56b8ca3a4 | ||
|
|
5f5f0ffa14 | ||
|
|
aecae3efbe | ||
|
|
67ade3a323 | ||
|
|
6d2e74eeec | ||
|
|
eaeb7ea0da | ||
|
|
9e28be8246 | ||
|
|
40963e236f | ||
|
|
a0118751c7 | ||
|
|
6860b2b03f | ||
|
|
ec8cfdf5f7 | ||
|
|
2bca30d3c1 | ||
|
|
3dde285776 | ||
|
|
1304669416 | ||
|
|
9703bc8645 | ||
|
|
85d67dba19 | ||
|
|
ffc880c33b | ||
|
|
ae881df7f3 | ||
|
|
33dd2e5c0a | ||
|
|
dbe471fa96 | ||
|
|
afe411515d | ||
|
|
a214969527 | ||
|
|
bd16ff863d | ||
|
|
c0b0627d6a | ||
|
|
13a52e8d64 | ||
|
|
968dd14720 | ||
|
|
065af06709 | ||
|
|
b2ce217a41 | ||
|
|
e93e4756bf | ||
|
|
349c972f8b | ||
|
|
f515e22217 | ||
|
|
90c99727cb | ||
|
|
54ce58c48f | ||
|
|
175b677afd | ||
|
|
39b34d624a | ||
|
|
e9a781607a | ||
|
|
05a6af49c4 | ||
|
|
4b2aa04ef6 | ||
|
|
5481491bf2 | ||
|
|
eef55f33fc | ||
|
|
2ad5219e59 | ||
|
|
e08024a136 | ||
|
|
9f465ee7ce | ||
|
|
29a64d540b | ||
|
|
749189f0e5 | ||
|
|
41413a9e91 | ||
|
|
e65c71cae7 | ||
|
|
c7775cfdfd | ||
|
|
78473cb21f | ||
|
|
4a3bdb0633 | ||
|
|
fdafa1ca42 | ||
|
|
243f846f3b | ||
|
|
8f72ff7ca2 | ||
|
|
adf0b65fde | ||
|
|
3bb0cb9168 | ||
|
|
670084427d | ||
|
|
c8727e0f41 | ||
|
|
705c54f397 | ||
|
|
1b2929af29 | ||
|
|
2f71e1bf8e | ||
|
|
408a082f2d | ||
|
|
2e0a4df2be | ||
|
|
6965b33968 | ||
|
|
d2235f9918 | ||
|
|
5ebb99740b | ||
|
|
2978d6138c | ||
|
|
4dbd9e7df1 | ||
|
|
686096bfae | ||
|
|
ddb80a95c1 | ||
|
|
18cc8b9389 | ||
|
|
22467ac123 | ||
|
|
2407cb9594 | ||
|
|
3c04e6c7ec | ||
|
|
52491d81a9 | ||
|
|
77723e3fb3 | ||
|
|
bba14f502a | ||
|
|
44913864eb | ||
|
|
3c66401546 | ||
|
|
2e6a7c0559 | ||
|
|
e7fc33c667 | ||
|
|
1cc484b61e | ||
|
|
17fefcb4bd | ||
|
|
f4d520fbaa | ||
|
|
a5c08129ce | ||
|
|
7c859ad4c3 | ||
|
|
2b2a762623 | ||
|
|
b9cfddc5c0 | ||
|
|
7dca937de2 | ||
|
|
6cb3fc7be8 | ||
|
|
95211e6cb9 | ||
|
|
db322d192f | ||
|
|
bf98050d33 | ||
|
|
caa588f11d | ||
|
|
764ba4ab36 | ||
|
|
9c139eb703 | ||
|
|
c289ed149f | ||
|
|
05d80a0549 | ||
|
|
8bef21274d | ||
|
|
869fb9d029 | ||
|
|
cb077a87dc | ||
|
|
6d662e4469 | ||
|
|
901c96a67a | ||
|
|
6b5a690c4f | ||
|
|
9debb0d48d | ||
|
|
484ae2539b | ||
|
|
1dfce22b89 | ||
|
|
e36098369b | ||
|
|
aec0251c4f | ||
|
|
c9613337d4 | ||
|
|
ac67b0484b | ||
|
|
edac21a4f4 | ||
|
|
f75924e683 | ||
|
|
0966aa9463 | ||
|
|
30cd16fd56 | ||
|
|
86f4ae4b6d | ||
|
|
219fcdddea | ||
|
|
4464183b42 | ||
|
|
ed8d9e985c | ||
|
|
840fef84db | ||
|
|
6c9411d173 | ||
|
|
6afb300d86 | ||
|
|
894141619c | ||
|
|
7e135cef47 | ||
|
|
34726e2c4e | ||
|
|
1f6c29cee6 | ||
|
|
2234acc28e | ||
|
|
13e6645f26 | ||
|
|
09983e412d | ||
|
|
3563f7e2ae | ||
|
|
dc5a3e43a6 | ||
|
|
2e59f64738 | ||
|
|
91e08cb117 | ||
|
|
c4b6cb54e6 | ||
|
|
118301c2b4 | ||
|
|
d6ddd9c195 | ||
|
|
596c214f1c | ||
|
|
9b7f1dc43e | ||
|
|
c4cddefcff | ||
|
|
062ce58cf8 | ||
|
|
8b6a993587 | ||
|
|
26bd01cbda | ||
|
|
f492040a57 | ||
|
|
d7fd8ac714 | ||
|
|
3e273a76f1 | ||
|
|
7504dc6dfe | ||
|
|
0f9387dbb9 | ||
|
|
ec54376a53 | ||
|
|
0793784f5a | ||
|
|
00a7b11039 | ||
|
|
5138e79424 | ||
|
|
00eff653cb | ||
|
|
a59464e45d | ||
|
|
feeaa8ee8b | ||
|
|
1c4b8a0a67 | ||
|
|
8ad5675916 | ||
|
|
7a7d45cf91 | ||
|
|
e77e62236c | ||
|
|
8e142a5ecb | ||
|
|
3d57122a65 | ||
|
|
665d8c1ef0 | ||
|
|
bd2c4d4bfc | ||
|
|
81e68c8ef4 | ||
|
|
cb5a41f119 | ||
|
|
082287387a | ||
|
|
9143bd2b31 | ||
|
|
2def894237 | ||
|
|
7dd7f9ca54 | ||
|
|
b33cde03c3 | ||
|
|
d80cd6f0a2 | ||
|
|
ebb85d17ce | ||
|
|
efe84f3f98 | ||
|
|
335349963c | ||
|
|
495e3b009f | ||
|
|
2d82f4c217 | ||
|
|
6a70077398 | ||
|
|
d25ca0ef6f | ||
|
|
985fc05a47 | ||
|
|
73cbe4ab4b | ||
|
|
8d656b51a0 | ||
|
|
0a889fd1b3 | ||
|
|
c18c43e5c8 | ||
|
|
c22f8d0dc0 | ||
|
|
0799e9d8ee | ||
|
|
9645ccb6cd | ||
|
|
be51704b1f | ||
|
|
7e2d414f13 | ||
|
|
d2e87bcbda | ||
|
|
9d168c6af8 | ||
|
|
3a67768c51 | ||
|
|
e3ccb8560b | ||
|
|
c355e10453 | ||
|
|
e20e55b589 | ||
|
|
c13ea699d9 | ||
|
|
873932b6a6 | ||
|
|
bc42539fc4 | ||
|
|
5f608e9bc6 | ||
|
|
2bfa99fa1a | ||
|
|
c12fe526b8 | ||
|
|
fc3bc0a542 | ||
|
|
78019396e2 | ||
|
|
7d31487275 | ||
|
|
f5c3f7e9bc | ||
|
|
9e127d2300 | ||
|
|
172bc22af5 | ||
|
|
4be9e74ed9 | ||
|
|
f4d134d0e2 | ||
|
|
752825db68 | ||
|
|
9a36473c8f | ||
|
|
3fa2af975c | ||
|
|
57a77a7edf | ||
|
|
6caae2df97 | ||
|
|
c00e534984 | ||
|
|
b638b783fe | ||
|
|
de18ab1e15 | ||
|
|
75751285fe | ||
|
|
04390c5acd | ||
|
|
ce25cfb159 | ||
|
|
e8f73ddb87 | ||
|
|
e6f796e99c | ||
|
|
0507e750b9 | ||
|
|
fe4cddda60 | ||
|
|
63347dc0eb | ||
|
|
426025f8c2 | ||
|
|
6fd4d1576e | ||
|
|
26ca3622c2 | ||
|
|
ef7693afa5 | ||
|
|
de9383e657 | ||
|
|
1e517469c3 | ||
|
|
66934cbb82 | ||
|
|
2246b16cf4 | ||
|
|
1fc07e8649 | ||
|
|
4e9b19c7f6 | ||
|
|
a8f794ef76 | ||
|
|
6825b4c999 | ||
|
|
54fb334288 | ||
|
|
798732cbcf | ||
|
|
e18d92944a | ||
|
|
f0498fb3ba | ||
|
|
0f938b19a3 | ||
|
|
bcd18ccb32 | ||
|
|
c6dd182f56 | ||
|
|
cae974d0df | ||
|
|
c41320e155 | ||
|
|
66ca985405 | ||
|
|
a366c6efa4 | ||
|
|
366f40a6e4 | ||
|
|
9da61b5991 | ||
|
|
9feeaf4dca | ||
|
|
7f9e09461c | ||
|
|
d15694041a | ||
|
|
59cdc9fd5d | ||
|
|
b196bb379a | ||
|
|
c7242f5d98 | ||
|
|
ace6cbb158 | ||
|
|
0523faee2b | ||
|
|
e4834027b0 | ||
|
|
303e2e84c5 | ||
|
|
04fd056cab | ||
|
|
74b545a8d9 | ||
|
|
957fd34168 | ||
|
|
dd836ad28e | ||
|
|
196ae5e29c | ||
|
|
f4e4c1d810 | ||
|
|
f3469f3dc4 | ||
|
|
d101989ebd | ||
|
|
b36c70acc1 | ||
|
|
25359ee88d | ||
|
|
6a06cb4684 | ||
|
|
12652d59c4 | ||
|
|
8e8458781d | ||
|
|
852b21e264 | ||
|
|
81d049e533 | ||
|
|
3fe8a8fb8d | ||
|
|
0f6cc20478 | ||
|
|
26c109e8ae | ||
|
|
1e34af6aa3 | ||
|
|
60c62e4f1a | ||
|
|
30dddf387c | ||
|
|
0528106fdf | ||
|
|
bdd207524c | ||
|
|
4be19fd028 | ||
|
|
66202d1fd6 | ||
|
|
d98a76272c | ||
|
|
15d0ecbe51 | ||
|
|
ca653fd5ff | ||
|
|
468a1683ce | ||
|
|
d827ebef72 | ||
|
|
65588f234a | ||
|
|
bb2ea77008 | ||
|
|
392c5511eb | ||
|
|
a9be049bf6 | ||
|
|
bf752a50ca | ||
|
|
a402817cc9 | ||
|
|
669eb360df | ||
|
|
25dd979a37 | ||
|
|
e653a5f9c0 | ||
|
|
5d26f23b7b | ||
|
|
e2a361276b | ||
|
|
319bca3ba8 | ||
|
|
02ca59638a | ||
|
|
b2cf5efed6 | ||
|
|
7258c3dd08 | ||
|
|
4bbd561a1b | ||
|
|
49d20db26f | ||
|
|
14d035adb1 | ||
|
|
844ce5dbfe | ||
|
|
8f1d9f051e | ||
|
|
a111439541 | ||
|
|
72331699b5 | ||
|
|
544ef081dc | ||
|
|
ddf3816f5a | ||
|
|
7d022ed380 | ||
|
|
40539bf3b7 | ||
|
|
c1a26d2e71 | ||
|
|
6f55189658 | ||
|
|
3c9b4bd20e | ||
|
|
2a6543bcea | ||
|
|
716a61aa5c | ||
|
|
127f0c1e20 | ||
|
|
fa83ba9556 | ||
|
|
317541a63b | ||
|
|
fd5eb7d23d | ||
|
|
06f308c182 | ||
|
|
2345daf783 | ||
|
|
df2bce4d53 | ||
|
|
c99411b8fd | ||
|
|
022a57479d | ||
|
|
56001d7780 | ||
|
|
a010302242 | ||
|
|
f306d54924 | ||
|
|
c1ff7e9c84 | ||
|
|
3255b8d23b | ||
|
|
8ea3ab9c02 | ||
|
|
ec9a57bede | ||
|
|
5f3da9a09a | ||
|
|
dbfd0fd25e | ||
|
|
ac62cbf348 | ||
|
|
08b1cada7e | ||
|
|
4960b34748 | ||
|
|
c612902779 | ||
|
|
3938b7883f | ||
|
|
e048491221 | ||
|
|
68598d3c45 | ||
|
|
7ca89435c6 | ||
|
|
cc320cf80a | ||
|
|
3c52d58a6e | ||
|
|
f282aa47c9 | ||
|
|
426d7dbfec | ||
|
|
ee78f08598 | ||
|
|
519480c0e4 | ||
|
|
45d6800143 | ||
|
|
e3cdad1f2a | ||
|
|
66cfd20f7b | ||
|
|
af40a85071 | ||
|
|
b11a730373 | ||
|
|
b709cb485e | ||
|
|
13fc91b355 | ||
|
|
9eec280868 | ||
|
|
45d4c79c26 | ||
|
|
1cf2bda36c | ||
|
|
b1adfe8c46 | ||
|
|
6aece16fd0 | ||
|
|
2684ac4933 | ||
|
|
1f0e9da5d0 | ||
|
|
bfa2f6edb9 | ||
|
|
ea7998257c | ||
|
|
25c178b267 | ||
|
|
7885b025ee | ||
|
|
3606726c4b | ||
|
|
d69b1f98c8 | ||
|
|
77d83e04d5 | ||
|
|
86c4d70daf | ||
|
|
e256b0da8a | ||
|
|
93bda87ad0 | ||
|
|
1ccd36e9e9 | ||
|
|
bf77ddc49b | ||
|
|
ebe6341b01 | ||
|
|
34a0e314e8 | ||
|
|
9762f34324 | ||
|
|
951f16f0a1 | ||
|
|
9ce14acc9a | ||
|
|
988b39f19d | ||
|
|
9333b407bb | ||
|
|
8867fd54f3 | ||
|
|
56bd3041d6 | ||
|
|
a53bb6650a | ||
|
|
0a99ae50b1 | ||
|
|
6bad3fdc2f | ||
|
|
c19a6c8b10 | ||
|
|
0abc2ca243 | ||
|
|
d56c19016a | ||
|
|
bb9a93986e | ||
|
|
c906b2df5e | ||
|
|
204b442e2f | ||
|
|
876f4ef700 | ||
|
|
7f02d4f39c | ||
|
|
c1f5450b9a | ||
|
|
7f291cf056 | ||
|
|
7706a0521c | ||
|
|
81d2927c0f | ||
|
|
f2698298f7 | ||
|
|
a4e8ea93bc | ||
|
|
ca49b373bf | ||
|
|
4c6ec75cd7 | ||
|
|
07826a9bf4 | ||
|
|
f79f521d3d | ||
|
|
7e465f7317 | ||
|
|
943c3e93a4 | ||
|
|
f070d47c2d | ||
|
|
4eda547734 | ||
|
|
5865c30155 | ||
|
|
838083e76d | ||
|
|
4d65052eef | ||
|
|
eec7d0fb63 | ||
|
|
d837562089 | ||
|
|
fddba1e059 | ||
|
|
1e85894a0c | ||
|
|
9b4dcd8d32 | ||
|
|
54ec9ea9a6 | ||
|
|
f51eda4f5e | ||
|
|
be40594ecf | ||
|
|
f340cdd4b7 | ||
|
|
24b6a4f16f | ||
|
|
f2e63b7621 | ||
|
|
6d3d717ab3 | ||
|
|
1dcf297a45 | ||
|
|
8cdc881f3d | ||
|
|
9187231737 | ||
|
|
2bc60b1211 | ||
|
|
90f1ef050b | ||
|
|
eb5b736484 | ||
|
|
92b506c85a | ||
|
|
2269c91244 | ||
|
|
210b0ab907 | ||
|
|
1337873adf | ||
|
|
d922761f85 | ||
|
|
737b77a6ac | ||
|
|
11b38f7ea0 | ||
|
|
e5c3474455 | ||
|
|
f89a5013d2 | ||
|
|
139d319965 | ||
|
|
89c929f970 | ||
|
|
6e813e9663 | ||
|
|
1f2caada3f | ||
|
|
14dd9acd3b | ||
|
|
d35d0e354e | ||
|
|
ec38b837aa | ||
|
|
d055e0b28b | ||
|
|
1f6d38c4ad | ||
|
|
eef2578e61 | ||
|
|
7127aeaa87 | ||
|
|
44648fa901 | ||
|
|
acc2e00c54 | ||
|
|
7d6fd69a3b | ||
|
|
75d1bb2f22 | ||
|
|
1e372b583a | ||
|
|
1b373b5af4 | ||
|
|
d0d9a38177 | ||
|
|
9d8867be9d | ||
|
|
fb7ff249b8 | ||
|
|
bbdd69b2a0 | ||
|
|
3687fc1702 | ||
|
|
caf2b88ee8 | ||
|
|
2ef3e29e7d | ||
|
|
6a3be92a4c | ||
|
|
845c99de10 | ||
|
|
9c7b6e007a | ||
|
|
4c3d3fb4b0 | ||
|
|
7036aa5bad | ||
|
|
1898941097 | ||
|
|
24f7405065 | ||
|
|
e1ead8f500 | ||
|
|
5db8209182 | ||
|
|
9c75f3fa59 | ||
|
|
25ac66ec59 | ||
|
|
bdcc9273f1 | ||
|
|
dca85ad127 | ||
|
|
36f7c31a03 | ||
|
|
63bd6309d3 | ||
|
|
bf76f0453b | ||
|
|
00677148b9 | ||
|
|
7bdfb55a1c | ||
|
|
c43051c6dd | ||
|
|
20453dfddd | ||
|
|
20f9bba845 | ||
|
|
25e29cb79a | ||
|
|
7f25f46ac0 | ||
|
|
d3c1286d1a | ||
|
|
13bf20daf0 | ||
|
|
e61c09f017 | ||
|
|
395793445c | ||
|
|
abc6cf6366 | ||
|
|
eb33eb72d1 | ||
|
|
1a39635c0e | ||
|
|
13347a6baf | ||
|
|
ff016e22fe | ||
|
|
579644c910 | ||
|
|
54b504eb8e | ||
|
|
4490ea5202 | ||
|
|
fb26d67e42 | ||
|
|
07052824d7 | ||
|
|
a2eb474a5c | ||
|
|
2bcb324582 | ||
|
|
37d1f3361c | ||
|
|
6e8b28cc8a | ||
|
|
9513502667 | ||
|
|
55c2c00b49 | ||
|
|
8545be307d | ||
|
|
04991a9856 | ||
|
|
978e1c8d4c | ||
|
|
6700698e52 | ||
|
|
c0f266dfa5 | ||
|
|
a40b287226 | ||
|
|
615be40b45 | ||
|
|
99d66fef52 | ||
|
|
c3a5da3ae6 | ||
|
|
ec852601a4 | ||
|
|
791025b153 | ||
|
|
be72e2effd | ||
|
|
717d5e6566 | ||
|
|
c07ec23a20 | ||
|
|
6260339508 | ||
|
|
10754a65c6 | ||
|
|
c3970a46a2 | ||
|
|
c9bc9a4b10 | ||
|
|
7b5993bc60 | ||
|
|
b42cce5747 | ||
|
|
9c595b7f84 | ||
|
|
acdaaffd4e | ||
|
|
c01b192a73 | ||
|
|
7538e6e8fd | ||
|
|
49c350376b | ||
|
|
4a259d09e4 | ||
|
|
918aa6988b | ||
|
|
7e4831e309 | ||
|
|
14034c1364 | ||
|
|
5a379b2cd8 | ||
|
|
ad6a663179 | ||
|
|
b185d61993 | ||
|
|
9f780a64cc | ||
|
|
88b44c3bf6 | ||
|
|
e83f99e389 | ||
|
|
ce4ab4e86c | ||
|
|
5cd6322284 | ||
|
|
0ab6cc3a0f | ||
|
|
c617a3446d | ||
|
|
d76fe5350e | ||
|
|
927aa54feb | ||
|
|
fc272ed1e1 | ||
|
|
882251864a | ||
|
|
4eaa234dd8 | ||
|
|
bf1fbfe03f | ||
|
|
66f068359f | ||
|
|
d3f93644db | ||
|
|
b9412dcb10 | ||
|
|
1125351cd1 | ||
|
|
c683dd8d32 | ||
|
|
843908dab2 | ||
|
|
ad35a04cdf | ||
|
|
2c87dac897 | ||
|
|
d5bf44ada8 | ||
|
|
7bfbeac368 | ||
|
|
ca55097b5c | ||
|
|
ee0c510569 | ||
|
|
3d9eec825c | ||
|
|
6d777944aa | ||
|
|
841398c6c1 | ||
|
|
f8c1f1e63d | ||
|
|
c7176a4660 | ||
|
|
e2534e7ca8 | ||
|
|
d09948e7f6 | ||
|
|
270f865ccd | ||
|
|
e52793e918 | ||
|
|
a8e09d4c89 | ||
|
|
7bdaf74e1a | ||
|
|
f04c3cc059 | ||
|
|
8c5040d88b | ||
|
|
3ca3b59802 | ||
|
|
fa6aa4ae2d | ||
|
|
f6e5df2088 | ||
|
|
e343958b07 | ||
|
|
298cb5322e | ||
|
|
d7097ff729 | ||
|
|
94f2b4397e | ||
|
|
75e89591a8 | ||
|
|
72b2211daa | ||
|
|
0850b1f982 | ||
|
|
375fbd3790 | ||
|
|
82c84de33f | ||
|
|
57972b3fb9 | ||
|
|
9dcb8f8f0e | ||
|
|
148d329ff8 | ||
|
|
517124cc93 | ||
|
|
797cea4f63 | ||
|
|
cb2f032ae1 | ||
|
|
0572a1c97d | ||
|
|
bb8a75937a | ||
|
|
8060d9c22e | ||
|
|
783e485aa5 | ||
|
|
6f96bd416c | ||
|
|
20e3d9065f | ||
|
|
7b6c59b6f1 | ||
|
|
0af8a31154 | ||
|
|
140bba23e0 | ||
|
|
ae0407ed5e | ||
|
|
a5d7bca8c0 | ||
|
|
8a40093399 | ||
|
|
b3d76bba05 | ||
|
|
e5a00d0793 | ||
|
|
5f0f97edbb | ||
|
|
19c46d3b32 | ||
|
|
5b356e6249 | ||
|
|
20878ba040 | ||
|
|
69e4c5478d | ||
|
|
0d70c87ea9 | ||
|
|
d3ce68e078 | ||
|
|
4fa46c4144 | ||
|
|
c2a8a3e4a3 | ||
|
|
01707a5150 | ||
|
|
5488d5a7fa | ||
|
|
c8ab5cdbf6 | ||
|
|
4fb9910221 | ||
|
|
7ae0df2cbc | ||
|
|
8bf2fe7b06 | ||
|
|
c0f8fce442 | ||
|
|
47e32d5d6d | ||
|
|
111af0acf8 | ||
|
|
e34f96832a | ||
|
|
f9e289561e | ||
|
|
e409d071cb | ||
|
|
2d6a05433a | ||
|
|
3c97ce3f66 | ||
|
|
f8c1733e14 | ||
|
|
2e726c9d6a | ||
|
|
7f75255282 | ||
|
|
918b863820 | ||
|
|
3c5991453d | ||
|
|
3776c4d050 | ||
|
|
5925a4b956 | ||
|
|
dafd703261 | ||
|
|
5c0f546d50 | ||
|
|
e8e9a4b5ed | ||
|
|
1371776f74 | ||
|
|
deb2709829 | ||
|
|
4cc35e5b8f | ||
|
|
ad68346332 | ||
|
|
97406e1b18 | ||
|
|
483b306e69 | ||
|
|
0bce215d33 | ||
|
|
73666399b4 | ||
|
|
60e890c244 | ||
|
|
16b5bd5d09 | ||
|
|
37eafd2fed | ||
|
|
e2cb3fdd3d | ||
|
|
2233d993c2 | ||
|
|
5d51f71f0c | ||
|
|
a989a0b20e | ||
|
|
4f3a400358 | ||
|
|
f67f1e3b52 | ||
|
|
570c0edfb3 | ||
|
|
c1b5216647 | ||
|
|
b2cc4bdb12 | ||
|
|
0a793938d6 | ||
|
|
7d71452ab5 | ||
|
|
29a60c1241 | ||
|
|
b46b392af4 | ||
|
|
998cb8d380 | ||
|
|
b8ffe25212 | ||
|
|
df0ab7c1dc | ||
|
|
519464ce57 | ||
|
|
f27de815e6 | ||
|
|
e8553c5035 | ||
|
|
349341400d | ||
|
|
7b92e58ff3 | ||
|
|
f2f7742709 | ||
|
|
689fb4baf0 | ||
|
|
650f98a080 | ||
|
|
88d3a53165 | ||
|
|
6e88976294 | ||
|
|
f25cc56ddb | ||
|
|
4134b2edb2 | ||
|
|
5b886b6eea | ||
|
|
6c80de2981 | ||
|
|
b9b3355317 | ||
|
|
88e9faea00 | ||
|
|
b1c9fbedd8 | ||
|
|
c9b7eb3494 | ||
|
|
66287f5b7d | ||
|
|
b61eeeba74 | ||
|
|
2bde40777d | ||
|
|
2179632c94 | ||
|
|
4342161a70 | ||
|
|
a48688ad35 | ||
|
|
1b4f783abd | ||
|
|
e2d8562868 | ||
|
|
91602b9d34 | ||
|
|
eba530eda0 | ||
|
|
8e1cfc3a29 | ||
|
|
c78bb598e7 | ||
|
|
f12abbf39e | ||
|
|
7dcb1dfbaa | ||
|
|
1fce2451c7 | ||
|
|
e4f1f5d08b | ||
|
|
d06ff8c93c | ||
|
|
9d9056ef86 | ||
|
|
1b61ca8abf | ||
|
|
f75a743090 | ||
|
|
82e617b19b | ||
|
|
7a4b87f743 | ||
|
|
71f0887a82 | ||
|
|
4116e206a3 | ||
|
|
e78f17460f | ||
|
|
70bfd91359 | ||
|
|
b3432a5e0d | ||
|
|
382cb42dcf | ||
|
|
dbd106d523 | ||
|
|
e8f94d4d4a | ||
|
|
3ea141d4b0 | ||
|
|
f783c8423d | ||
|
|
9d4a9861f2 | ||
|
|
6d69286df6 | ||
|
|
3121ffb55a | ||
|
|
c0af51fbc0 | ||
|
|
ad85b0fcc8 | ||
|
|
5ad73b1c14 | ||
|
|
bf83f462e0 | ||
|
|
8dd79a19c3 | ||
|
|
a8368eb78d | ||
|
|
e88bac6e0f | ||
|
|
281d1f65f1 | ||
|
|
2704d71b09 | ||
|
|
583ab0ff74 | ||
|
|
ad5c0444f7 | ||
|
|
2a74e75506 | ||
|
|
fb7ad76b8b | ||
|
|
eddb471366 | ||
|
|
35c88ffab4 | ||
|
|
1cdef8f864 | ||
|
|
82fd56dc67 | ||
|
|
0e01d40aa7 | ||
|
|
f7d1543aa3 | ||
|
|
d2ebc81e25 | ||
|
|
278e1cec1d | ||
|
|
31436e627d | ||
|
|
4e50865927 | ||
|
|
6ad218782e | ||
|
|
8f1f875d76 | ||
|
|
9fb8e01cd2 | ||
|
|
950a35310b | ||
|
|
12884cce01 | ||
|
|
f8d153cf95 | ||
|
|
c871f1c797 | ||
|
|
9af7ae0357 | ||
|
|
073eea2847 | ||
|
|
9cb6ff3847 | ||
|
|
c699e044fd | ||
|
|
7d3ec7da1e | ||
|
|
5d288da69d | ||
|
|
4ef7abc132 | ||
|
|
9e486f3a9d | ||
|
|
fbcd390220 | ||
|
|
338c23dc3d | ||
|
|
4d6f38a3d9 | ||
|
|
5b8f335b2a | ||
|
|
f0330ed21c | ||
|
|
02da749c65 | ||
|
|
5a1393c039 | ||
|
|
d7a5e613fa | ||
|
|
75a944eba8 | ||
|
|
9a0fb95d06 | ||
|
|
31655847a9 | ||
|
|
6655d06d80 | ||
|
|
6850dd1a53 | ||
|
|
ecb01224fd | ||
|
|
fdd5cbb0d7 | ||
|
|
eb9827a3ae | ||
|
|
d207b240f2 | ||
|
|
d9654b52cf | ||
|
|
d3965924aa | ||
|
|
36d535546c | ||
|
|
19568afde5 | ||
|
|
2b312dbf8e | ||
|
|
5446abe3a9 | ||
|
|
c66c100dcb | ||
|
|
d4051f6828 | ||
|
|
33781c3d16 | ||
|
|
defc4049cb | ||
|
|
243d0c1493 | ||
|
|
a448c69a2a | ||
|
|
0b774402cf | ||
|
|
0bda44e97f | ||
|
|
60f6c63c8a | ||
|
|
62c2d551f8 | ||
|
|
f32bb5ce8f | ||
|
|
5e6d22f1af | ||
|
|
34289e3340 | ||
|
|
98ef60a48e | ||
|
|
8323344d7d | ||
|
|
fbecc363cf | ||
|
|
2779734ec0 | ||
|
|
5548deb6b5 | ||
|
|
df925f5a1a | ||
|
|
30073b88f1 | ||
|
|
51bd109f13 | ||
|
|
b12d5f6bd7 | ||
|
|
f32e4523e8 | ||
|
|
fadf3b5106 | ||
|
|
ed959ca07a | ||
|
|
588c1c201c | ||
|
|
f005d1e5f8 | ||
|
|
1d12c3629a | ||
|
|
a1a0671382 | ||
|
|
e66e23bbf6 | ||
|
|
26abd02032 | ||
|
|
8e582bff45 | ||
|
|
8df4b79f97 | ||
|
|
74c90d43a7 | ||
|
|
454fa752e2 | ||
|
|
83d4408ddf | ||
|
|
1322db89bc | ||
|
|
6f4f69ac3e | ||
|
|
14b5ccb53e | ||
|
|
2e4229ce18 | ||
|
|
6bc9e6a0bb | ||
|
|
a0ee5c6414 | ||
|
|
48ef9b162d | ||
|
|
2d70caf03d | ||
|
|
419784f6d1 | ||
|
|
674ab19d06 | ||
|
|
c53fe4b197 | ||
|
|
67c81c0168 | ||
|
|
e44b37538e | ||
|
|
56beac9a1f | ||
|
|
0596547223 | ||
|
|
ca6e839ca4 | ||
|
|
6557c6036f | ||
|
|
a3d83efd7d | ||
|
|
0483aae369 | ||
|
|
cbf3e4734d | ||
|
|
0c484954df | ||
|
|
b672b0a4e0 | ||
|
|
c84408004c | ||
|
|
4b0d64dfc6 | ||
|
|
a78bff5ee6 | ||
|
|
a8c0ff0c0a | ||
|
|
fc824b1fbe | ||
|
|
321f3117a4 | ||
|
|
09839ab024 | ||
|
|
da2b9aca03 | ||
|
|
20d0cbaf27 | ||
|
|
c177403628 | ||
|
|
b7704fb2ca | ||
|
|
2437401daf | ||
|
|
7f189cd4d4 | ||
|
|
16e3924d3f | ||
|
|
d510905ba2 | ||
|
|
d4889eb52c | ||
|
|
f2fe095b43 | ||
|
|
13865ff29e | ||
|
|
9386912b9b | ||
|
|
95d356de10 | ||
|
|
f2dccf7845 | ||
|
|
e442961dbb | ||
|
|
d58e54befa | ||
|
|
0fe9577abf | ||
|
|
9703b86784 | ||
|
|
e2af3470dc | ||
|
|
741419f47b | ||
|
|
4fc305ebff | ||
|
|
db8dd5a487 | ||
|
|
d8d15f27ab | ||
|
|
6119503057 | ||
|
|
6e6ddbf378 | ||
|
|
5793ba45c3 | ||
|
|
903973ce4b | ||
|
|
c6c7961dc9 | ||
|
|
fd0b475b84 | ||
|
|
b69195253b | ||
|
|
a537d9ac17 | ||
|
|
c742101250 | ||
|
|
dcbae2a117 | ||
|
|
e530003c72 | ||
|
|
48e87c4659 | ||
|
|
9ae32b9366 | ||
|
|
e782776ab5 | ||
|
|
75d914b894 | ||
|
|
69c93cd048 | ||
|
|
e3a8656e32 | ||
|
|
137c5889eb | ||
|
|
3b768c144b | ||
|
|
f19d6b6800 | ||
|
|
90daab8219 | ||
|
|
45e27fd6ad | ||
|
|
2706e47f85 | ||
|
|
43e9da9cbc | ||
|
|
dbe88e58f4 | ||
|
|
debda80db1 | ||
|
|
1756c7c050 | ||
|
|
01b8c7aee6 | ||
|
|
7b9d7163e1 | ||
|
|
e6e5e5506e | ||
|
|
8f12fcd09b | ||
|
|
7ec239fd68 | ||
|
|
5299ea674e | ||
|
|
6a3c80468b | ||
|
|
aa4b386ee6 | ||
|
|
0459eceee7 | ||
|
|
367ec00a02 | ||
|
|
e34ba248b9 | ||
|
|
6d6030d60a | ||
|
|
e941f00384 | ||
|
|
e17b15ec2c | ||
|
|
c98c9b3369 | ||
|
|
73cec00fb9 | ||
|
|
90a9ff251d | ||
|
|
a626dfafc8 | ||
|
|
b4aeee9433 | ||
|
|
e8e103020c | ||
|
|
ff6e41f2e1 | ||
|
|
aabd152314 | ||
|
|
6a17d04570 | ||
|
|
32547a214b | ||
|
|
ef40e9dce5 | ||
|
|
e090ba9123 | ||
|
|
81f825d4b3 | ||
|
|
ecf02f454c | ||
|
|
9f823c0b11 | ||
|
|
401d8a423d | ||
|
|
d80401a49b | ||
|
|
cb768d3190 | ||
|
|
7be888bfa8 | ||
|
|
77c927ac59 | ||
|
|
0257e09a40 | ||
|
|
61df0e1eeb | ||
|
|
1b7f78b562 | ||
|
|
9e84c72148 | ||
|
|
7034003fd1 | ||
|
|
b5bcbc4fc8 | ||
|
|
04e6f5b52d | ||
|
|
76a255fb7b | ||
|
|
1f3722edc1 | ||
|
|
3f7ddddba3 | ||
|
|
91931b2e6f | ||
|
|
cab94842aa | ||
|
|
86c3c286ee | ||
|
|
d6e7bb0a0a | ||
|
|
970a93360a | ||
|
|
7d9b588bd2 | ||
|
|
13c38c2215 | ||
|
|
2b08ef5c42 | ||
|
|
b7ea9e7d97 | ||
|
|
d85db01752 | ||
|
|
69c03c9190 | ||
|
|
00de72a6eb |
+24
-22
@@ -1,32 +1,34 @@
|
||||
**/*.html
|
||||
dist
|
||||
docs
|
||||
**/*.html
|
||||
node_modules
|
||||
plugins/*
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
public
|
||||
|
||||
!plugins/talk-plugin-akismet
|
||||
!plugins/talk-plugin-auth
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-offtopic
|
||||
!plugins/talk-plugin-like
|
||||
!plugins/talk-plugin-mod
|
||||
!plugins/talk-plugin-love
|
||||
!plugins/talk-plugin-viewing-options
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-flag-details
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-like
|
||||
!plugins/talk-plugin-love
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-mod
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-offtopic
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
!plugins/talk-plugin-sort-most-loved
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-subscriber
|
||||
!plugins/talk-plugin-flag-details
|
||||
|
||||
node_modules
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-viewing-options
|
||||
@@ -13,6 +13,7 @@ client/coral-framework/graphql/introspection.json
|
||||
.idea/
|
||||
*.swp
|
||||
*.DS_STORE
|
||||
.prettierrc.json
|
||||
|
||||
coverage/
|
||||
test/e2e/reports/
|
||||
@@ -22,6 +23,7 @@ browserstack.err
|
||||
|
||||
plugins.json
|
||||
plugins/*
|
||||
!plugins/talk-plugin-akismet
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
!plugins/talk-plugin-auth
|
||||
!plugins/talk-plugin-respect
|
||||
@@ -52,3 +54,4 @@ plugins/*
|
||||
!plugins/talk-plugin-slack-notifications
|
||||
|
||||
**/node_modules/*
|
||||
yarn-error.log
|
||||
|
||||
+7
-1
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"exec": "npm-run-all --parallel generate-introspection start:development",
|
||||
"verbose": true,
|
||||
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
|
||||
"ext": "js,json,graphql"
|
||||
"ext": "js,json,graphql,yml",
|
||||
"watch": [
|
||||
".",
|
||||
"bin/cli",
|
||||
"bin/cli-serve"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ ONBUILD ARG TALK_THREADING_LEVEL=3
|
||||
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
|
||||
ONBUILD ARG TALK_DEFAULT_LANG=en
|
||||
ONBUILD ARG TALK_PLUGINS_JSON
|
||||
ONBUILD ARG TALK_WEBPACK_SOURCE_MAP
|
||||
|
||||
# Bundle app source
|
||||
ONBUILD COPY . /usr/src/app
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2017 Mozilla Foundation
|
||||
Copyright 2018 Mozilla Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,16 +1,38 @@
|
||||
const express = require('express');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
const uuid = require('uuid');
|
||||
const merge = require('lodash/merge');
|
||||
const helmet = require('helmet');
|
||||
const plugins = require('./services/plugins');
|
||||
const compression = require('compression');
|
||||
const {HELMET_CONFIGURATION} = require('./config');
|
||||
const {MOUNT_PATH} = require('./url');
|
||||
const { HELMET_CONFIGURATION } = require('./config');
|
||||
const { MOUNT_PATH } = require('./url');
|
||||
const routes = require('./routes');
|
||||
const debug = require('debug')('talk:app');
|
||||
const { ENABLE_TRACING, APOLLO_ENGINE_KEY, PORT } = require('./config');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Request Identity Middleware
|
||||
app.use((req, res, next) => {
|
||||
req.id = uuid.v4();
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// PLUGIN PRE APPLICATION MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
// Inject server route plugins.
|
||||
plugins.get('server', 'app').forEach(({ plugin, app: callback }) => {
|
||||
debug(`added plugin '${plugin.name}'`);
|
||||
|
||||
// Pass the app to the plugin to mount it's routes.
|
||||
callback(app);
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// APPLICATION WIDE MIDDLEWARE
|
||||
//==============================================================================
|
||||
@@ -20,15 +42,35 @@ if (process.env.NODE_ENV !== 'test') {
|
||||
app.use(morgan('dev'));
|
||||
}
|
||||
|
||||
if (ENABLE_TRACING && APOLLO_ENGINE_KEY) {
|
||||
const { Engine } = require('apollo-engine');
|
||||
|
||||
const engine = new Engine({
|
||||
engineConfig: {
|
||||
apiKey: APOLLO_ENGINE_KEY,
|
||||
},
|
||||
graphqlPort: PORT,
|
||||
endpoint: `${MOUNT_PATH}api/v1/graph/ql`,
|
||||
});
|
||||
|
||||
engine.start();
|
||||
|
||||
app.use(engine.expressMiddleware());
|
||||
}
|
||||
|
||||
// Trust the first proxy in front of us, this will enable us to trust the fact
|
||||
// that SSL was terminated correctly.
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
// Enable a suite of security good practices through helmet. We disable
|
||||
// frameguard to allow crossdomain injection of the embed.
|
||||
app.use(helmet(merge(HELMET_CONFIGURATION, {
|
||||
frameguard: false,
|
||||
})));
|
||||
app.use(
|
||||
helmet(
|
||||
merge(HELMET_CONFIGURATION, {
|
||||
frameguard: false,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Compress the responses if appropriate.
|
||||
app.use(compression());
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const {head, map} = require('lodash');
|
||||
require('./util');
|
||||
const program = require('commander');
|
||||
const { head, map } = require('lodash');
|
||||
const Matcher = require('did-you-mean');
|
||||
|
||||
program
|
||||
@@ -17,7 +18,6 @@ program
|
||||
.command('token', 'work with the access tokens')
|
||||
.command('users', 'work with the application auth')
|
||||
.command('migration', 'provides utilities for migrating the database')
|
||||
.command('verify', 'provides utilities for performing data verification')
|
||||
.command(
|
||||
'plugins',
|
||||
'provides utilities for interacting with the plugin system'
|
||||
@@ -25,33 +25,18 @@ program
|
||||
.parse(process.argv);
|
||||
|
||||
// If the command wasn't found, output help.
|
||||
const cmds = map(program.commands, '_name');
|
||||
const cmd = head(program.args);
|
||||
if (!cmds.includes(cmd)) {
|
||||
const m = new Matcher(cmds);
|
||||
const similarCMDs = m.list(cmd);
|
||||
const commands = map(program.commands, '_name');
|
||||
const command = head(program.args);
|
||||
if (!commands.includes(command)) {
|
||||
const m = new Matcher(commands);
|
||||
const similarCommands = m.list(command);
|
||||
|
||||
console.error(`cli '${cmd}' is not a talk cli command. See 'cli --help'.`);
|
||||
if (similarCMDs.length > 0) {
|
||||
const sc = similarCMDs.map(({value}) => `\t${value}\n`).join('');
|
||||
console.error(
|
||||
`cli '${command}' is not a talk cli command. See 'cli --help'.`
|
||||
);
|
||||
if (similarCommands.length > 0) {
|
||||
const sc = similarCommands.map(({ value }) => `\t${value}\n`).join('');
|
||||
console.error(`\nThe most similar commands are\n${sc}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* When this provess exists, check to see if we have a running command, if we do
|
||||
* check to see if it is still running. If it is, then kill it with a SIGINT
|
||||
* signal. This is for the use case where we want to kill the process that is
|
||||
* labled with the PID written out by the parent process.
|
||||
*/
|
||||
process.once('exit', () => {
|
||||
if (
|
||||
|
||||
// program.runningCommand &&
|
||||
program.runningCommand.killed === false &&
|
||||
program.runningCommand.exitCode === null
|
||||
) {
|
||||
program.runningCommand.kill('SIGINT');
|
||||
}
|
||||
});
|
||||
|
||||
+95
-26
@@ -4,7 +4,8 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const parseDuration = require('ms');
|
||||
const Table = require('cli-table');
|
||||
const AssetModel = require('../models/asset');
|
||||
@@ -12,34 +13,28 @@ const CommentModel = require('../models/comment');
|
||||
const AssetsService = require('../services/assets');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const scraper = require('../services/scraper');
|
||||
const util = require('./util');
|
||||
const inquirer = require('inquirer');
|
||||
const { URL } = require('url');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
/**
|
||||
* Lists all the assets registered in the database.
|
||||
*/
|
||||
async function listAssets() {
|
||||
try {
|
||||
let assets = await AssetModel.find({}).sort({'created_at': 1});
|
||||
let assets = await AssetModel.find({}).sort({ created_at: 1 });
|
||||
|
||||
let table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Title',
|
||||
'URL'
|
||||
]
|
||||
head: ['ID', 'Title', 'URL'],
|
||||
});
|
||||
|
||||
assets.forEach((asset) => {
|
||||
assets.forEach(asset => {
|
||||
table.push([
|
||||
asset.id,
|
||||
asset.title ? asset.title : '',
|
||||
asset.url ? asset.url : ''
|
||||
asset.url ? asset.url : '',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -61,13 +56,13 @@ async function refreshAssets(ageString) {
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
$lte: age
|
||||
}
|
||||
$lte: age,
|
||||
},
|
||||
},
|
||||
{
|
||||
scraped: null
|
||||
}
|
||||
]
|
||||
scraped: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Queue all the assets for scraping.
|
||||
@@ -95,7 +90,6 @@ async function updateURL(assetID, assetURL) {
|
||||
|
||||
async function merge(srcID, dstID) {
|
||||
try {
|
||||
|
||||
// Grab the assets...
|
||||
let [srcAsset, dstAsset] = await AssetsService.findByIDs([srcID, dstID]);
|
||||
if (!srcAsset || !dstAsset) {
|
||||
@@ -103,21 +97,22 @@ async function merge(srcID, dstID) {
|
||||
}
|
||||
|
||||
// Count the affected resources...
|
||||
let srcCommentCount = await CommentModel.find({asset_id: srcID}).count();
|
||||
let srcCommentCount = await CommentModel.find({ asset_id: srcID }).count();
|
||||
|
||||
console.log(`Now going to update ${srcCommentCount} comments and delete the source Asset[${srcID}].`);
|
||||
console.log(
|
||||
`Now going to update ${srcCommentCount} comments and delete the source Asset[${srcID}].`
|
||||
);
|
||||
|
||||
let {confirm} = await inquirer.prompt([
|
||||
let { confirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Proceed with merge',
|
||||
default: false
|
||||
}
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (confirm) {
|
||||
|
||||
// Perform the merge!
|
||||
await AssetsService.merge(srcID, dstID);
|
||||
} else {
|
||||
@@ -131,6 +126,70 @@ async function merge(srcID, dstID) {
|
||||
}
|
||||
}
|
||||
|
||||
async function rewrite(search, replace, options) {
|
||||
try {
|
||||
search = new RegExp(search);
|
||||
|
||||
const assets = await AssetModel.find({
|
||||
url: { $regex: search },
|
||||
});
|
||||
if (assets.length === 0) {
|
||||
console.log(`No assets found with the pattern: ${search}`);
|
||||
return util.shutdown(0);
|
||||
}
|
||||
|
||||
let opts = [];
|
||||
assets.forEach(({ id, url: oldURL }) => {
|
||||
// Replace the url.
|
||||
const newURL = oldURL.replace(search, replace);
|
||||
|
||||
// Try to validate that the new url is valid.
|
||||
try {
|
||||
new URL(newURL);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Rewrite would have replaced the valid URL ${oldURL} with an invalid one ${newURL}`
|
||||
);
|
||||
}
|
||||
|
||||
opts.push({
|
||||
find: { id },
|
||||
updateOne: { $set: { url: newURL } },
|
||||
id,
|
||||
oldURL,
|
||||
newURL,
|
||||
});
|
||||
});
|
||||
|
||||
if (opts.length > 0) {
|
||||
if (options.dryRun) {
|
||||
const table = new Table({ head: ['ID', 'Old URL', 'New URL'] });
|
||||
|
||||
opts.forEach(({ id, oldURL, newURL }) => {
|
||||
table.push([id, oldURL, newURL]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
} else {
|
||||
const bulk = AssetModel.collection.initializeUnorderedBulkOp();
|
||||
opts.forEach(({ find, updateOne, oldURL, newURL }) => {
|
||||
// If the url was updated with the operation, then queue up the update op.
|
||||
if (newURL !== oldURL) {
|
||||
bulk.find(find).updateOne(updateOne);
|
||||
}
|
||||
});
|
||||
await bulk.execute();
|
||||
console.log(`${opts.length} assets had their url's updated`);
|
||||
}
|
||||
}
|
||||
|
||||
util.shutdown(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
@@ -152,9 +211,19 @@ program
|
||||
|
||||
program
|
||||
.command('merge <srcID> <dstID>')
|
||||
.description('merges two assets together by moving comments from src to dst and deleting the src asset')
|
||||
.description(
|
||||
'merges two assets together by moving comments from src to dst and deleting the src asset'
|
||||
)
|
||||
.action(merge);
|
||||
|
||||
program
|
||||
.command('rewrite <search> <replace>')
|
||||
.option('-d, --dry-run', 'enables dry run of the replacement')
|
||||
.description(
|
||||
"rewrites asset url's using the provided regex replacement pattern"
|
||||
)
|
||||
.action(rewrite);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
|
||||
+14
-22
@@ -4,27 +4,22 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const scraper = require('../services/scraper');
|
||||
const mailer = require('../services/mailer');
|
||||
const util = require('./util');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const kue = require('../services/kue');
|
||||
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect(),
|
||||
]);
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
/**
|
||||
* Starts the job processor.
|
||||
*/
|
||||
function processJobs() {
|
||||
|
||||
// The scraper only needs to shutdown when the scraper has actually been
|
||||
// started.
|
||||
util.onshutdown([
|
||||
() => kue.Task.shutdown()
|
||||
]);
|
||||
util.onshutdown([() => kue.Task.shutdown()]);
|
||||
|
||||
// Start the scraper processor.
|
||||
scraper.process();
|
||||
@@ -39,13 +34,15 @@ function processJobs() {
|
||||
* @return {Promise}
|
||||
*/
|
||||
function removeJob(job) {
|
||||
return new Promise((resolve, reject) => job.remove((err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return new Promise((resolve, reject) =>
|
||||
job.remove(err => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(job);
|
||||
}));
|
||||
return resolve(job);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,17 +79,13 @@ async function getJobBatch(n, includeStuck) {
|
||||
* Cleans up the jobs that are in the queue.
|
||||
*/
|
||||
async function cleanupJobs(options) {
|
||||
|
||||
// The scraper only needs to shutdown when the scraper has actually been
|
||||
// started.
|
||||
util.onshutdown([
|
||||
() => kue.Task.shutdown()
|
||||
]);
|
||||
util.onshutdown([() => kue.Task.shutdown()]);
|
||||
|
||||
const n = 100;
|
||||
|
||||
try {
|
||||
|
||||
// Connect to redis by establishing a queue.
|
||||
kue.Task.connect();
|
||||
|
||||
@@ -100,9 +93,8 @@ async function cleanupJobs(options) {
|
||||
let jobs = await getJobBatch(n, options.stuck);
|
||||
|
||||
while (jobs.length > 0) {
|
||||
|
||||
// Remove all the jobs.
|
||||
await Promise.all(jobs.map((job) => removeJob(job)));
|
||||
await Promise.all(jobs.map(job => removeJob(job)));
|
||||
|
||||
jobCount += jobs.length;
|
||||
|
||||
|
||||
+60
-32
@@ -4,20 +4,18 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const _ = require('lodash');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const MigrationService = require('../services/migration');
|
||||
|
||||
// Register shutdown hooks.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
async function createMigration(name) {
|
||||
try {
|
||||
|
||||
// Create the migration.
|
||||
await MigrationService.create(name);
|
||||
|
||||
@@ -28,47 +26,60 @@ async function createMigration(name) {
|
||||
}
|
||||
}
|
||||
|
||||
async function runMigrations() {
|
||||
|
||||
async function runMigrations(options) {
|
||||
const { yes, queryBatchSize, updateBatchSize } = options;
|
||||
try {
|
||||
if (!yes) {
|
||||
const { backedUp } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'backedUp',
|
||||
message: 'Did you perform a database backup',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
let {backedUp} = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'backedUp',
|
||||
message: 'Did you perform a database backup',
|
||||
default: false
|
||||
if (!backedUp) {
|
||||
throw new Error(
|
||||
'Please backup your databases prior to migrations occuring'
|
||||
);
|
||||
}
|
||||
]);
|
||||
|
||||
if (!backedUp) {
|
||||
throw new Error('Please backup your databases prior to migrations occuring');
|
||||
}
|
||||
|
||||
// Get the migrations to run.
|
||||
let migrations = await MigrationService.listPending();
|
||||
const migrations = await MigrationService.listPending();
|
||||
|
||||
console.log('Now going to run the following migrations:\n');
|
||||
|
||||
for (let {filename} of migrations) {
|
||||
for (const { filename } of migrations) {
|
||||
console.log(`\tmigrations/${filename}`);
|
||||
}
|
||||
|
||||
let {confirm} = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Proceed with migrations',
|
||||
default: false
|
||||
if (!yes) {
|
||||
const { confirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Proceed with migrations',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (confirm) {
|
||||
// Run the migrations.
|
||||
await MigrationService.run(migrations, {
|
||||
queryBatchSize,
|
||||
updateBatchSize,
|
||||
});
|
||||
} else {
|
||||
console.warn('Skipping migrations');
|
||||
}
|
||||
]);
|
||||
|
||||
if (confirm) {
|
||||
|
||||
// Run the migrations.
|
||||
await MigrationService.run(migrations);
|
||||
} else {
|
||||
console.warn('Skipping migrations');
|
||||
// Run the migrations.
|
||||
await MigrationService.run(migrations, {
|
||||
queryBatchSize,
|
||||
updateBatchSize,
|
||||
});
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
@@ -87,8 +98,25 @@ program
|
||||
.description('creates a new migration')
|
||||
.action(createMigration);
|
||||
|
||||
// Bypasses issue that defaults + coercion doesn't work well together.
|
||||
// Ref: https://github.com/tj/commander.js/issues/400#issuecomment-310860869
|
||||
const parse10 = _.ary(_.partialRight(parseInt, 10), 1);
|
||||
|
||||
program
|
||||
.command('run')
|
||||
.option(
|
||||
'-q, --query-batch-size <n>',
|
||||
'change the size of queried documents that are batched at a time',
|
||||
parse10,
|
||||
10000
|
||||
)
|
||||
.option(
|
||||
'-u, --update-batch-size <n>',
|
||||
'change the size of documents that are batched before the update is sent',
|
||||
parse10,
|
||||
20000
|
||||
)
|
||||
.option('-y, --yes', 'will answer yes to all questions')
|
||||
.description('runs all pending migrations')
|
||||
.action(runMigrations);
|
||||
|
||||
|
||||
+158
-115
@@ -7,24 +7,25 @@
|
||||
// Interface heavily inspired by the yarn package manager:
|
||||
// https://yarnpkg.com/
|
||||
|
||||
const program = require('./commander');
|
||||
require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
|
||||
// Make things colorful!
|
||||
require('colors');
|
||||
const emoji = require('node-emoji');
|
||||
|
||||
const dir = process.cwd();
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const dir = path.resolve(__dirname, '..');
|
||||
const spawn = require('cross-spawn');
|
||||
const semver = require('semver');
|
||||
const resolve = require('resolve');
|
||||
const {plugins, itteratePlugins, isInternal} = require('../plugins');
|
||||
const { plugins, iteratePlugins, isInternal } = require('../plugins');
|
||||
|
||||
function existsInNodeModules(name) {
|
||||
try {
|
||||
resolve.sync(name, {basedir: dir});
|
||||
resolve.sync(name, { basedir: dir });
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -38,13 +39,13 @@ function versionMatch(name, version) {
|
||||
|
||||
resolve.sync(name, {
|
||||
basedir: dir,
|
||||
packageFilter: (pkg) => {
|
||||
packageFilter: pkg => {
|
||||
if (pkg && pkg.version && semver.satisfies(pkg.version, version)) {
|
||||
matched = true;
|
||||
}
|
||||
|
||||
return pkg;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return matched;
|
||||
@@ -55,7 +56,7 @@ function versionMatch(name, version) {
|
||||
|
||||
const EXTERNAL = /^\w[a-z\-0-9.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
|
||||
|
||||
function reconcilePackages({quiet = false, upgradeRemote = false}) {
|
||||
function reconcilePackages({ quiet = false, upgradeRemote = false }) {
|
||||
const fetchable = [];
|
||||
const local = [];
|
||||
const upgradable = [];
|
||||
@@ -70,13 +71,14 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
|
||||
}
|
||||
|
||||
for (let i in plugins) {
|
||||
let section = itteratePlugins(plugins[i]);
|
||||
let section = iteratePlugins(plugins[i]);
|
||||
|
||||
for (let j in section) {
|
||||
let {name, version} = section[j];
|
||||
let { name, version } = section[j];
|
||||
|
||||
let namespaced = name.charAt(0) === '@';
|
||||
let dep = name.split('/')
|
||||
let dep = name
|
||||
.split('/')
|
||||
.slice(0, namespaced ? 2 : 1)
|
||||
.join('/');
|
||||
|
||||
@@ -90,7 +92,7 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
|
||||
console.log(` l ${name}`);
|
||||
}
|
||||
|
||||
local.push({name, version});
|
||||
local.push({ name, version });
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -98,9 +100,8 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
|
||||
if (!quiet) {
|
||||
console.log(` m ${name}`);
|
||||
}
|
||||
fetchable.push({name, version});
|
||||
fetchable.push({ name, version });
|
||||
} else if (!versionMatch(dep, version)) {
|
||||
|
||||
// A plugin was found, yet the current version does not match the
|
||||
// current version installed. We should warn if upgradeRemote is
|
||||
// not enabled that it is currently not supported.
|
||||
@@ -114,14 +115,14 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
|
||||
|
||||
console.log(` oe ${name} (package upgrade may be required)`);
|
||||
|
||||
upgradable.push({name, version});
|
||||
upgradable.push({ name, version });
|
||||
} else {
|
||||
if (!quiet) {
|
||||
console.log(` e ${name}`);
|
||||
}
|
||||
|
||||
if (upgradeRemote) {
|
||||
upgradable.push({name, version});
|
||||
upgradable.push({ name, version });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,33 +132,44 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
|
||||
console.log();
|
||||
}
|
||||
|
||||
return {local, fetchable, upgradable};
|
||||
return { local, fetchable, upgradable };
|
||||
}
|
||||
|
||||
async function reconcileRemotePlugins({skipLocal, dryRun, upgradeRemote}) {
|
||||
console.log(`\n[${skipLocal ? '1/2' : '2/3'}] ${emoji.get('mag')} Reconciling plugins...`.yellow);
|
||||
const {fetchable, upgradable} = reconcilePackages({upgradeRemote});
|
||||
async function reconcileRemotePlugins({ skipLocal, dryRun, upgradeRemote }) {
|
||||
console.log(
|
||||
`\n[${skipLocal ? '1/2' : '2/3'}] ${emoji.get(
|
||||
'mag'
|
||||
)} Reconciling plugins...`.yellow
|
||||
);
|
||||
const { fetchable, upgradable } = reconcilePackages({ upgradeRemote });
|
||||
|
||||
console.log(`[${skipLocal ? '2/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`.yellow);
|
||||
console.log(
|
||||
`[${skipLocal ? '2/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`
|
||||
.yellow
|
||||
);
|
||||
|
||||
if (fetchable.length > 0) {
|
||||
|
||||
console.log(`$ yarn add --ignore-scripts ${fetchable.map(({name, version}) => `${name}@${version}`.cyan)}`);
|
||||
console.log(
|
||||
`$ yarn add --ignore-scripts ${fetchable.map(
|
||||
({ name, version }) => `${name}@${version}`.cyan
|
||||
)}`
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
|
||||
let args = [
|
||||
'add',
|
||||
'--ignore-scripts',
|
||||
...fetchable.map(({name, version}) => `${name}@${version}`)
|
||||
...fetchable.map(({ name, version }) => `${name}@${version}`),
|
||||
];
|
||||
|
||||
let output = spawn.sync('yarn', args, {
|
||||
stdio: ['ignore', 'pipe', 'inherit']
|
||||
stdio: ['ignore', 'pipe', 'inherit'],
|
||||
});
|
||||
|
||||
if (output.status) {
|
||||
throw new Error('Could not install external plugins, errors occured during install');
|
||||
throw new Error(
|
||||
'Could not install external plugins, errors occured during install'
|
||||
);
|
||||
}
|
||||
|
||||
console.log(output.stdout.toString());
|
||||
@@ -165,36 +177,45 @@ async function reconcileRemotePlugins({skipLocal, dryRun, upgradeRemote}) {
|
||||
}
|
||||
|
||||
if (upgradable.length > 0) {
|
||||
console.log(`$ yarn upgrade ${upgradable.map(({name, version}) => `${name}@${version}`.cyan)}`);
|
||||
console.log(
|
||||
`$ yarn upgrade ${upgradable.map(
|
||||
({ name, version }) => `${name}@${version}`.cyan
|
||||
)}`
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
|
||||
let args = [
|
||||
'upgrade',
|
||||
...upgradable.map(({name, version}) => `${name}@${version}`)
|
||||
...upgradable.map(({ name, version }) => `${name}@${version}`),
|
||||
];
|
||||
|
||||
let output = spawn.sync('yarn', args, {
|
||||
stdio: ['ignore', 'pipe', 'inherit']
|
||||
stdio: ['ignore', 'pipe', 'inherit'],
|
||||
});
|
||||
|
||||
if (output.status) {
|
||||
throw new Error('Could not install external plugins, errors occured during install');
|
||||
throw new Error(
|
||||
'Could not install external plugins, errors occured during install'
|
||||
);
|
||||
}
|
||||
|
||||
console.log(output.stdout.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return {upgradable, fetchable};
|
||||
return { upgradable, fetchable };
|
||||
}
|
||||
|
||||
async function reconcileLocalPlugins({skipRemote, dryRun}) {
|
||||
console.log(`\n[${skipRemote ? '1/1' : '1/3'}] ${emoji.get('pick')} Installing local plugin dependencies...\n`.yellow);
|
||||
const {local} = reconcilePackages({quiet: true});
|
||||
async function reconcileLocalPlugins({ skipRemote, dryRun }) {
|
||||
console.log(
|
||||
`\n[${skipRemote ? '1/1' : '1/3'}] ${emoji.get(
|
||||
'pick'
|
||||
)} Installing local plugin dependencies...\n`.yellow
|
||||
);
|
||||
const { local } = reconcilePackages({ quiet: true });
|
||||
|
||||
for (let i in local) {
|
||||
let {name} = local[i];
|
||||
let { name } = local[i];
|
||||
|
||||
if (!fs.existsSync(path.join(dir, 'plugins', name, 'package.json'))) {
|
||||
continue;
|
||||
@@ -209,11 +230,13 @@ async function reconcileLocalPlugins({skipRemote, dryRun}) {
|
||||
|
||||
let output = spawn.sync('yarn', args, {
|
||||
stdio: ['ignore', 'pipe', 'inherit'],
|
||||
cwd: wd
|
||||
cwd: wd,
|
||||
});
|
||||
|
||||
if (output.status) {
|
||||
throw new Error('Could not install local plugin dependencies, errors occured during install');
|
||||
throw new Error(
|
||||
'Could not install local plugin dependencies, errors occurred during install'
|
||||
);
|
||||
}
|
||||
|
||||
console.log(output.stdout.toString());
|
||||
@@ -224,53 +247,67 @@ async function reconcileLocalPlugins({skipRemote, dryRun}) {
|
||||
// This traverses the local plugins and installs any dependencies listed there,
|
||||
// this only is really needed for plugins that are installed via docker because
|
||||
// core plugins will have their dependencies already included in core.
|
||||
async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote}) {
|
||||
let startTime = new Date();
|
||||
async function reconcilePluginDeps({
|
||||
skipLocal,
|
||||
skipRemote,
|
||||
dryRun,
|
||||
upgradeRemote,
|
||||
}) {
|
||||
try {
|
||||
let startTime = new Date();
|
||||
|
||||
// We don't need to do anything if we skip everything....
|
||||
if (skipLocal && skipRemote) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Traverse local plugins and install dependencies if enabled.
|
||||
if (!skipLocal) {
|
||||
await reconcileLocalPlugins({skipRemote, dryRun});
|
||||
}
|
||||
|
||||
// Locate any external plugins and install them.
|
||||
if (!skipRemote) {
|
||||
let results = [];
|
||||
try {
|
||||
results = await reconcileRemotePlugins({skipLocal, skipRemote, dryRun, upgradeRemote});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
// We don't need to do anything if we skip everything....
|
||||
if (skipLocal && skipRemote) {
|
||||
return;
|
||||
}
|
||||
|
||||
let status;
|
||||
if (dryRun) {
|
||||
status = '[dry-run] success'.green;
|
||||
} else {
|
||||
status = 'success'.green;
|
||||
// Traverse local plugins and install dependencies if enabled.
|
||||
if (!skipLocal) {
|
||||
await reconcileLocalPlugins({ skipRemote, dryRun });
|
||||
}
|
||||
|
||||
let message;
|
||||
if (results.upgradable.length === 0 && results.fetchable.length === 0) {
|
||||
message = 'Already up-to-date.';
|
||||
} else if (results.upgradable.length === 0) {
|
||||
message = `Fetched ${results.fetchable.length} new plugins.`;
|
||||
} else if (results.fetchable.length === 0) {
|
||||
message = `Upgraded ${results.upgradable.length} new plugins.`;
|
||||
} else {
|
||||
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${results.upgradable.length} plugins.`;
|
||||
// Locate any external plugins and install them.
|
||||
if (!skipRemote) {
|
||||
const results = await reconcileRemotePlugins({
|
||||
skipLocal,
|
||||
skipRemote,
|
||||
dryRun,
|
||||
upgradeRemote,
|
||||
});
|
||||
|
||||
let status;
|
||||
if (dryRun) {
|
||||
status = '[dry-run] success'.green;
|
||||
} else {
|
||||
status = 'success'.green;
|
||||
}
|
||||
|
||||
let message;
|
||||
if (results.upgradable.length === 0 && results.fetchable.length === 0) {
|
||||
message = 'Already up-to-date.';
|
||||
} else if (results.upgradable.length === 0) {
|
||||
message = `Fetched ${results.fetchable.length} new plugins.`;
|
||||
} else if (results.fetchable.length === 0) {
|
||||
message = `Upgraded ${results.upgradable.length} new plugins.`;
|
||||
} else {
|
||||
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
|
||||
results.upgradable.length
|
||||
} plugins.`;
|
||||
}
|
||||
|
||||
console.log(`\n${status} ${message}`);
|
||||
}
|
||||
|
||||
console.log(`\n${status} ${message}`);
|
||||
let endTime = new Date();
|
||||
|
||||
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(
|
||||
2
|
||||
);
|
||||
console.log(`✨ Done in ${totalTime}s.`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let endTime = new Date();
|
||||
|
||||
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(2);
|
||||
console.log(`✨ Done in ${totalTime}s.`);
|
||||
}
|
||||
|
||||
async function createSeedPlugin() {
|
||||
@@ -278,8 +315,7 @@ async function createSeedPlugin() {
|
||||
|
||||
function pluginNameExists(pluginName) {
|
||||
const pluginNames = fs.readdirSync(pluginsDir);
|
||||
return !!pluginNames
|
||||
.filter((pn) => pn === pluginName).length;
|
||||
return !!pluginNames.filter(pn => pn === pluginName).length;
|
||||
}
|
||||
|
||||
let answers = await inquirer.prompt([
|
||||
@@ -287,8 +323,7 @@ async function createSeedPlugin() {
|
||||
type: 'input',
|
||||
name: 'pluginName',
|
||||
message: 'Plugin Name:',
|
||||
validate: (input) => {
|
||||
|
||||
validate: input => {
|
||||
if (pluginNameExists(input)) {
|
||||
return 'Please, choose another name. That name already exists';
|
||||
}
|
||||
@@ -298,23 +333,23 @@ async function createSeedPlugin() {
|
||||
}
|
||||
|
||||
return 'Plugin Name is required.';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'server',
|
||||
message: 'Is this plugin extending the server capabilities?'
|
||||
message: 'Is this plugin extending the server capabilities?',
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'client',
|
||||
message: 'Is this plugin extending the client capabilities?'
|
||||
message: 'Is this plugin extending the client capabilities?',
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'addPluginsJson',
|
||||
message: 'Should we add it to the plugins.json?'
|
||||
}
|
||||
message: 'Should we add it to the plugins.json?',
|
||||
},
|
||||
]);
|
||||
|
||||
//==============================================================================
|
||||
@@ -325,41 +360,41 @@ async function createSeedPlugin() {
|
||||
const newPluginPath = path.join(pluginsDir, answers.pluginName);
|
||||
|
||||
if (fs.existsSync(seedPlugin)) {
|
||||
|
||||
if (answers.server && answers.client) {
|
||||
|
||||
// This is a server-side and client-side plugin!, let's copy the template
|
||||
fs.copySync(seedPlugin, newPluginPath);
|
||||
} else {
|
||||
} else {
|
||||
fs.copySync(seedPlugin, newPluginPath, {
|
||||
filter: p => {
|
||||
// Allowing plugin folder and files with no subfolders
|
||||
const rootRx = /plugin$|plugin\/[^/]*(\.).{2,3}/gim;
|
||||
if (
|
||||
rootRx.test(p) &&
|
||||
(fs.lstatSync(p).isDirectory() || fs.lstatSync(p).isFile())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
fs.copySync(seedPlugin, newPluginPath, {filter: (p) => {
|
||||
// If it's a client-side plugin, copying client folder
|
||||
if (answers.client) {
|
||||
return /client/.test(p);
|
||||
}
|
||||
|
||||
// Allowing plugin folder and files with no subfolders
|
||||
const rootRx = /plugin$|plugin\/[^/]*(\.).{2,3}/igm;
|
||||
if (rootRx.test(p) && (fs.lstatSync(p).isDirectory() || fs.lstatSync(p).isFile())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If it's a client-side plugin, copying client folder
|
||||
if (answers.client) {
|
||||
return /client/.test(p);
|
||||
}
|
||||
|
||||
// If it's a server-side plugin, copying server folder
|
||||
if (answers.server) {
|
||||
return /server/.test(p);
|
||||
}
|
||||
|
||||
}});
|
||||
// If it's a server-side plugin, copying server folder
|
||||
if (answers.server) {
|
||||
return /server/.test(p);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Let's add this to the plugins.json
|
||||
if (answers.addPluginsJson) {
|
||||
const pluginsJson = path.resolve(__dirname, '..', 'plugins.json');
|
||||
|
||||
fs.readJson(pluginsJson)
|
||||
.then((j) => {
|
||||
|
||||
fs
|
||||
.readJson(pluginsJson)
|
||||
.then(j => {
|
||||
// This is a client-side plugin, let's push this.
|
||||
if (answers.client) {
|
||||
j.client.push(answers.pluginName);
|
||||
@@ -376,14 +411,17 @@ async function createSeedPlugin() {
|
||||
fs.writeFileSync(pluginsJson, output);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✨ Yay! Plugin created! Find your plugin: ${answers.pluginName} in the ./plugins folder`);
|
||||
console.log(
|
||||
`✨ Yay! Plugin created! Find your plugin: ${
|
||||
answers.pluginName
|
||||
} in the ./plugins folder`
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
@@ -402,9 +440,14 @@ program
|
||||
|
||||
program
|
||||
.command('reconcile')
|
||||
.description('reconciles local plugin dependencies and downloads external plugins')
|
||||
.description(
|
||||
'reconciles local plugin dependencies and downloads external plugins'
|
||||
)
|
||||
.option('-u, --upgrade-remote', 'upgrades remote dependencies')
|
||||
.option('-d, --dry-run', 'does not actually change anything on the filesystem acts only as a simulation')
|
||||
.option(
|
||||
'-d, --dry-run',
|
||||
'does not actually change anything on the filesystem acts only as a simulation'
|
||||
)
|
||||
.option('--skip-local', 'skips the local dependancy reconciliation')
|
||||
.option('--skip-remote', 'skips the remote plugin reconciliation')
|
||||
.action(reconcilePluginDeps);
|
||||
|
||||
+6
-4
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const serve = require('../serve');
|
||||
|
||||
//==============================================================================
|
||||
@@ -10,12 +10,14 @@ const serve = require('../serve');
|
||||
|
||||
program
|
||||
.option('-j, --jobs', 'enable job processing on this thread')
|
||||
.option('-w, --websockets', 'enable the websocket (subscriptions) handler on this thread')
|
||||
.option(
|
||||
'-w, --websockets',
|
||||
'enable the websocket (subscriptions) handler on this thread'
|
||||
)
|
||||
.parse(process.argv);
|
||||
|
||||
// Start serving.
|
||||
serve({jobs: program.jobs, websockets: program.websockets}).catch((err) => {
|
||||
serve({ jobs: program.jobs, websockets: program.websockets }).catch(err => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
|
||||
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const SettingsService = require('../services/settings');
|
||||
const util = require('./util');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
@@ -16,12 +16,12 @@ async function changeOrgName() {
|
||||
try {
|
||||
let settings = await SettingsService.retrieve();
|
||||
|
||||
let {organizationName} = await inquirer.prompt([
|
||||
let { organizationName } = await inquirer.prompt([
|
||||
{
|
||||
name: 'organizationName',
|
||||
message: 'Organization Name',
|
||||
default: settings.organizationName
|
||||
}
|
||||
default: settings.organizationName,
|
||||
},
|
||||
]);
|
||||
|
||||
if (settings.organizationName !== organizationName) {
|
||||
|
||||
+49
-50
@@ -4,7 +4,8 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const SettingModel = require('../models/setting');
|
||||
@@ -12,13 +13,11 @@ const MODERATION_OPTIONS = require('../models/enum/moderation_options');
|
||||
const SettingsService = require('../services/settings');
|
||||
const SetupService = require('../services/setup');
|
||||
const UsersService = require('../services/users');
|
||||
const util = require('./util');
|
||||
const MigrationService = require('../services/migration');
|
||||
const errors = require('../errors');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
@@ -34,19 +33,15 @@ program
|
||||
//==============================================================================
|
||||
|
||||
const performSetup = async () => {
|
||||
|
||||
// Get the current settings, we are expecing an error here.
|
||||
try {
|
||||
|
||||
// Try to get the settings.
|
||||
await SettingsService.retrieve();
|
||||
|
||||
// We should NOT have gotten a settings object, this means that the
|
||||
// application is already setup. Error out here.
|
||||
throw errors.ErrSettingsInit;
|
||||
|
||||
} catch (e) {
|
||||
|
||||
// If the error is `not init`, then we're good, otherwise, it's something
|
||||
// else.
|
||||
if (e !== errors.ErrSettingsNotInit) {
|
||||
@@ -57,6 +52,12 @@ const performSetup = async () => {
|
||||
if (program.defaults) {
|
||||
await SettingsService.init();
|
||||
|
||||
// Get the migrations to run.
|
||||
let migrations = await MigrationService.listPending();
|
||||
|
||||
// Perform all migrations.
|
||||
await MigrationService.run(migrations);
|
||||
|
||||
console.log('Settings created.');
|
||||
console.log('\nTalk is now installed!');
|
||||
|
||||
@@ -66,7 +67,9 @@ const performSetup = async () => {
|
||||
// Create the base settings model.
|
||||
let settings = new SettingModel();
|
||||
|
||||
console.log('\nWe\'ll ask you some questions in order to setup your installation of Talk.\n');
|
||||
console.log(
|
||||
"\nWe'll ask you some questions in order to setup your installation of Talk.\n"
|
||||
);
|
||||
|
||||
let answers = await inquirer.prompt([
|
||||
{
|
||||
@@ -74,31 +77,31 @@ const performSetup = async () => {
|
||||
name: 'organizationName',
|
||||
message: 'Organization Name',
|
||||
default: settings.organizationName,
|
||||
validate: (input) => {
|
||||
validate: input => {
|
||||
if (input && input.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'Organization Name is required.';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
choices: MODERATION_OPTIONS,
|
||||
name: 'moderation',
|
||||
default: settings.moderation,
|
||||
message: 'Select a moderation mode'
|
||||
message: 'Select a moderation mode',
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'requireEmailConfirmation',
|
||||
default: settings.requireEmailConfirmation,
|
||||
message: 'Should emails always be confirmed'
|
||||
message: 'Should emails always be confirmed',
|
||||
},
|
||||
]);
|
||||
|
||||
// Update the settings that were changed.
|
||||
Object.keys(answers).forEach((key) => {
|
||||
Object.keys(answers).forEach(key => {
|
||||
if (answers[key] !== undefined) {
|
||||
settings[key] = answers[key];
|
||||
}
|
||||
@@ -109,105 +112,101 @@ const performSetup = async () => {
|
||||
type: 'confirm',
|
||||
name: 'inputWhitelistedDomains',
|
||||
default: true,
|
||||
message: 'Would you like to specify a whitelisted domain'
|
||||
message: 'Would you like to specify a whitelisted domain',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'whitelistedDomain',
|
||||
message: 'Whitelisted Domain',
|
||||
when: ({inputWhitelistedDomains}) => inputWhitelistedDomains,
|
||||
validate: (input) => {
|
||||
when: ({ inputWhitelistedDomains }) => inputWhitelistedDomains,
|
||||
validate: input => {
|
||||
if (input && input.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'Whitelisted Domain cannot be empty.';
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
if (answers.inputWhitelistedDomains) {
|
||||
settings.domains.whitelist = [answers.whitelistedDomain];
|
||||
}
|
||||
|
||||
console.log('\nWe\'ll ask you some questions about your first admin user.\n');
|
||||
console.log("\nWe'll ask you some questions about your first admin user.\n");
|
||||
|
||||
let user = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'username',
|
||||
message: 'Username',
|
||||
filter: (username) => {
|
||||
return UsersService
|
||||
.isValidUsername(username, false)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
filter: username => {
|
||||
return UsersService.isValidUsername(username, false).catch(err => {
|
||||
throw err.message;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
message: 'Email',
|
||||
format: 'email',
|
||||
validate: (value) => {
|
||||
validate: value => {
|
||||
if (value && value.length >= 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'Email is required';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
filter: (password) => {
|
||||
return UsersService
|
||||
.isValidPassword(password)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
filter: password => {
|
||||
return UsersService.isValidPassword(password).catch(err => {
|
||||
throw err.message;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
filter: (confirmPassword, {password}) => {
|
||||
filter: (confirmPassword, { password }) => {
|
||||
if (password !== confirmPassword) {
|
||||
return Promise.reject(new Error('Passwords do not match'));
|
||||
}
|
||||
|
||||
return UsersService
|
||||
.isValidPassword(confirmPassword)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
return UsersService.isValidPassword(confirmPassword).catch(err => {
|
||||
throw err.message;
|
||||
});
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
let {user: newUser} = await SetupService.setup({
|
||||
let { user: newUser } = await SetupService.setup({
|
||||
settings: settings.toObject(),
|
||||
user: {
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
password: user.password
|
||||
}
|
||||
password: user.password,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Settings created.');
|
||||
console.log(`User ${newUser.id} created.`);
|
||||
console.log('\nTalk is now installed!');
|
||||
console.log('\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.');
|
||||
console.log(
|
||||
'\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.'
|
||||
);
|
||||
};
|
||||
|
||||
// Start tthe setup process.
|
||||
// Start the setup process.
|
||||
performSetup()
|
||||
.then(() => {
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((e) => {
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
});
|
||||
|
||||
+7
-19
@@ -4,35 +4,25 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const TokensService = require('../services/tokens');
|
||||
const util = require('./util');
|
||||
const Table = require('cli-table');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
async function listTokens(userID) {
|
||||
try {
|
||||
let tokens = await TokensService.list(userID);
|
||||
|
||||
let table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Name',
|
||||
'Status'
|
||||
]
|
||||
head: ['ID', 'Name', 'Status'],
|
||||
});
|
||||
|
||||
tokens.forEach((token) => {
|
||||
table.push([
|
||||
token.id,
|
||||
token.name,
|
||||
token.active ? 'Active' : 'Revoked'
|
||||
]);
|
||||
tokens.forEach(token => {
|
||||
table.push([token.id, token.name, token.active ? 'Active' : 'Revoked']);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
@@ -46,7 +36,6 @@ async function listTokens(userID) {
|
||||
|
||||
async function revokeToken(tokenID) {
|
||||
try {
|
||||
|
||||
await TokensService.revoke(null, tokenID);
|
||||
|
||||
console.log(`Revoked Token[${tokenID}]`);
|
||||
@@ -60,8 +49,7 @@ async function revokeToken(tokenID) {
|
||||
|
||||
async function createToken(userID, tokenName) {
|
||||
try {
|
||||
|
||||
let {pat: {id}, jwt} = await TokensService.create(userID, tokenName);
|
||||
let { pat: { id }, jwt } = await TokensService.create(userID, tokenName);
|
||||
|
||||
console.log(`Created Token[${id}] for User[${userID}] = ${jwt}`);
|
||||
|
||||
|
||||
+236
-422
@@ -4,160 +4,135 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const { graphql } = require('graphql');
|
||||
const helpers = require('../services/migration/helpers');
|
||||
const { stripIndent } = require('common-tags');
|
||||
const Table = require('cli-table');
|
||||
|
||||
// Make things colorful!
|
||||
require('colors');
|
||||
|
||||
// Register the autocomplete plugin.
|
||||
inquirer.registerPrompt(
|
||||
'autocomplete',
|
||||
require('inquirer-autocomplete-prompt')
|
||||
);
|
||||
|
||||
const schema = require('../graph/schema');
|
||||
const Context = require('../graph/context');
|
||||
const UsersService = require('../services/users');
|
||||
const UserModel = require('../models/user');
|
||||
const CommentModel = require('../models/comment');
|
||||
const ActionModel = require('../models/action');
|
||||
const USER_ROLES = require('../models/enum/user_roles');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
const Table = require('cli-table');
|
||||
const databaseVerifications = require('./verifications/database');
|
||||
|
||||
const validateRequired = (msg = 'Field is required', len = 1) => (input) => {
|
||||
if (input && input.length >= len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return msg;
|
||||
};
|
||||
|
||||
// Regeister the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
function getUserCreateAnswers(options) {
|
||||
if (options.flag_mode) {
|
||||
|
||||
let user = {
|
||||
email: options.email,
|
||||
password: options.password,
|
||||
confirmPassword: options.password,
|
||||
username: options.name,
|
||||
roles: []
|
||||
};
|
||||
|
||||
if (options.role && USER_ROLES.indexOf(options.role) > -1) {
|
||||
user.roles = [options.role];
|
||||
}
|
||||
|
||||
return Promise.resolve(user);
|
||||
}
|
||||
|
||||
return inquirer.prompt([
|
||||
{
|
||||
name: 'email',
|
||||
message: 'Email',
|
||||
format: 'email',
|
||||
validate: validateRequired('Email is required')
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
filter: (password) => {
|
||||
return UsersService
|
||||
.isValidPassword(password)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
filter: (confirmPassword) => {
|
||||
return UsersService
|
||||
.isValidPassword(confirmPassword)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
message: 'Username',
|
||||
filter: (username) => {
|
||||
return UsersService
|
||||
.isValidUsername(username)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'roles',
|
||||
message: 'User Role',
|
||||
type: 'checkbox',
|
||||
choices: USER_ROLES
|
||||
}
|
||||
]);
|
||||
}
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
/**
|
||||
* Prompts for input and registers a user based on those.
|
||||
* transforms a specific action to a removal action on the target model.
|
||||
*/
|
||||
async function createUser(options) {
|
||||
try {
|
||||
const answers = await getUserCreateAnswers(options);
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
throw new Error('Passwords do not match');
|
||||
}
|
||||
|
||||
const user = await UsersService.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim());
|
||||
console.log(`Created user ${user.id}.`);
|
||||
|
||||
if (answers.roles.length > 0) {
|
||||
return Promise.all(answers.roles.map((role) => {
|
||||
return UsersService
|
||||
.addRoleToUser(user.id, role)
|
||||
.then(() => {
|
||||
console.log(`Added the role ${role} to User ${user.id}.`);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
const actionDecrTransformer = ({ item_id, action_type, group_id }) => ({
|
||||
query: { id: item_id },
|
||||
update: {
|
||||
$inc: {
|
||||
[`action_counts.${action_type.toLowerCase()}`]: -1,
|
||||
[`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`]: -1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes a user.
|
||||
* Deletes a user and cleans up their associated verifications.
|
||||
*/
|
||||
async function deleteUser(userID) {
|
||||
|
||||
try {
|
||||
|
||||
// Find the user we're removing.
|
||||
const user = await UserModel.findOne({id: userID});
|
||||
const user = await UserModel.findOne({ id: userID });
|
||||
if (!user) {
|
||||
throw new Error(`user with id ${userID} not found`);
|
||||
}
|
||||
|
||||
printUserAsTable(user);
|
||||
|
||||
console.warn(stripIndent`
|
||||
|
||||
This will delete the above user.
|
||||
|
||||
This might take a long time if there is a lot of data, please confirm that
|
||||
you want to continue.
|
||||
`);
|
||||
const { confirm } = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Continue',
|
||||
default: false,
|
||||
});
|
||||
if (!confirm) {
|
||||
return util.shutdown();
|
||||
}
|
||||
|
||||
const { transformSingleWithCursor } = helpers({
|
||||
queryBatchSize: 10000,
|
||||
updateBatchSize: 10000,
|
||||
});
|
||||
|
||||
console.warn("Removing user's actions");
|
||||
|
||||
// Remove all actions against comments.
|
||||
await transformSingleWithCursor(
|
||||
ActionModel.collection.find({ user_id: user.id, item_type: 'COMMENTS' }),
|
||||
actionDecrTransformer,
|
||||
CommentModel
|
||||
);
|
||||
|
||||
// Remove all actions against users.
|
||||
await transformSingleWithCursor(
|
||||
ActionModel.collection.find({ user_id: user.id, item_type: 'USERS' }),
|
||||
actionDecrTransformer,
|
||||
UserModel
|
||||
);
|
||||
|
||||
// Remove all the user's actions.
|
||||
await ActionModel
|
||||
.where({user_id: user.id})
|
||||
.setOptions({multi: true})
|
||||
await ActionModel.where({ user_id: user.id })
|
||||
.setOptions({ multi: true })
|
||||
.remove();
|
||||
|
||||
console.warn("Removing user's comments");
|
||||
|
||||
// Removes all the user's reply counts on each of the comments that they
|
||||
// have commented on.
|
||||
await transformSingleWithCursor(
|
||||
CommentModel.collection.aggregate([
|
||||
{ $match: { author_id: user.id } },
|
||||
{
|
||||
$group: {
|
||||
_id: '$parent_id',
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
]),
|
||||
({ _id: parent_id, count }) => ({
|
||||
query: { id: parent_id },
|
||||
update: {
|
||||
$inc: {
|
||||
reply_count: -1 * count,
|
||||
},
|
||||
},
|
||||
}),
|
||||
CommentModel
|
||||
);
|
||||
|
||||
// Remove all the user's comments.
|
||||
await CommentModel
|
||||
.where({author_id: user.id})
|
||||
.setOptions({multi: true})
|
||||
await CommentModel.where({ author_id: user.id })
|
||||
.setOptions({ multi: true })
|
||||
.remove();
|
||||
|
||||
// Update the counts that might have changed.
|
||||
for (const verification of databaseVerifications) {
|
||||
await verification({fix: true, limit: Infinity, batch: 1000});
|
||||
}
|
||||
console.warn('Removing the user');
|
||||
|
||||
// Remove the user.
|
||||
await user.remove();
|
||||
@@ -169,146 +144,94 @@ async function deleteUser(userID) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the password for a user.
|
||||
*/
|
||||
function passwd(userID) {
|
||||
inquirer.prompt([
|
||||
function printUserAsTable(user) {
|
||||
let table = new Table({});
|
||||
|
||||
table.push(
|
||||
{ ID: user.id.gray },
|
||||
{ Username: user.username },
|
||||
{ Emails: user.emails },
|
||||
{ Tags: user.tags ? user.tags.map(({ tag: { name } }) => name) : '' },
|
||||
{ Role: user.role },
|
||||
{ Verified: user.hasVerifiedEmail },
|
||||
{ Username: user.status.username.status },
|
||||
{ Banned: user.banned },
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
validate: validateRequired('Password is required')
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
validate: validateRequired('Confirm Password is required')
|
||||
Suspension: user.suspended
|
||||
? `Until ${user.status.suspension.until}`
|
||||
: false,
|
||||
}
|
||||
])
|
||||
.then((answers) => {
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
throw new Error('Password mismatch');
|
||||
}
|
||||
);
|
||||
|
||||
return UsersService.changePassword(userID, answers.password);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Password changed.');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
console.log(table.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the user from the options array.
|
||||
* Searches for users based on their username and email address.
|
||||
*/
|
||||
function updateUser(userID, options) {
|
||||
const updates = [];
|
||||
|
||||
if (options.email && typeof options.email === 'string' && options.email.length > 0) {
|
||||
let q = UserModel.update({
|
||||
'id': userID,
|
||||
'profiles.provider': 'local'
|
||||
}, {
|
||||
$set: {
|
||||
'profiles.$.id': options.email
|
||||
async function searchUsers() {
|
||||
const ctx = Context.forSystem();
|
||||
const searchQuery = `
|
||||
query SearchUsers($value: String) {
|
||||
users(query: {value: $value}) {
|
||||
nodes {
|
||||
id
|
||||
username
|
||||
role
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
`;
|
||||
|
||||
updates.push(q);
|
||||
}
|
||||
|
||||
if (options.name && typeof options.name === 'string' && options.name.length > 0) {
|
||||
let q = UserModel.update({
|
||||
'id': userID
|
||||
}, {
|
||||
$set: {
|
||||
username: options.name
|
||||
}
|
||||
});
|
||||
|
||||
updates.push(q);
|
||||
}
|
||||
|
||||
Promise
|
||||
.all(updates.map((q) => q.exec()))
|
||||
.then(() => {
|
||||
console.log(`User ${userID} updated.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all the users registered in the database.
|
||||
*/
|
||||
function listUsers() {
|
||||
UsersService
|
||||
.all()
|
||||
.then((users) => {
|
||||
let table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Username',
|
||||
'Profiles',
|
||||
'Roles',
|
||||
'Status',
|
||||
'State'
|
||||
]
|
||||
});
|
||||
|
||||
users.forEach((user) => {
|
||||
let state = user.disabled ? 'Disabled' : 'Enabled';
|
||||
const profile = user.profiles.find(({provider}) => provider === 'local');
|
||||
if (profile && profile.metadata && profile.metadata.confirmed_at) {
|
||||
state += ', Verified';
|
||||
} else {
|
||||
state += ', Unverified';
|
||||
try {
|
||||
const answers = await inquirer.prompt({
|
||||
type: 'autocomplete',
|
||||
name: 'userID',
|
||||
message: 'Search for a user',
|
||||
source: async (answers, value) => {
|
||||
if (value === null) {
|
||||
value = '';
|
||||
}
|
||||
|
||||
table.push([
|
||||
user.id,
|
||||
user.username,
|
||||
user.profiles.map((p) => p.provider).join(', '),
|
||||
user.roles.join(', '),
|
||||
user.status,
|
||||
state
|
||||
]);
|
||||
});
|
||||
const { data, errors } = await graphql(schema, searchQuery, {}, ctx, {
|
||||
value,
|
||||
});
|
||||
if (errors && errors.length > 0) {
|
||||
throw errors[0];
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
if (data.users === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges two users using the specified ID's.
|
||||
* @param {String} dstUserID id of the user to which is the target of the merge
|
||||
* @param {String} srcUserID id of the user to which is the source of the merge
|
||||
*/
|
||||
function mergeUsers(dstUserID, srcUserID) {
|
||||
UsersService
|
||||
.mergeUsers(dstUserID, srcUserID)
|
||||
.then(() => {
|
||||
console.log(`User ${srcUserID} was merged into user ${dstUserID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
return data.users.nodes.map(user => {
|
||||
const emails = user.profiles
|
||||
.filter(({ provider }) => provider === 'local')
|
||||
.map(({ id }) => id)
|
||||
.join(', ');
|
||||
return {
|
||||
name: `${user.username} (${emails}) ${user.id.gray} - ${
|
||||
user.role.gray
|
||||
}`,
|
||||
value: user.id,
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { userID } = answers;
|
||||
const user = await UserModel.findOne({ id: userID });
|
||||
|
||||
printUserAsTable(user);
|
||||
util.shutdown(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,127 +239,70 @@ function mergeUsers(dstUserID, srcUserID) {
|
||||
* @param {String} userUD id of the user to add the role to
|
||||
* @param {String} role the role to add
|
||||
*/
|
||||
function addRole(userID, role) {
|
||||
async function setUserRole(userID) {
|
||||
try {
|
||||
const { role } = await inquirer.prompt([
|
||||
{
|
||||
name: 'role',
|
||||
message: 'User Role',
|
||||
type: 'list',
|
||||
choices: USER_ROLES,
|
||||
},
|
||||
]);
|
||||
|
||||
if (USER_ROLES.indexOf(role) === -1) {
|
||||
console.error(`Role '${role}' is not supported. Supported roles are ${USER_ROLES.join(', ')}.`);
|
||||
await UsersService.setRole(userID, role);
|
||||
|
||||
console.log(`Set User ${userID} to the ${role} role.`);
|
||||
util.shutdown();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
return;
|
||||
}
|
||||
|
||||
UsersService
|
||||
.addRoleToUser(userID, role)
|
||||
.then(() => {
|
||||
console.log(`Added the ${role} role to User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a role from a user
|
||||
* @param {String} userUD id of the user to remove the role from
|
||||
* @param {String} role the role to remove
|
||||
*/
|
||||
function removeRole(userID, role) {
|
||||
|
||||
if (USER_ROLES.indexOf(role) === -1) {
|
||||
console.error(`Role '${role}' is not supported. Supported roles are ${USER_ROLES.join(', ')}.`);
|
||||
util.shutdown(1);
|
||||
return;
|
||||
}
|
||||
|
||||
UsersService
|
||||
.removeRoleFromUser(userID, role)
|
||||
.then(() => {
|
||||
console.log(`Removed the ${role} role from User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ban a user
|
||||
* @param {String} userID id of the user to ban
|
||||
*/
|
||||
function ban(userID) {
|
||||
UsersService
|
||||
.setStatus(userID, 'BANNED')
|
||||
.then(() => {
|
||||
console.log(`Banned the User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unban a user
|
||||
* @param {String} userUD id of the user to remove the role from
|
||||
*/
|
||||
function unban(userID) {
|
||||
UsersService
|
||||
.setStatus(userID, 'ACTIVE')
|
||||
.then(() => {
|
||||
console.log(`Unban the User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a given user.
|
||||
* @param {String} userID the ID of a user to disable
|
||||
*/
|
||||
function disableUser(userID) {
|
||||
UsersService
|
||||
.disableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was disabled.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enabled a given user.
|
||||
* @param {String} userID the ID of a user to enable
|
||||
*/
|
||||
function enableUser(userID) {
|
||||
UsersService
|
||||
.enableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was enabled.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies an email address for a user.
|
||||
*
|
||||
* @param userID the user's id
|
||||
* @param email the user's email address to be verified
|
||||
* @param email the user's email address to be verified, otherwise verifies the
|
||||
* first email if there is one, if there are multiple, you get a
|
||||
* prompt.
|
||||
*/
|
||||
async function verify(userID, email) {
|
||||
async function verifyUserEmail(userID, email) {
|
||||
try {
|
||||
// Get the user.
|
||||
const user = await UserModel.findOne({ id: userID });
|
||||
if (!user) {
|
||||
throw new Error(`user with ID ${userID} cannot be found`);
|
||||
}
|
||||
|
||||
// Get all the user's email addresses.
|
||||
const emails = user.emails;
|
||||
if (emails.length === 0) {
|
||||
throw new Error('user did not have any email addresses');
|
||||
}
|
||||
|
||||
if (!email && emails.length === 1) {
|
||||
// The email wasn't passed, and there is only one option.
|
||||
email = emails[0];
|
||||
} else if (!emails.includes(email)) {
|
||||
// The email passed doesn't belong to this user.
|
||||
throw new Error(`user does not have the email ${email}`);
|
||||
} else if (emails.length > 1) {
|
||||
// The email wasn't passed, and there is more than one choice.
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
name: 'email',
|
||||
message: 'Select Email to Verify',
|
||||
type: 'list',
|
||||
choices: emails,
|
||||
},
|
||||
]);
|
||||
|
||||
email = answers.email;
|
||||
}
|
||||
|
||||
// Verify the email.
|
||||
await UsersService.confirmEmail(userID, email);
|
||||
console.log(`User ${userID} had their email ${email} verified.`);
|
||||
util.shutdown();
|
||||
@@ -450,77 +316,25 @@ async function verify(userID, email) {
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('create')
|
||||
.option('--email [email]', 'Email to use')
|
||||
.option('--password [password]', 'Password to use')
|
||||
.option('--name [name]', 'Name to use')
|
||||
.option('--role [role]', 'Role to add')
|
||||
.option('-f, --flag_mode', 'Source from flags instead of prompting')
|
||||
.description('create a new user')
|
||||
.action(createUser);
|
||||
|
||||
program
|
||||
.command('delete <userID>')
|
||||
.description('delete a user')
|
||||
.action(deleteUser);
|
||||
|
||||
program
|
||||
.command('passwd <userID>')
|
||||
.description('change a password for a user')
|
||||
.action(passwd);
|
||||
|
||||
program
|
||||
.command('update <userID>')
|
||||
.option('--email [email]', 'Email to use')
|
||||
.option('--name [name]', 'Name to use')
|
||||
.description('update a user')
|
||||
.action(updateUser);
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description('list all the users in the database')
|
||||
.action(listUsers);
|
||||
.description('searches for a user based on their stored username and email')
|
||||
.action(searchUsers);
|
||||
|
||||
program
|
||||
.command('merge <dstUserID> <srcUserID>')
|
||||
.description('merge srcUser into the dstUser')
|
||||
.action(mergeUsers);
|
||||
|
||||
program
|
||||
.command('addrole <userID> <role>')
|
||||
.description('adds a role to a given user')
|
||||
.action(addRole);
|
||||
|
||||
program
|
||||
.command('removerole <userID> <role>')
|
||||
.description('removes a role from a given user')
|
||||
.action(removeRole);
|
||||
|
||||
program
|
||||
.command('ban <userID>')
|
||||
.description('ban a given user')
|
||||
.action(ban);
|
||||
|
||||
program
|
||||
.command('uban <userID>')
|
||||
.description('unban a given user')
|
||||
.action(unban);
|
||||
|
||||
program
|
||||
.command('disable <userID>')
|
||||
.description('disable a given user from logging in')
|
||||
.action(disableUser);
|
||||
|
||||
program
|
||||
.command('enable <userID>')
|
||||
.description('enable a given user from logging in')
|
||||
.action(enableUser);
|
||||
.command('set-role <userID> <role>')
|
||||
.description('sets the role on a user')
|
||||
.action(setUserRole);
|
||||
|
||||
program
|
||||
.command('verify <userID> <email>')
|
||||
.description('verifies the given user\'s email address')
|
||||
.action(verify);
|
||||
.description("verifies the given user's email address")
|
||||
.action(verifyUserEmail);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
const databaseVerifications = require('./verifications/database');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
async function database({fix = false, limit = Infinity, batch = 1000}) {
|
||||
try {
|
||||
for (const verification of databaseVerifications) {
|
||||
await verification({fix, limit, batch});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to process all the ${databaseVerifications.length} verifications`, err);
|
||||
util.shutdown(1);
|
||||
return;
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('db')
|
||||
.description('verifies the database integrity')
|
||||
.option('-f, --fix', 'fix the problems found with database inconsistencies')
|
||||
.option('-l, --limit [size]', 'limit the amount of documents to process in a single pass, this will ensure only a maximum number of batch operations are issued [default: inf]', parseInt)
|
||||
.option('-b, --batch [size]', 'batch size to process verifications and repairs of documents [default: 1000]', parseInt)
|
||||
.action(database);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
const pkg = require('../package.json');
|
||||
const dotenv = require('dotenv');
|
||||
const fs = require('fs');
|
||||
const program = require('commander');
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
const parseArgs = require('minimist')(process.argv.slice(2), {
|
||||
alias: {
|
||||
'c': 'config'
|
||||
},
|
||||
string: [
|
||||
'config',
|
||||
'pid'
|
||||
],
|
||||
default: {
|
||||
'config': null,
|
||||
'pid': null
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* If the config flag is present, then we have to load the configuration from
|
||||
* the file specified. We will then load those values into the environment.
|
||||
*/
|
||||
if (parseArgs.config) {
|
||||
let envConfig = dotenv.parse(fs.readFileSync(parseArgs.config, {encoding: 'utf8'}));
|
||||
|
||||
Object.keys(envConfig).forEach((k) => {
|
||||
process.env[k] = envConfig[k];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If the pid flag is present, then we have to create a pid file at the location
|
||||
* specified.
|
||||
*/
|
||||
if (parseArgs.pid) {
|
||||
const util = require('./util');
|
||||
|
||||
console.log('Wrote PID');
|
||||
|
||||
util.pid(parseArgs.pid);
|
||||
}
|
||||
|
||||
module.exports = program
|
||||
.version(pkg.version)
|
||||
.option('-c, --config [path]', 'Specify the configuration file to load')
|
||||
.option('--pid [path]', 'Specify a path to output the current PID to');
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import {CoralLogo} from 'plugin-api/beta/client/components/ui';
|
||||
import { CoralLogo } from 'plugin-api/beta/client/components/ui';
|
||||
import styles from './MyPluginComponent.css';
|
||||
|
||||
class MyPluginComponent extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.myPluginContainer}>
|
||||
<CoralLogo className={styles.logo}/>
|
||||
<CoralLogo className={styles.logo} />
|
||||
<div className={styles.description}>
|
||||
<h3>Plugin created by Talk CLI</h3>
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
This is a client index example file and it could look like this:
|
||||
|
||||
@@ -21,6 +20,6 @@ import MyPluginComponent from './components/MyPluginComponent';
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
stream: [MyPluginComponent]
|
||||
}
|
||||
stream: [MyPluginComponent],
|
||||
},
|
||||
};
|
||||
|
||||
+22
-48
@@ -1,7 +1,9 @@
|
||||
const debug = require('debug')('talk:util');
|
||||
const fs = require('fs');
|
||||
// Setup the environment.
|
||||
require('../services/env');
|
||||
|
||||
const util = module.exports = {};
|
||||
const debug = require('debug')('talk:util');
|
||||
|
||||
const util = (module.exports = {});
|
||||
|
||||
/**
|
||||
* Stores an array of functions that should be executed in the event that the
|
||||
@@ -15,20 +17,22 @@ util.toshutdown = [];
|
||||
* @param {Number} [defaultCode=0] default return code upon sucesfull shutdown.
|
||||
*/
|
||||
util.shutdown = (defaultCode = 0, signal = null) => {
|
||||
|
||||
if (signal) {
|
||||
debug(`Reached ${signal} signal`);
|
||||
}
|
||||
|
||||
debug(`${util.toshutdown.length} jobs now being called`);
|
||||
|
||||
Promise
|
||||
.all(util.toshutdown.map((func) => func ? func(signal) : null).filter((func) => func))
|
||||
Promise.all(
|
||||
util.toshutdown
|
||||
.map(func => (func ? func(signal) : null))
|
||||
.filter(func => func)
|
||||
)
|
||||
.then(() => {
|
||||
debug('Shutdown complete, now exiting');
|
||||
process.exit(defaultCode);
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
|
||||
process.exit(1);
|
||||
@@ -41,54 +45,24 @@ util.shutdown = (defaultCode = 0, signal = null) => {
|
||||
* @param {Array} jobs Array of promise capable shutdown functions that are
|
||||
* executed.
|
||||
*/
|
||||
util.onshutdown = (jobs) => {
|
||||
|
||||
util.onshutdown = jobs => {
|
||||
debug(`${jobs.length} jobs registered to be called during shutdown`);
|
||||
|
||||
// Add the new jobs to shutdown to the object reference.
|
||||
util.toshutdown = util.toshutdown.concat(jobs);
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a PID file to be maintained for the lifespan of the process.
|
||||
* @param {String} path path to the PID file to create
|
||||
*/
|
||||
util.pid = (path) => {
|
||||
if (!/\//.test(path)) {
|
||||
if (!/\.pid/.test(path)) {
|
||||
path += '.pid';
|
||||
}
|
||||
path = `/tmp/${path}`;
|
||||
}
|
||||
|
||||
const pid = `${process.pid.toString()}\n`;
|
||||
|
||||
fs.writeFile(path, pid, (err) => {
|
||||
if (err) {
|
||||
console.error(`Can't write PID file: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Add the cleanup for the fs onto the shutdown.
|
||||
util.onshutdown([
|
||||
() => new Promise((resolve, reject) => {
|
||||
|
||||
// Remove the pid file.
|
||||
fs.unlink(path, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
})
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
// Attach to the SIGTERM + SIGINT handles to ensure a clean shutdown in the
|
||||
// event that we have an external event. SIGUSR2 is called when the app is asked
|
||||
// to be 'killed', same procedure here.
|
||||
process.on('SIGTERM', () => util.shutdown(0, 'SIGTERM'));
|
||||
process.on('SIGINT', () => util.shutdown(0, 'SIGINT'));
|
||||
process.on('SIGTERM', () => util.shutdown(0, 'SIGTERM'));
|
||||
process.on('SIGINT', () => util.shutdown(0, 'SIGINT'));
|
||||
process.once('SIGUSR2', () => util.shutdown(0, 'SIGUSR2'));
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
const CommentModel = require('../../../models/comment');
|
||||
const ActionsService = require('../../../services/actions');
|
||||
const {arrayJoinBy, singleJoinBy} = require('../../../graph/loaders/util');
|
||||
const sc = require('snake-case');
|
||||
const debug = require('debug')('talk:cli:verify');
|
||||
|
||||
const getBatch = async (limit, offset) => CommentModel
|
||||
.find({})
|
||||
.select({'id': 1, 'action_counts': 1, 'reply_count': 1})
|
||||
.limit(limit)
|
||||
.skip(offset)
|
||||
.sort('created_at');
|
||||
|
||||
module.exports = async ({fix, limit, batch}) => {
|
||||
let operations = [];
|
||||
|
||||
// Count how many comments there are to process.
|
||||
const totalCount = await CommentModel.count();
|
||||
|
||||
let offset = 0;
|
||||
let comments = [];
|
||||
let commentIDs = [];
|
||||
|
||||
console.log(`Processing ${totalCount} comments in batches of ${limit}...`);
|
||||
|
||||
// Keep processing documents until there are is none left.
|
||||
while (offset < totalCount) {
|
||||
|
||||
// Get a batch of comments.
|
||||
comments = await getBatch(batch, offset);
|
||||
commentIDs = comments.map(({id}) => id);
|
||||
|
||||
// Get their reply counts.
|
||||
let allReplyCounts = await CommentModel
|
||||
.aggregate([
|
||||
{
|
||||
$match: {
|
||||
parent_id: {
|
||||
$in: commentIDs,
|
||||
},
|
||||
status: {
|
||||
$in: ['NONE', 'ACCEPTED']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$parent_id',
|
||||
count: {
|
||||
$sum: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
.then(singleJoinBy(commentIDs, '_id'))
|
||||
.then((results) => results.map((result) => result ? result.count : 0));
|
||||
|
||||
// Get their action summaries.
|
||||
let allActionSummaries = await ActionsService
|
||||
.getActionSummaries(commentIDs)
|
||||
.then(arrayJoinBy(commentIDs, 'item_id'));
|
||||
|
||||
// Loop over the comments, with their action summaries.
|
||||
for (let i = 0; i < comments.length; i++) {
|
||||
let comment = comments[i];
|
||||
let actionSummaries = allActionSummaries[i];
|
||||
let replyCount = allReplyCounts[i];
|
||||
|
||||
// And check to see if the action summaries we just computed match what is
|
||||
// currently set for the comments.
|
||||
let commentOperations = [];
|
||||
|
||||
// If the reply count needs to be updated, then update it!
|
||||
if (comment.reply_count !== replyCount) {
|
||||
commentOperations.push({
|
||||
reply_count: replyCount,
|
||||
});
|
||||
}
|
||||
|
||||
// First we process all the group id's.
|
||||
for (let actionSummary of actionSummaries) {
|
||||
if (actionSummary.group_id === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// And we generate the group id.
|
||||
const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase());
|
||||
const GROUP_ID = sc(actionSummary.group_id.toLowerCase());
|
||||
|
||||
if (GROUP_ID.length <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// And we add a new batch operation if the action summary is associated
|
||||
// with a group.
|
||||
const ACTION_COUNT_FIELD = `${ACTION_TYPE}_${GROUP_ID}`;
|
||||
|
||||
// Check that the action summaries match the cached counts.
|
||||
if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== actionSummary.count) {
|
||||
|
||||
// Batch updates for those changes.
|
||||
commentOperations.push({
|
||||
[`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Group all the action summaries together from all the different group
|
||||
// ids.
|
||||
let groupedActionSummaries = actionSummaries.reduce((acc, actionSummary) => {
|
||||
const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase());
|
||||
|
||||
if (!(ACTION_TYPE in acc)) {
|
||||
acc[ACTION_TYPE] = 0;
|
||||
}
|
||||
|
||||
acc[ACTION_TYPE] += actionSummary.count;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
for (const ACTION_COUNT_FIELD of Object.keys(groupedActionSummaries)) {
|
||||
const count = groupedActionSummaries[ACTION_COUNT_FIELD];
|
||||
|
||||
// Check that the action summaries match the cached counts.
|
||||
if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== count) {
|
||||
|
||||
// Batch updates for those changes.
|
||||
commentOperations.push({
|
||||
[`action_counts.${ACTION_COUNT_FIELD}`]: count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If this comment has action summaries that should be updated, then
|
||||
// perform an update!
|
||||
if (commentOperations.length > 0) {
|
||||
operations.push({
|
||||
updateOne: {
|
||||
filter: {
|
||||
id: comment.id
|
||||
},
|
||||
update: {
|
||||
$set: Object.assign({}, ...commentOperations),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
debug(`Processed batch of ${comments.length} comments.`);
|
||||
|
||||
if (operations.length >= limit) {
|
||||
debug(`Queued operations are ${operations.length}, reached limit of ${limit}, not processing any more.`);
|
||||
|
||||
if (operations.length > limit) {
|
||||
debug(`${operations.length - limit} operations have been truncated to enforce the limit`);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
offset += batch;
|
||||
}
|
||||
|
||||
const OPERATIONS_LENGTH = operations.length;
|
||||
|
||||
if (limit < Infinity && offset + comments.length < totalCount) {
|
||||
console.log(`Processed ${offset + comments.length}/${totalCount} comments because we reached the update limit of ${limit}.`);
|
||||
} else {
|
||||
console.log(`Processed all ${totalCount} comments.`);
|
||||
}
|
||||
|
||||
console.log(`${OPERATIONS_LENGTH} documents need fixing.`);
|
||||
|
||||
// If fix was enabled, execute the batch writes.
|
||||
if (OPERATIONS_LENGTH > 0) {
|
||||
if (fix) {
|
||||
debug(`Fixing ${OPERATIONS_LENGTH} documents...`);
|
||||
|
||||
while (operations.length) {
|
||||
let batchOperations = operations.splice(0, batch);
|
||||
let result = await CommentModel.collection.bulkWrite(batchOperations);
|
||||
|
||||
debug(`Fixed batch of ${result.modifiedCount} documents.`);
|
||||
}
|
||||
|
||||
console.log(`Applied all ${OPERATIONS_LENGTH} fixes.`);
|
||||
} else {
|
||||
console.warn('Skipping fixing, --fix was not enabled, pass --fix to fix these errors');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
// This will import all the verifications that should be run by the:
|
||||
//
|
||||
// cli verify database
|
||||
//
|
||||
// command. They exist in the form:
|
||||
//
|
||||
// async ({fix = false, batch = 1000}) => {}
|
||||
//
|
||||
// where their options are derrived.
|
||||
module.exports = [
|
||||
require('./comments'),
|
||||
];
|
||||
+16
-6
@@ -7,6 +7,8 @@ machine:
|
||||
environment:
|
||||
PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
|
||||
NODE_ENV: "test"
|
||||
MOCHA_FILE: "${CIRCLE_TEST_REPORTS}/junit/test-results.xml"
|
||||
MOCHA_REPORTER: "mocha-junit-reporter"
|
||||
pre:
|
||||
# TODO: use the following to add in support for MongoDB 3.4.
|
||||
# # Upgrade the database version to 3.4.
|
||||
@@ -17,6 +19,13 @@ machine:
|
||||
# - sudo apt-get install -y mongodb-org
|
||||
# - sudo service mongod restart
|
||||
|
||||
# Force sync the time.
|
||||
- date
|
||||
- sudo service ntp stop
|
||||
- sudo ntpdate -s time.nist.gov
|
||||
- sudo service ntp start
|
||||
- date
|
||||
|
||||
# Install chromium for e2e and remove old google-chrome
|
||||
- sudo rm -rf /opt/google/chrome
|
||||
- sudo rm -f /usr/bin/google-chrome*
|
||||
@@ -28,7 +37,7 @@ dependencies:
|
||||
|
||||
# Install node dependencies.
|
||||
- yarn --version
|
||||
- yarn global add node-gyp nsp --force
|
||||
- yarn global add node-gyp --force
|
||||
- yarn
|
||||
|
||||
post:
|
||||
@@ -47,10 +56,9 @@ database:
|
||||
test:
|
||||
override:
|
||||
# Run the tests using the junit reporter.
|
||||
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test
|
||||
# Check dependancies using nsp.
|
||||
- nsp check
|
||||
- yarn e2e-ci
|
||||
- yarn test
|
||||
# Run the end to end tests
|
||||
- yarn e2e:ci
|
||||
|
||||
deployment:
|
||||
release:
|
||||
@@ -59,7 +67,9 @@ deployment:
|
||||
- bash ./scripts/docker.sh deploy
|
||||
|
||||
latest:
|
||||
branch: master
|
||||
branch:
|
||||
- master
|
||||
- next
|
||||
owner: coralproject
|
||||
commands:
|
||||
- bash ./scripts/docker.sh deploy
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
|
||||
import { Router, Route, IndexRedirect, IndexRoute } from 'react-router';
|
||||
|
||||
import Configure from 'routes/Configure';
|
||||
import Install from 'routes/Install';
|
||||
import Stories from 'routes/Stories';
|
||||
import Community from 'routes/Community/containers/Community';
|
||||
import {ModerationLayout, Moderation} from 'routes/Moderation';
|
||||
import Community from 'routes/Community';
|
||||
import { ModerationLayout, Moderation } from 'routes/Moderation';
|
||||
|
||||
import Layout from 'containers/Layout';
|
||||
|
||||
const routes = (
|
||||
<div>
|
||||
<Route exact path="/admin/install" component={Install}/>
|
||||
<Route path='/admin' component={Layout}>
|
||||
<IndexRedirect to='/admin/moderate' />
|
||||
<Route path='configure' component={Configure} />
|
||||
<Route path='stories' component={Stories} />
|
||||
<Route exact path="/admin/install" component={Install} />
|
||||
<Route path="/admin" component={Layout}>
|
||||
<IndexRedirect to="/admin/moderate" />
|
||||
<Route path="configure" component={Configure} />
|
||||
<Route path="stories" component={Stories} />
|
||||
|
||||
{/* Community Routes */}
|
||||
|
||||
<Route path='community'>
|
||||
<Route path='flagged' components={Community}>
|
||||
<Route path=':id' components={Community} />
|
||||
<Route path="community">
|
||||
<Route path="flagged" components={Community}>
|
||||
<Route path=":id" components={Community} />
|
||||
</Route>
|
||||
<Route path='people' components={Community}>
|
||||
<Route path=':id' components={Community} />
|
||||
<Route path="people" components={Community}>
|
||||
<Route path=":id" components={Community} />
|
||||
</Route>
|
||||
<IndexRedirect to='flagged' />
|
||||
<IndexRedirect to="flagged" />
|
||||
</Route>
|
||||
|
||||
{/* Moderation Routes */}
|
||||
|
||||
<Route path='moderate' component={ModerationLayout}>
|
||||
<Route path="moderate" component={ModerationLayout}>
|
||||
<IndexRoute components={Moderation} />
|
||||
|
||||
<Route path=':tabOrId' components={Moderation} />
|
||||
<Route path=":tabOrId" components={Moderation} />
|
||||
|
||||
<Route path=':tab' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
<Route path=":tab" components={Moderation}>
|
||||
<Route path=":id" components={Moderation} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@@ -7,29 +7,33 @@ import jwtDecode from 'jwt-decode';
|
||||
// SIGN IN
|
||||
//==============================================================================
|
||||
|
||||
export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client, storage}) => {
|
||||
dispatch({type: actions.LOGIN_REQUEST});
|
||||
export const handleLogin = (email, password, recaptchaResponse) => (
|
||||
dispatch,
|
||||
_,
|
||||
{ rest, client, localStorage }
|
||||
) => {
|
||||
dispatch({ type: actions.LOGIN_REQUEST });
|
||||
|
||||
const params = {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email,
|
||||
password
|
||||
}
|
||||
password,
|
||||
},
|
||||
};
|
||||
|
||||
if (recaptchaResponse) {
|
||||
params.headers = {
|
||||
'X-Recaptcha-Response': recaptchaResponse
|
||||
'X-Recaptcha-Response': recaptchaResponse,
|
||||
};
|
||||
}
|
||||
|
||||
return rest('/auth/local', params)
|
||||
.then(({user, token}) => {
|
||||
|
||||
.then(({ user, token }) => {
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios && storage) {
|
||||
storage.removeItem('token');
|
||||
if (!bowser.safari && !bowser.ios && localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
@@ -38,19 +42,19 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _,
|
||||
client.resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
|
||||
if (error.translation_key === 'NOT_AUTHORIZED') {
|
||||
|
||||
// invalid credentials
|
||||
dispatch({
|
||||
type: actions.LOGIN_FAILURE,
|
||||
message: t('error.email_password')
|
||||
message: t('error.email_password'),
|
||||
});
|
||||
}
|
||||
else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
|
||||
} else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
|
||||
dispatch({
|
||||
type: actions.LOGIN_MAXIMUM_EXCEEDED,
|
||||
message: t(`error.${error.translation_key}`),
|
||||
@@ -69,27 +73,32 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _,
|
||||
//==============================================================================
|
||||
|
||||
const forgotPasswordRequest = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
|
||||
type: actions.FETCH_FORGOT_PASSWORD_REQUEST,
|
||||
});
|
||||
|
||||
const forgotPasswordSuccess = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
|
||||
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS,
|
||||
});
|
||||
|
||||
const forgotPasswordFailure = (error) => ({
|
||||
const forgotPasswordFailure = error => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const requestPasswordReset = (email) => (dispatch, _, {rest}) => {
|
||||
export const requestPasswordReset = email => (dispatch, _, { rest }) => {
|
||||
dispatch(forgotPasswordRequest(email));
|
||||
const redirectUri = location.href;
|
||||
|
||||
return rest('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
|
||||
return rest('/account/password/reset', {
|
||||
method: 'POST',
|
||||
body: { email, loc: redirectUri },
|
||||
})
|
||||
.then(() => dispatch(forgotPasswordSuccess()))
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(forgotPasswordFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
@@ -99,27 +108,32 @@ export const requestPasswordReset = (email) => (dispatch, _, {rest}) => {
|
||||
//==============================================================================
|
||||
|
||||
const checkLoginRequest = () => ({
|
||||
type: actions.CHECK_LOGIN_REQUEST
|
||||
type: actions.CHECK_LOGIN_REQUEST,
|
||||
});
|
||||
|
||||
const checkLoginSuccess = (user, isAdmin) => ({
|
||||
type: actions.CHECK_LOGIN_SUCCESS,
|
||||
user,
|
||||
isAdmin
|
||||
isAdmin,
|
||||
});
|
||||
|
||||
const checkLoginFailure = (error) => ({
|
||||
const checkLoginFailure = error => ({
|
||||
type: actions.CHECK_LOGIN_FAILURE,
|
||||
error
|
||||
error,
|
||||
});
|
||||
|
||||
export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
|
||||
export const checkLogin = () => (
|
||||
dispatch,
|
||||
_,
|
||||
{ rest, client, localStorage }
|
||||
) => {
|
||||
dispatch(checkLoginRequest());
|
||||
return rest('/auth')
|
||||
.then(({user}) => {
|
||||
.then(({ user }) => {
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios && storage) {
|
||||
storage.removeItem('token');
|
||||
if (!bowser.safari && !bowser.ios && localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
@@ -127,9 +141,11 @@ export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
|
||||
client.resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
@@ -138,16 +154,17 @@ export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
|
||||
// LOGOUT
|
||||
//==============================================================================
|
||||
|
||||
export const logout = () => (dispatch, _, {rest, client, storage}) => {
|
||||
return rest('/auth', {method: 'DELETE'}).then(() => {
|
||||
if (storage) {
|
||||
storage.removeItem('token');
|
||||
export const logout = () => (dispatch, _, { rest, client, localStorage }) => {
|
||||
return rest('/auth', { method: 'DELETE' }).then(() => {
|
||||
if (localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({type: actions.LOGOUT});
|
||||
dispatch({ type: actions.LOGOUT });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -155,11 +172,10 @@ export const logout = () => (dispatch, _, {rest, client, storage}) => {
|
||||
// AUTH TOKEN
|
||||
//==============================================================================
|
||||
|
||||
export const handleAuthToken = (token) => (dispatch, _, {storage}) => {
|
||||
if (storage) {
|
||||
storage.setItem('exp', jwtDecode(token).exp);
|
||||
storage.setItem('token', token);
|
||||
export const handleAuthToken = token => (dispatch, _, { localStorage }) => {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
dispatch({type: 'HANDLE_AUTH_TOKEN'});
|
||||
dispatch({ type: 'HANDLE_AUTH_TOKEN' });
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import {SHOW_BAN_USER_DIALOG, HIDE_BAN_USER_DIALOG} from '../constants/banUserDialog';
|
||||
import {
|
||||
SHOW_BAN_USER_DIALOG,
|
||||
HIDE_BAN_USER_DIALOG,
|
||||
} from '../constants/banUserDialog';
|
||||
|
||||
export const showBanUserDialog = ({userId, username, commentId, commentStatus}) =>
|
||||
({type: SHOW_BAN_USER_DIALOG, userId, username, commentId, commentStatus});
|
||||
|
||||
export const hideBanUserDialog = () => ({type: HIDE_BAN_USER_DIALOG});
|
||||
export const showBanUserDialog = ({
|
||||
userId,
|
||||
username,
|
||||
commentId,
|
||||
commentStatus,
|
||||
}) => ({
|
||||
type: SHOW_BAN_USER_DIALOG,
|
||||
userId,
|
||||
username,
|
||||
commentId,
|
||||
commentStatus,
|
||||
});
|
||||
|
||||
export const hideBanUserDialog = () => ({ type: HIDE_BAN_USER_DIALOG });
|
||||
|
||||
@@ -7,73 +7,71 @@ import {
|
||||
SORT_UPDATE,
|
||||
SET_PAGE,
|
||||
SET_SEARCH_VALUE,
|
||||
SET_ROLE,
|
||||
SET_COMMENTER_STATUS,
|
||||
SHOW_BANUSER_DIALOG,
|
||||
HIDE_BANUSER_DIALOG,
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG
|
||||
HIDE_REJECT_USERNAME_DIALOG,
|
||||
SET_INDICATOR_TRACK,
|
||||
} from '../constants/community';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const fetchUsers = (query = {}) => (dispatch, _, {rest}) => {
|
||||
export const fetchUsers = (query = {}) => (dispatch, _, { rest }) => {
|
||||
dispatch(requestFetchUsers());
|
||||
rest(`/users?${queryString.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>{
|
||||
.then(({ result, page, count, limit, totalPages }) => {
|
||||
dispatch({
|
||||
type: FETCH_USERS_SUCCESS,
|
||||
users: result,
|
||||
page,
|
||||
count,
|
||||
limit,
|
||||
totalPages
|
||||
totalPages,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: FETCH_USERS_FAILURE, error: errorMessage});
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch({ type: FETCH_USERS_FAILURE, error: errorMessage });
|
||||
});
|
||||
};
|
||||
|
||||
const requestFetchUsers = () => ({
|
||||
type: FETCH_USERS_REQUEST
|
||||
type: FETCH_USERS_REQUEST,
|
||||
});
|
||||
|
||||
export const updateSorting = (sort) => ({
|
||||
export const updateSorting = sort => ({
|
||||
type: SORT_UPDATE,
|
||||
sort
|
||||
sort,
|
||||
});
|
||||
|
||||
export const setPage = (page) => ({
|
||||
export const setPage = page => ({
|
||||
type: SET_PAGE,
|
||||
page,
|
||||
});
|
||||
|
||||
export const setSearchValue = (value) => ({
|
||||
export const setSearchValue = value => ({
|
||||
type: SET_SEARCH_VALUE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const setRole = (id, role) => (dispatch, _, {rest}) => {
|
||||
return rest(`/users/${id}/role`, {method: 'POST', body: {role}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_ROLE, id, role});
|
||||
});
|
||||
};
|
||||
|
||||
export const setCommenterStatus = (id, status) => (dispatch, _, {rest}) => {
|
||||
return rest(`/users/${id}/status`, {method: 'POST', body: {status}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_COMMENTER_STATUS, id, status});
|
||||
});
|
||||
};
|
||||
|
||||
// Ban User Dialog
|
||||
export const showBanUserDialog = (user) => ({type: SHOW_BANUSER_DIALOG, user});
|
||||
export const hideBanUserDialog = () => ({type: HIDE_BANUSER_DIALOG});
|
||||
export const showBanUserDialog = user => ({ type: SHOW_BANUSER_DIALOG, user });
|
||||
export const hideBanUserDialog = () => ({ type: HIDE_BANUSER_DIALOG });
|
||||
|
||||
// Reject Username Dialog
|
||||
export const showRejectUsernameDialog = (user) => ({type: SHOW_REJECT_USERNAME_DIALOG, user});
|
||||
export const hideRejectUsernameDialog = () => ({type: HIDE_REJECT_USERNAME_DIALOG});
|
||||
export const showRejectUsernameDialog = user => ({
|
||||
type: SHOW_REJECT_USERNAME_DIALOG,
|
||||
user,
|
||||
});
|
||||
export const hideRejectUsernameDialog = () => ({
|
||||
type: HIDE_REJECT_USERNAME_DIALOG,
|
||||
});
|
||||
|
||||
// Enable or disable the activity indicator subscriptions.
|
||||
export const setIndicatorTrack = track => ({
|
||||
type: SET_INDICATOR_TRACK,
|
||||
track,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
|
||||
|
||||
export const fetchConfig = () => (dispatch) => {
|
||||
export const fetchConfig = () => dispatch => {
|
||||
let json = document.getElementById('data');
|
||||
let data = JSON.parse(json.textContent);
|
||||
dispatch({type: CONFIG_UPDATED, data});
|
||||
dispatch({ type: CONFIG_UPDATED, data });
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as actions from 'constants/configure';
|
||||
|
||||
export const updatePending = ({updater, errorUpdater}) => {
|
||||
return {type: actions.UPDATE_PENDING, updater, errorUpdater};
|
||||
export const updatePending = ({ updater, errorUpdater }) => {
|
||||
return { type: actions.UPDATE_PENDING, updater, errorUpdater };
|
||||
};
|
||||
|
||||
export const clearPending = () => {
|
||||
return {type: actions.CLEAR_PENDING};
|
||||
return { type: actions.CLEAR_PENDING };
|
||||
};
|
||||
|
||||
export const setActiveSection = (section) => {
|
||||
return {type: actions.SET_ACTIVE_SECTION, section};
|
||||
export const setActiveSection = section => {
|
||||
return { type: actions.SET_ACTIVE_SECTION, section };
|
||||
};
|
||||
|
||||
@@ -3,17 +3,17 @@ import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const nextStep = () => ({type: actions.NEXT_STEP});
|
||||
export const previousStep = () => ({type: actions.PREVIOUS_STEP});
|
||||
export const goToStep = (step) => ({type: actions.GO_TO_STEP, step});
|
||||
export const nextStep = () => ({ type: actions.NEXT_STEP });
|
||||
export const previousStep = () => ({ type: actions.PREVIOUS_STEP });
|
||||
export const goToStep = step => ({ type: actions.GO_TO_STEP, step });
|
||||
|
||||
const installRequest = () => ({type: actions.INSTALL_REQUEST});
|
||||
const installSuccess = () => ({type: actions.INSTALL_SUCCESS});
|
||||
const installFailure = (error) => ({type: actions.INSTALL_FAILURE, error});
|
||||
const installRequest = () => ({ type: actions.INSTALL_REQUEST });
|
||||
const installSuccess = () => ({ type: actions.INSTALL_SUCCESS });
|
||||
const installFailure = error => ({ type: actions.INSTALL_FAILURE, error });
|
||||
|
||||
const addError = (name, error) => ({type: actions.ADD_ERROR, name, error});
|
||||
const hasError = (error) => ({type: actions.HAS_ERROR, error});
|
||||
const clearErrors = () => ({type: actions.CLEAR_ERRORS});
|
||||
const addError = (name, error) => ({ type: actions.ADD_ERROR, name, error });
|
||||
const hasError = error => ({ type: actions.HAS_ERROR, error });
|
||||
const clearErrors = () => ({ type: actions.CLEAR_ERRORS });
|
||||
|
||||
const validation = (formData, dispatch, next) => {
|
||||
if (!(formData != null)) {
|
||||
@@ -21,24 +21,21 @@ const validation = (formData, dispatch, next) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const validKeys = Object.keys(formData)
|
||||
.filter((name) => name !== 'domains');
|
||||
const validKeys = Object.keys(formData).filter(name => name !== 'domains');
|
||||
|
||||
// Required Validation
|
||||
const empty = validKeys
|
||||
.filter((name) => {
|
||||
const cond = !formData[name].length;
|
||||
const empty = validKeys.filter(name => {
|
||||
const cond = !formData[name].length;
|
||||
|
||||
if (cond) {
|
||||
if (cond) {
|
||||
// Adding Error
|
||||
dispatch(addError(name, 'This field is required.'));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
|
||||
// Adding Error
|
||||
dispatch(addError(name, 'This field is required.'));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
|
||||
return cond;
|
||||
});
|
||||
return cond;
|
||||
});
|
||||
|
||||
if (empty.length) {
|
||||
dispatch(hasError());
|
||||
@@ -46,19 +43,17 @@ const validation = (formData, dispatch, next) => {
|
||||
}
|
||||
|
||||
// RegExp Validation
|
||||
const validation = validKeys
|
||||
.filter((name) => {
|
||||
const cond = !validate[name](formData[name]);
|
||||
if (cond) {
|
||||
|
||||
const validation = validKeys.filter(name => {
|
||||
const cond = !validate[name](formData[name]);
|
||||
if (cond) {
|
||||
// Adding Error
|
||||
dispatch(addError(name, errorMsj[name]));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
dispatch(addError(name, errorMsj[name]));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
|
||||
return cond;
|
||||
});
|
||||
return cond;
|
||||
});
|
||||
|
||||
if (validation.length) {
|
||||
dispatch(hasError());
|
||||
@@ -67,20 +62,21 @@ const validation = (formData, dispatch, next) => {
|
||||
|
||||
// Confirm Validation
|
||||
const prefixLength = 'confirm'.length;
|
||||
const confirm = validKeys
|
||||
.filter((name) => {
|
||||
if (!name.startsWith('confirm')) {
|
||||
return false;
|
||||
}
|
||||
const confirm = validKeys.filter(name => {
|
||||
if (!name.startsWith('confirm')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that 'confirmX' equals 'X'.
|
||||
const other = name.substr(prefixLength, 1).toLowerCase() + name.substr(prefixLength + 1);
|
||||
const cond = formData[other] !== formData[name];
|
||||
if (cond) {
|
||||
dispatch(addError(name, errorMsj[name]));
|
||||
}
|
||||
return cond;
|
||||
});
|
||||
// Check that 'confirmX' equals 'X'.
|
||||
const other =
|
||||
name.substr(prefixLength, 1).toLowerCase() +
|
||||
name.substr(prefixLength + 1);
|
||||
const cond = formData[other] !== formData[name];
|
||||
if (cond) {
|
||||
dispatch(addError(name, errorMsj[name]));
|
||||
}
|
||||
return cond;
|
||||
});
|
||||
|
||||
if (confirm.length) {
|
||||
dispatch(hasError());
|
||||
@@ -105,41 +101,62 @@ export const submitUser = () => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const finishInstall = () => (dispatch, getState, {rest}) => {
|
||||
export const finishInstall = () => (dispatch, getState, { rest }) => {
|
||||
const data = getState().install.data;
|
||||
dispatch(installRequest());
|
||||
return rest('/setup', {method: 'POST', body: data})
|
||||
return rest('/setup', { method: 'POST', body: data })
|
||||
.then(() => {
|
||||
dispatch(installSuccess());
|
||||
dispatch(nextStep());
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(installFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSettingsFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_SETTINGS, name, value});
|
||||
export const updateUserFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_USER, name, value});
|
||||
export const updatePermittedDomains = (value) => ({type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS, value});
|
||||
export const updateSettingsFormData = (name, value) => ({
|
||||
type: actions.UPDATE_FORMDATA_SETTINGS,
|
||||
name,
|
||||
value,
|
||||
});
|
||||
export const updateUserFormData = (name, value) => ({
|
||||
type: actions.UPDATE_FORMDATA_USER,
|
||||
name,
|
||||
value,
|
||||
});
|
||||
export const updatePermittedDomains = value => ({
|
||||
type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS,
|
||||
value,
|
||||
});
|
||||
|
||||
const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
|
||||
const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
|
||||
const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error});
|
||||
const checkInstallRequest = () => ({ type: actions.CHECK_INSTALL_REQUEST });
|
||||
const checkInstallSuccess = installed => ({
|
||||
type: actions.CHECK_INSTALL_SUCCESS,
|
||||
installed,
|
||||
});
|
||||
const checkInstallFailure = error => ({
|
||||
type: actions.CHECK_INSTALL_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const checkInstall = (next) => async (dispatch, _, {rest}) => {
|
||||
export const checkInstall = next => async (dispatch, _, { rest }) => {
|
||||
dispatch(checkInstallRequest());
|
||||
|
||||
try {
|
||||
const {installed} = await rest('/setup');
|
||||
const { installed } = await rest('/setup');
|
||||
dispatch(checkInstallSuccess(installed));
|
||||
if (installed) {
|
||||
next();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(checkInstallFailure(errorMessage));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,36 +1,46 @@
|
||||
import * as actions from 'constants/moderation';
|
||||
|
||||
export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open});
|
||||
export const singleView = () => ({type: actions.SINGLE_VIEW});
|
||||
export const toggleModal = open => ({ type: actions.TOGGLE_MODAL, open });
|
||||
export const singleView = () => ({ type: actions.SINGLE_VIEW });
|
||||
|
||||
// hide shortcuts note
|
||||
export const hideShortcutsNote = () => (dispatch, _, {storage}) => {
|
||||
export const hideShortcutsNote = () => (dispatch, _, { localStorage }) => {
|
||||
try {
|
||||
if (storage) {
|
||||
storage.setItem('coral:shortcutsNote', 'hide');
|
||||
if (localStorage) {
|
||||
localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
// above will fail in Safari private mode
|
||||
}
|
||||
|
||||
dispatch({type: actions.HIDE_SHORTCUTS_NOTE});
|
||||
dispatch({ type: actions.HIDE_SHORTCUTS_NOTE });
|
||||
};
|
||||
|
||||
export const setSortOrder = (order) => ({
|
||||
export const setSortOrder = order => ({
|
||||
type: actions.SET_SORT_ORDER,
|
||||
order
|
||||
order,
|
||||
});
|
||||
|
||||
export const toggleStorySearch = (active) => ({
|
||||
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH
|
||||
export const toggleStorySearch = active => ({
|
||||
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH,
|
||||
});
|
||||
|
||||
export const storySearchChange = (value) => ({
|
||||
export const storySearchChange = value => ({
|
||||
type: actions.STORY_SEARCH_CHANGE_VALUE,
|
||||
value
|
||||
value,
|
||||
});
|
||||
|
||||
export const clearState = () => ({
|
||||
type: actions.MODERATION_CLEAR_STATE
|
||||
type: actions.CLEAR_STATE,
|
||||
});
|
||||
|
||||
export const selectCommentId = id => ({
|
||||
type: actions.SELECT_COMMENT,
|
||||
id,
|
||||
});
|
||||
|
||||
// Enable or disable the activity indicator subscriptions.
|
||||
export const setIndicatorTrack = track => ({
|
||||
type: actions.SET_INDICATOR_TRACK,
|
||||
track,
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
UPDATE_ASSET_STATE_REQUEST,
|
||||
UPDATE_ASSET_STATE_SUCCESS,
|
||||
UPDATE_ASSET_STATE_FAILURE,
|
||||
UPDATE_ASSETS
|
||||
UPDATE_ASSETS,
|
||||
} from '../constants/stories';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -21,53 +21,58 @@ import t from 'coral-framework/services/i18n';
|
||||
|
||||
// Fetch a page of assets
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const fetchAssets = (query = {}) => (dispatch, _, {rest}) => {
|
||||
dispatch({type: FETCH_ASSETS_REQUEST});
|
||||
export const fetchAssets = (query = {}) => (dispatch, _, { rest }) => {
|
||||
dispatch({ type: FETCH_ASSETS_REQUEST });
|
||||
return rest(`/assets?${queryString.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>
|
||||
dispatch({type: FETCH_ASSETS_SUCCESS,
|
||||
.then(({ result, page, count, limit, totalPages }) =>
|
||||
dispatch({
|
||||
type: FETCH_ASSETS_SUCCESS,
|
||||
assets: result,
|
||||
page,
|
||||
count,
|
||||
limit,
|
||||
totalPages,
|
||||
}))
|
||||
.catch((error) => {
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: FETCH_ASSETS_FAILURE, error: errorMessage});
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch({ type: FETCH_ASSETS_FAILURE, error: errorMessage });
|
||||
});
|
||||
};
|
||||
|
||||
// Update an asset state
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const updateAssetState = (id, closedAt) => (dispatch, _, {rest}) => {
|
||||
dispatch({type: UPDATE_ASSET_STATE_REQUEST, id, closedAt});
|
||||
return rest(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
|
||||
.then(() => dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
|
||||
.catch((error) => {
|
||||
export const updateAssetState = (id, closedAt) => (dispatch, _, { rest }) => {
|
||||
dispatch({ type: UPDATE_ASSET_STATE_REQUEST, id, closedAt });
|
||||
return rest(`/assets/${id}/status`, { method: 'PUT', body: { closedAt } })
|
||||
.then(() => dispatch({ type: UPDATE_ASSET_STATE_SUCCESS }))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: UPDATE_ASSET_STATE_FAILURE, error: errorMessage});
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch({ type: UPDATE_ASSET_STATE_FAILURE, error: errorMessage });
|
||||
});
|
||||
};
|
||||
|
||||
export const updateAssets = (assets) => (dispatch) => {
|
||||
dispatch({type: UPDATE_ASSETS, assets});
|
||||
export const updateAssets = assets => dispatch => {
|
||||
dispatch({ type: UPDATE_ASSETS, assets });
|
||||
};
|
||||
|
||||
export const setPage = (page) => ({
|
||||
export const setPage = page => ({
|
||||
type: SET_PAGE,
|
||||
page,
|
||||
});
|
||||
|
||||
export const setSearchValue = (value) => ({
|
||||
export const setSearchValue = value => ({
|
||||
type: SET_SEARCH_VALUE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const setCriteria = (criteria) => ({
|
||||
export const setCriteria = criteria => ({
|
||||
type: SET_CRITERIA,
|
||||
criteria,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import {SHOW_SUSPEND_USER_DIALOG, HIDE_SUSPEND_USER_DIALOG} from '../constants/suspendUserDialog.js';
|
||||
import {
|
||||
SHOW_SUSPEND_USER_DIALOG,
|
||||
HIDE_SUSPEND_USER_DIALOG,
|
||||
} from '../constants/suspendUserDialog.js';
|
||||
|
||||
export const showSuspendUserDialog = ({userId, username, commentId, commentStatus}) =>
|
||||
({type: SHOW_SUSPEND_USER_DIALOG, userId, username, commentId, commentStatus});
|
||||
|
||||
export const hideSuspendUserDialog = () => ({type: HIDE_SUSPEND_USER_DIALOG});
|
||||
export const showSuspendUserDialog = ({
|
||||
userId,
|
||||
username,
|
||||
commentId,
|
||||
commentStatus,
|
||||
}) => ({
|
||||
type: SHOW_SUSPEND_USER_DIALOG,
|
||||
userId,
|
||||
username,
|
||||
commentId,
|
||||
commentStatus,
|
||||
});
|
||||
|
||||
export const hideSuspendUserDialog = () => ({ type: HIDE_SUSPEND_USER_DIALOG });
|
||||
|
||||
@@ -1,28 +1,37 @@
|
||||
import * as actions from 'constants/userDetail';
|
||||
|
||||
export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, userId});
|
||||
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
|
||||
export const viewUserDetail = userId => ({
|
||||
type: actions.VIEW_USER_DETAIL,
|
||||
userId,
|
||||
});
|
||||
export const hideUserDetail = () => ({ type: actions.HIDE_USER_DETAIL });
|
||||
|
||||
export const changeUserDetailStatuses = (tab) => {
|
||||
export const changeTab = tab => {
|
||||
let statuses = null;
|
||||
if (tab === 'rejected') {
|
||||
statuses = ['REJECTED'];
|
||||
}
|
||||
return {type: actions.CHANGE_USER_DETAIL_STATUSES, tab, statuses};
|
||||
return { type: actions.CHANGE_TAB_USER_DETAIL, tab, statuses };
|
||||
};
|
||||
|
||||
export const clearUserDetailSelections = () => ({type: actions.CLEAR_USER_DETAIL_SELECTIONS});
|
||||
export const clearUserDetailSelections = () => ({
|
||||
type: actions.CLEAR_USER_DETAIL_SELECTIONS,
|
||||
});
|
||||
|
||||
export const toggleSelectCommentInUserDetail = (id, active) => {
|
||||
return {
|
||||
type: active ? actions.SELECT_USER_DETAIL_COMMENT : actions.UNSELECT_USER_DETAIL_COMMENT,
|
||||
id
|
||||
type: active
|
||||
? actions.SELECT_USER_DETAIL_COMMENT
|
||||
: actions.UNSELECT_USER_DETAIL_COMMENT,
|
||||
id,
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleSelectAllCommentInUserDetail = (ids, active) => {
|
||||
return {
|
||||
type: active ? actions.SELECT_ALL_USER_DETAIL_COMMENT : actions.CLEAR_USER_DETAIL_SELECTIONS,
|
||||
ids
|
||||
type: active
|
||||
? actions.SELECT_ALL_USER_DETAIL_COMMENT
|
||||
: actions.CLEAR_USER_DETAIL_SELECTIONS,
|
||||
ids,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,38 +6,56 @@ import t from 'coral-framework/services/i18n';
|
||||
*/
|
||||
// change status of a user
|
||||
export const userStatusUpdate = (status, userId, commentId) => {
|
||||
return (dispatch, _, {rest}) => {
|
||||
dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
|
||||
return rest(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
|
||||
.then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
|
||||
.catch((error) => {
|
||||
return (dispatch, _, { rest }) => {
|
||||
dispatch({ type: userTypes.UPDATE_STATUS_REQUEST });
|
||||
return rest(`/users/${userId}/status`, {
|
||||
method: 'POST',
|
||||
body: { status: status, comment_id: commentId },
|
||||
})
|
||||
.then(res => dispatch({ type: userTypes.UPDATE_STATUS_SUCCESS, res }))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error: errorMessage});
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch({
|
||||
type: userTypes.UPDATE_STATUS_FAILURE,
|
||||
error: errorMessage,
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// change status of a user
|
||||
export const sendNotificationEmail = (userId, subject, body) => {
|
||||
return (dispatch, _, {rest}) => {
|
||||
return rest(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: userTypes.USER_EMAIL_FAILURE, error: errorMessage});
|
||||
});
|
||||
return (dispatch, _, { rest }) => {
|
||||
return rest(`/users/${userId}/email`, {
|
||||
method: 'POST',
|
||||
body: { subject, body },
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch({ type: userTypes.USER_EMAIL_FAILURE, error: errorMessage });
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// let a user edit their username
|
||||
export const enableUsernameEdit = (userId) => {
|
||||
return (dispatch, _, {rest}) => {
|
||||
return rest(`/users/${userId}/username-enable`, {method: 'POST'})
|
||||
.catch((error) => {
|
||||
export const enableUsernameEdit = userId => {
|
||||
return (dispatch, _, { rest }) => {
|
||||
return rest(`/users/${userId}/username-enable`, { method: 'POST' }).catch(
|
||||
error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error: errorMessage});
|
||||
});
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch({
|
||||
type: userTypes.USERNAME_ENABLE_FAILURE,
|
||||
error: errorMessage,
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
.table {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table, .headerRow, .row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.headerRowItem, .item {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
|
||||
&:nth-child(2) {
|
||||
flex: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.headerRowItem {
|
||||
color: #595959;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.headerRow, .row {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.action {
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { murmur3 } from 'murmurhash-js';
|
||||
import styles from './AccountHistory.css';
|
||||
import cn from 'classnames';
|
||||
import flatten from 'lodash/flatten';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import moment from 'moment';
|
||||
|
||||
const buildUserHistory = (userState = {}) => {
|
||||
return orderBy(
|
||||
flatten(
|
||||
Object.keys(userState.status)
|
||||
.filter(k => k !== '__typename')
|
||||
.map(k => userState.status[k].history)
|
||||
),
|
||||
'created_at',
|
||||
'desc'
|
||||
);
|
||||
};
|
||||
|
||||
const buildActionResponse = (typename, until, status) => {
|
||||
switch (typename) {
|
||||
case 'UsernameStatusHistory':
|
||||
return `Username ${status}`;
|
||||
case 'BannedStatusHistory':
|
||||
return status ? 'User banned' : 'Ban removed';
|
||||
case 'SuspensionStatusHistory':
|
||||
return until ? 'Account Suspended' : 'Suspension removed';
|
||||
default:
|
||||
return '-';
|
||||
}
|
||||
};
|
||||
|
||||
const getModerationValue = (userId, assignedBy = {}) => {
|
||||
if (assignedBy && userId !== assignedBy.id) {
|
||||
return assignedBy.username;
|
||||
}
|
||||
return 'SYSTEM';
|
||||
};
|
||||
|
||||
class AccountHistory extends React.Component {
|
||||
render() {
|
||||
const { user } = this.props;
|
||||
const userHistory = buildUserHistory(user.state);
|
||||
return (
|
||||
<div>
|
||||
<div className={cn(styles.table, 'talk-admin-account-history')}>
|
||||
<div
|
||||
className={cn(
|
||||
styles.headerRow,
|
||||
'talk-admin-account-history-header-row'
|
||||
)}
|
||||
>
|
||||
<div className={styles.headerRowItem}>Date</div>
|
||||
<div className={styles.headerRowItem}>Action</div>
|
||||
<div className={styles.headerRowItem}>Moderation</div>
|
||||
</div>
|
||||
{userHistory.map(
|
||||
({ __typename, created_at, assigned_by, until, status }) => (
|
||||
<div
|
||||
className={cn(styles.row, 'talk-admin-account-history-row')}
|
||||
key={`${__typename}_${murmur3(created_at)}`}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
styles.item,
|
||||
'talk-admin-account-history-row-date'
|
||||
)}
|
||||
>
|
||||
{moment(new Date(created_at)).format('MMM DD, YYYY')}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
styles.item,
|
||||
styles.action,
|
||||
'talk-admin-account-history-row-status'
|
||||
)}
|
||||
>
|
||||
{buildActionResponse(__typename, until, status)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
styles.item,
|
||||
'talk-admin-account-history-row-assigned-by'
|
||||
)}
|
||||
>
|
||||
{getModerationValue(user.id, assigned_by)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AccountHistory.propTypes = {
|
||||
user: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default AccountHistory;
|
||||
@@ -8,9 +8,6 @@
|
||||
color: black;
|
||||
> :global(.mdl-menu__container) {
|
||||
margin-left: 10px;
|
||||
> :global(.mdl-menu__outline) {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +15,13 @@
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #616161;
|
||||
border-color: #616161;
|
||||
}
|
||||
|
||||
.arrowIcon {
|
||||
margin-left: 6px;
|
||||
margin-right: 0;
|
||||
vertical-align: middle;
|
||||
vertical-align: middle;
|
||||
margin-right: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -33,8 +31,10 @@
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
background-color: #2a2a2a;
|
||||
color: white;
|
||||
color: #2a2a2a;
|
||||
background-color: white;
|
||||
font-size: 0.95em;
|
||||
|
||||
&:first-child {
|
||||
margin-bottom: 1px;
|
||||
border-radius: 2px 2px 0px 0px;
|
||||
@@ -43,7 +43,8 @@
|
||||
border-radius: 0px 0px 2px 2px;
|
||||
}
|
||||
&:hover, &:active, &:focus {
|
||||
background-color: #767676;
|
||||
background-color: #e2e2e2;
|
||||
border-color: #616161;
|
||||
}
|
||||
&[disabled], &[disabled]:hover, &[disabled]:focus, &[disabled]:active {
|
||||
background-color: #262626;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, Icon} from 'coral-ui';
|
||||
import {Menu} from 'react-mdl';
|
||||
import { Button, Icon } from 'coral-ui';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import { Menu } from 'react-mdl';
|
||||
import cn from 'classnames';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import styles from './ActionsMenu.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -13,46 +14,60 @@ let count = 0;
|
||||
class ActionsMenu extends React.Component {
|
||||
id = `actions-dropdown-${count++}`;
|
||||
menu = null;
|
||||
state = {open: false};
|
||||
state = { open: false };
|
||||
timeout = null;
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
handleRef = (ref) => {
|
||||
handleRef = ref => {
|
||||
this.menu = ref ? findDOMNode(ref).parentNode : null;
|
||||
}
|
||||
};
|
||||
|
||||
syncOpenState = () => {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
this.setState({open: this.menu.className.indexOf('is-visible') >= 0});
|
||||
this.setState({ open: this.menu.className.indexOf('is-visible') >= 0 });
|
||||
}, 150);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {className = ''} = this.props;
|
||||
const { className = '', buttonClassNames = '', label = '' } = this.props;
|
||||
return (
|
||||
<div className={cn(styles.root, className)} onBlur={this.syncOpenState} >
|
||||
<Button
|
||||
cStyle='actions'
|
||||
className={cn(styles.button, {[styles.buttonOpen]: this.state.open})}
|
||||
disabled={false}
|
||||
id={this.id}
|
||||
<ClickOutside onClickOutside={this.syncOpenState}>
|
||||
<div
|
||||
className={cn(styles.root, className)}
|
||||
onBlur={this.syncOpenState}
|
||||
onClick={this.syncOpenState}
|
||||
icon={this.props.icon}
|
||||
raised>
|
||||
{t('modqueue.actions')}
|
||||
<Icon
|
||||
name={this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
|
||||
className={styles.arrowIcon}
|
||||
/>
|
||||
</Button>
|
||||
<Menu target={this.id} className={styles.menu} ref={this.handleRef}>
|
||||
{this.props.children}
|
||||
</Menu>
|
||||
</div>
|
||||
onKeyUp={this.syncOpenState}
|
||||
>
|
||||
<Button
|
||||
cStyle="actions"
|
||||
className={cn(
|
||||
styles.button,
|
||||
{ [styles.buttonOpen]: this.state.open },
|
||||
buttonClassNames
|
||||
)}
|
||||
disabled={false}
|
||||
id={this.id}
|
||||
onClick={this.syncOpenState}
|
||||
icon={this.props.icon}
|
||||
raised
|
||||
>
|
||||
{label ? label : t('modqueue.actions')}
|
||||
<Icon
|
||||
name={
|
||||
this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'
|
||||
}
|
||||
className={styles.arrowIcon}
|
||||
/>
|
||||
</Button>
|
||||
<Menu target={this.id} className={styles.menu} ref={this.handleRef}>
|
||||
{this.props.children}
|
||||
</Menu>
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -61,6 +76,8 @@ ActionsMenu.propTypes = {
|
||||
icon: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
buttonClassNames: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ActionsMenu;
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import {MenuItem} from 'react-mdl';
|
||||
import { MenuItem } from 'react-mdl';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ActionsMenu.css';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
|
||||
const ActionsMenuItem = (props) =>
|
||||
<MenuItem className={cn(styles.menuItem, props.className, 'action-menu-item')} {...props} id={camelCase(props.children)}/>;
|
||||
|
||||
const ActionsMenuItem = props => (
|
||||
<MenuItem
|
||||
className={cn(styles.menuItem, props.className, 'action-menu-item')}
|
||||
{...props}
|
||||
id={camelCase(props.children)}
|
||||
/>
|
||||
);
|
||||
|
||||
ActionsMenuItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.string,
|
||||
|
||||
@@ -1,103 +1,129 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
import Layout from 'coral-admin/src/components/Layout';
|
||||
import styles from './NotFound.css';
|
||||
import {Button, TextField, Alert, Success} from 'coral-ui';
|
||||
import { Button, TextField, Alert, Success } from 'coral-ui';
|
||||
import Recaptcha from 'react-recaptcha';
|
||||
import cn from 'classnames';
|
||||
|
||||
class AdminLogin extends React.Component {
|
||||
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {email: '', password: '', requestPassword: false};
|
||||
this.state = { email: '', password: '', requestPassword: false };
|
||||
}
|
||||
|
||||
handleSignIn = (e) => {
|
||||
handleSignIn = e => {
|
||||
e.preventDefault();
|
||||
this.props.handleLogin(this.state.email, this.state.password);
|
||||
}
|
||||
};
|
||||
|
||||
onRecaptchaLoad = () => {
|
||||
|
||||
// do something?
|
||||
}
|
||||
};
|
||||
|
||||
onRecaptchaVerify = (recaptchaResponse) => {
|
||||
this.props.handleLogin(this.state.email, this.state.password, recaptchaResponse);
|
||||
}
|
||||
onRecaptchaVerify = recaptchaResponse => {
|
||||
this.props.handleLogin(
|
||||
this.state.email,
|
||||
this.state.password,
|
||||
recaptchaResponse
|
||||
);
|
||||
};
|
||||
|
||||
handleRequestPassword = (e) => {
|
||||
handleRequestPassword = e => {
|
||||
e.preventDefault();
|
||||
this.props.requestPasswordReset(this.state.email);
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props;
|
||||
render() {
|
||||
const { errorMessage, loginMaxExceeded, recaptchaPublic } = this.props;
|
||||
const signInForm = (
|
||||
<form className="talk-admin-login-sign-in" onSubmit={this.handleSignIn}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label='Email Address'
|
||||
label="Email Address"
|
||||
value={this.state.email}
|
||||
onChange={(e) => this.setState({email: e.target.value})} />
|
||||
onChange={e => this.setState({ email: e.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label='Password'
|
||||
label="Password"
|
||||
value={this.state.password}
|
||||
onChange={(e) => this.setState({password: e.target.value})}
|
||||
type='password' />
|
||||
<div style={{height: 10}}></div>
|
||||
onChange={e => this.setState({ password: e.target.value })}
|
||||
type="password"
|
||||
/>
|
||||
<div style={{ height: 10 }} />
|
||||
<Button
|
||||
className="talk-admin-login-sign-in-button"
|
||||
type='submit'
|
||||
cStyle='black'
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
full
|
||||
onClick={this.handleSignIn}>Sign In</Button>
|
||||
onClick={this.handleSignIn}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
Forgot your password? <a href="#" className={styles.forgotPasswordLink} onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.setState({requestPassword: true});
|
||||
}}>Request a new one.</a>
|
||||
Forgot your password?{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
this.setState({ requestPassword: true });
|
||||
}}
|
||||
>
|
||||
Request a new one.
|
||||
</a>
|
||||
</p>
|
||||
{
|
||||
loginMaxExceeded &&
|
||||
{loginMaxExceeded && (
|
||||
<Recaptcha
|
||||
sitekey={recaptchaPublic}
|
||||
render='explicit'
|
||||
theme='dark'
|
||||
render="explicit"
|
||||
theme="dark"
|
||||
onloadCallback={this.onRecaptchaLoad}
|
||||
verifyCallback={this.onRecaptchaVerify} />
|
||||
}
|
||||
verifyCallback={this.onRecaptchaVerify}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
const requestPasswordForm = (
|
||||
this.props.passwordRequestSuccess
|
||||
? <p className={styles.passwordRequestSuccess} onClick={() => {
|
||||
const requestPasswordForm = this.props.passwordRequestSuccess ? (
|
||||
<p
|
||||
className={styles.passwordRequestSuccess}
|
||||
onClick={() => {
|
||||
location.href = location.href;
|
||||
}}>
|
||||
{this.props.passwordRequestSuccess} <a className={styles.signInLink} href="#">Sign in</a>
|
||||
<Success />
|
||||
</p>
|
||||
: <form onSubmit={this.handleRequestPassword}>
|
||||
<TextField
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
onChange={(e) => this.setState({email: e.target.value})} />
|
||||
<Button
|
||||
type='submit'
|
||||
cStyle='black'
|
||||
full
|
||||
onClick={this.handleRequestPassword}>Reset Password</Button>
|
||||
</form>
|
||||
}}
|
||||
>
|
||||
{this.props.passwordRequestSuccess}{' '}
|
||||
<a className={styles.signInLink} href="#">
|
||||
Sign in
|
||||
</a>
|
||||
<Success />
|
||||
</p>
|
||||
) : (
|
||||
<form onSubmit={this.handleRequestPassword}>
|
||||
<TextField
|
||||
label="Email Address"
|
||||
value={this.state.email}
|
||||
onChange={e => this.setState({ email: e.target.value })}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
full
|
||||
onClick={this.handleRequestPassword}
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<div className={cn(styles.loginLayout, 'talk-admin-login')}>
|
||||
<h1 className={styles.loginHeader}>Team sign in</h1>
|
||||
<p className={styles.loginCTA}>Sign in to interact with your community.</p>
|
||||
{ this.state.requestPassword ? requestPasswordForm : signInForm }
|
||||
<p className={styles.loginCTA}>
|
||||
Sign in to interact with your community.
|
||||
</p>
|
||||
{this.state.requestPassword ? requestPasswordForm : signInForm}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'material-design-lite';
|
||||
import AppRouter from '../AppRouter';
|
||||
|
||||
export default class App extends React.Component {
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<ToastContainer />
|
||||
|
||||
@@ -5,23 +5,22 @@
|
||||
background: white;
|
||||
padding: 10px 12px;
|
||||
box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
line-height: 24px;
|
||||
font-size: 17px;
|
||||
height: 47px;
|
||||
border-radius: 3px;
|
||||
text-transform: capitalize;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
|
||||
width: 129px;
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
margin: 0 0 .5em;
|
||||
|
||||
&:hover {
|
||||
&:not(:disabled):hover {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #519954;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
|
||||
@@ -3,16 +3,21 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import cn from 'classnames';
|
||||
import styles from './ApproveButton.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
import { Icon } from 'coral-ui';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ApproveButton = ({active, minimal, onClick, className}) => {
|
||||
const ApproveButton = ({ active, minimal, onClick, className, disabled }) => {
|
||||
const text = active ? t('modqueue.approved') : t('modqueue.approve');
|
||||
return (
|
||||
<button
|
||||
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
|
||||
className={cn(
|
||||
styles.root,
|
||||
{ [styles.minimal]: minimal, [styles.active]: active },
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled || active}
|
||||
>
|
||||
<Icon name={'done'} className={styles.icon} />
|
||||
{!minimal && text}
|
||||
@@ -24,8 +29,8 @@ ApproveButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ApproveButton;
|
||||
|
||||
|
||||
@@ -1,153 +1,153 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 500px;
|
||||
width: 400px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 184px;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: black;
|
||||
font-size: 1.76em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
.header {
|
||||
color: black;
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: black;
|
||||
font-size: 1.4em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
.subheader {
|
||||
color: black;
|
||||
font-size: 1.3em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.textField {
|
||||
margin-top: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.textField label {
|
||||
font-size: 1.08em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
font-size: 1.08em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.textField input {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
outline: none;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
padding: 10px 6px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin: 5px auto;
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
outline: none;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
padding: 10px 6px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin: 5px auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin: 20px auto 10px;
|
||||
text-align: center;
|
||||
margin: 20px auto 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer span {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.socialConnections {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.signInButton {
|
||||
margin-top: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #6b6b6b;
|
||||
color: #6b6b6b;
|
||||
}
|
||||
|
||||
input.error{
|
||||
border: solid 2px #f44336;
|
||||
border: solid 2px #f44336;
|
||||
}
|
||||
|
||||
.errorMsg, .hint {
|
||||
color: grey;
|
||||
font-weight: 600;
|
||||
padding: 3px 0 16px;
|
||||
color: grey;
|
||||
font-weight: 600;
|
||||
padding: 3px 0 16px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.alert--success {
|
||||
border: solid 1px #1ec00e;
|
||||
background: #cbf1b8;
|
||||
color: #006900;
|
||||
border: solid 1px #1ec00e;
|
||||
background: #cbf1b8;
|
||||
color: #006900;
|
||||
}
|
||||
|
||||
.alert--error {
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
}
|
||||
|
||||
.userBox a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.attention {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: #B71C1C;
|
||||
color: #FFEBEE;
|
||||
font-weight: bolder;
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 9px;
|
||||
line-height: 7px;
|
||||
text-align: center;
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: #B71C1C;
|
||||
color: #FFEBEE;
|
||||
font-weight: bolder;
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 9px;
|
||||
line-height: 7px;
|
||||
text-align: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.passwordRequestSuccess {
|
||||
border: 1px solid green;
|
||||
background-color: lightgreen;
|
||||
padding: 10px;
|
||||
border: 1px solid green;
|
||||
background-color: lightgreen;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.passwordRequestFailure {
|
||||
border: 1px solid orange;
|
||||
background-color: 1px solid coral;
|
||||
padding: 10px;
|
||||
border: 1px solid orange;
|
||||
background-color: 1px solid coral;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
@@ -160,6 +160,20 @@ input.error{
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin: 20px;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.legend {
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.messageInput {
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -1,45 +1,130 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import { Dialog } from 'coral-ui';
|
||||
import styles from './BanUserDialog.css';
|
||||
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const BanUserDialog = ({open, onCancel, onPerform, username, info}) => (
|
||||
<Dialog
|
||||
className={cn(styles.dialog, 'talk-ban-user-dialog')}
|
||||
id="banUserDialog"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
title={t('bandialog.ban_user')}>
|
||||
<span className={styles.close} onClick={onCancel}>×</span>
|
||||
<div className={styles.header}>
|
||||
<h2>{t('bandialog.ban_user')}</h2>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h3>{t('bandialog.are_you_sure', username)}</h3>
|
||||
<i>{info}</i>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
className={cn(styles.cancel, 'talk-ban-user-dialog-button-cancel')}
|
||||
cStyle="cancel"
|
||||
onClick={onCancel}
|
||||
raised >
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(styles.ban, 'talk-ban-user-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={onPerform}
|
||||
raised >
|
||||
{t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
const initialState = { step: 0, message: '' };
|
||||
|
||||
class BanUserDialog extends React.Component {
|
||||
state = initialState;
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
if (this.props.open && !next.open) {
|
||||
this.setState(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
handleMessageChange = e => {
|
||||
const { value: message } = e;
|
||||
this.setState({ message });
|
||||
};
|
||||
|
||||
goToStep1 = () => {
|
||||
this.setState({
|
||||
step: 1,
|
||||
message: t('bandialog.email_message_ban', this.props.username),
|
||||
});
|
||||
};
|
||||
|
||||
renderStep0() {
|
||||
const { onCancel, username, info } = this.props;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 className={styles.header}>{t('bandialog.ban_user')}</h2>
|
||||
<h3 className={styles.subheader}>
|
||||
{t('bandialog.are_you_sure', username)}
|
||||
</h3>
|
||||
<p className={styles.description}>{info}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
className={cn('talk-ban-user-dialog-button-cancel')}
|
||||
cStyle="white"
|
||||
onClick={onCancel}
|
||||
raised
|
||||
>
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn('talk-ban-user-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={this.goToStep1}
|
||||
raised
|
||||
>
|
||||
{t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
renderStep1() {
|
||||
const { onCancel, onPerform } = this.props;
|
||||
const { message } = this.state;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 className={styles.header}>{t('bandialog.notify_ban_headline')}</h2>
|
||||
<p className={styles.description}>
|
||||
{t('bandialog.notify_ban_description')}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>
|
||||
{t('bandialog.write_a_message')}
|
||||
</legend>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={styles.messageInput}
|
||||
value={message}
|
||||
onChange={this.handleMessageChange}
|
||||
/>
|
||||
</fieldset>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
className={cn('talk-ban-user-dialog-button-cancel')}
|
||||
cStyle="white"
|
||||
onClick={onCancel}
|
||||
raised
|
||||
>
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn('talk-ban-user-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={onPerform}
|
||||
raised
|
||||
>
|
||||
{t('bandialog.send')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { step } = this.state;
|
||||
const { open, onCancel } = this.props;
|
||||
return (
|
||||
<Dialog
|
||||
className={cn(styles.dialog, 'talk-ban-user-dialog')}
|
||||
id="banUserDialog"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
title={t('bandialog.ban_user')}
|
||||
>
|
||||
<span className={styles.close} onClick={onCancel}>
|
||||
×
|
||||
</span>
|
||||
{step === 0 && this.renderStep0()}
|
||||
{step === 1 && this.renderStep1()}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BanUserDialog.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import React from 'react';
|
||||
import {Button} from 'coral-ui';
|
||||
import { Button } from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {withCopyToClipboard} from 'coral-framework/hocs';
|
||||
import { withCopyToClipboard } from 'coral-framework/hocs';
|
||||
|
||||
class ButtonCopyToClipboard extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<Button {...this.props} >
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
);
|
||||
render() {
|
||||
return <Button {...this.props}>{t('common.copy')}</Button>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import {murmur3} from 'murmurhash-js';
|
||||
import {CSSTransitionGroup} from 'react-transition-group';
|
||||
import { murmur3 } from 'murmurhash-js';
|
||||
import { CSSTransitionGroup } from 'react-transition-group';
|
||||
import styles from './CommentAnimatedEdit.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const CommentBodyHighlighter = ({children, body}) => {
|
||||
const CommentAnimatedEdit = ({ children, body }) => {
|
||||
return (
|
||||
<CSSTransitionGroup
|
||||
component={'div'}
|
||||
@@ -20,14 +20,16 @@ const CommentBodyHighlighter = ({children, body}) => {
|
||||
transitionEnterTimeout={3600}
|
||||
transitionLeaveTimeout={2800}
|
||||
>
|
||||
{React.cloneElement(React.Children.only(children), {key: murmur3(body)})}
|
||||
{React.cloneElement(React.Children.only(children), {
|
||||
key: murmur3(body),
|
||||
})}
|
||||
</CSSTransitionGroup>
|
||||
);
|
||||
};
|
||||
|
||||
CommentBodyHighlighter.propTypes = {
|
||||
CommentAnimatedEdit.propTypes = {
|
||||
children: PropTypes.node,
|
||||
body: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CommentBodyHighlighter;
|
||||
export default CommentAnimatedEdit;
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import React from 'react';
|
||||
import {matchLinks} from '../utils';
|
||||
import memoize from 'lodash/memoize';
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
// generate a regulare expression that catches the `phrases`.
|
||||
function generateRegExp(phrases) {
|
||||
const inner = phrases
|
||||
.map((phrase) =>
|
||||
phrase.split(/\s+/)
|
||||
.map((word) => escapeRegExp(word))
|
||||
.join('[\\s"?!.]+')
|
||||
).join('|');
|
||||
|
||||
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
|
||||
try {
|
||||
return new RegExp(pattern, 'iu');
|
||||
}
|
||||
catch (_err) {
|
||||
|
||||
// IE does not support unicode support, so we'll create one without.
|
||||
return new RegExp(pattern, 'i');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
|
||||
function getPhrasesRegexp(suspectWords, bannedWords) {
|
||||
return generateRegExp([...suspectWords, ...bannedWords]);
|
||||
}
|
||||
|
||||
// Memoized version as arguments rarely change.
|
||||
const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
|
||||
|
||||
// markPhrases looks for `supsectWords` and `bannedWords` inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
|
||||
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
|
||||
const tokens = body.split(regexp);
|
||||
return tokens.map((token, i) =>
|
||||
i % 3 === 2
|
||||
? <mark key={`${keyPrefix}_${i}`}>{token}</mark>
|
||||
: token
|
||||
);
|
||||
}
|
||||
|
||||
// markLinks looks for links inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markLinks(body) {
|
||||
const matches = matchLinks(body);
|
||||
const content = [];
|
||||
let index = 0;
|
||||
if (matches) {
|
||||
matches
|
||||
.forEach((match, i) => {
|
||||
content.push(body.substring(index, match.index));
|
||||
content.push(<mark key={i}>{match.text}</mark>);
|
||||
index = match.lastIndex;
|
||||
});
|
||||
}
|
||||
content.push(body.substring(index));
|
||||
return content;
|
||||
}
|
||||
|
||||
export default ({suspectWords, bannedWords, body, ...rest}) => {
|
||||
|
||||
// First highlight links.
|
||||
const content = markLinks(body)
|
||||
.map((element, index) => {
|
||||
|
||||
// Keep highlighted links.
|
||||
if (typeof element !== 'string') {
|
||||
return element;
|
||||
}
|
||||
|
||||
// Highlight suspect and banned phrase inside this part of text.
|
||||
return markPhrases(element, suspectWords, bannedWords, index);
|
||||
});
|
||||
return (
|
||||
<div {...rest}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
.moreDetail {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: black;
|
||||
right: 16px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {Component} from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './CommentDetails.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -7,25 +7,26 @@ import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
|
||||
|
||||
class CommentDetails extends Component {
|
||||
state = {
|
||||
showDetail: false
|
||||
showDetail: false,
|
||||
};
|
||||
|
||||
constructor () {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
showDetail: false
|
||||
showDetail: false,
|
||||
};
|
||||
}
|
||||
|
||||
toggleDetail = () => {
|
||||
this.setState((state) => ({
|
||||
showDetail: !state.showDetail
|
||||
this.setState(state => ({
|
||||
showDetail: !state.showDetail,
|
||||
}));
|
||||
}
|
||||
this.props.clearHeightCache && this.props.clearHeightCache();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {data, root, comment} = this.props;
|
||||
const {showDetail} = this.state;
|
||||
const { data, root, comment, clearHeightCache } = this.props;
|
||||
const { showDetail } = this.state;
|
||||
const queryData = {
|
||||
root,
|
||||
comment,
|
||||
@@ -44,14 +45,18 @@ class CommentDetails extends Component {
|
||||
<Slot
|
||||
fill="adminCommentDetailArea"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
more={showDetail}
|
||||
/>
|
||||
{showDetail && <Slot
|
||||
fill="adminCommentMoreDetails"
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
/>}
|
||||
{showDetail && (
|
||||
<Slot
|
||||
fill="adminCommentMoreDetails"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -61,6 +66,7 @@ CommentDetails.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object.isRequired,
|
||||
clearHeightCache: PropTypes.func,
|
||||
};
|
||||
|
||||
export default CommentDetails;
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { matchLinks } from '../utils';
|
||||
import memoize from 'lodash/memoize';
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
// generate a regulare expression that catches the `phrases`.
|
||||
function generateRegExp(phrases) {
|
||||
const inner = phrases
|
||||
.map(phrase =>
|
||||
phrase
|
||||
.split(/\s+/)
|
||||
.map(word => escapeRegExp(word))
|
||||
.join('[\\s"?!.]+')
|
||||
)
|
||||
.join('|');
|
||||
|
||||
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
|
||||
try {
|
||||
return new RegExp(pattern, 'iu');
|
||||
} catch (_err) {
|
||||
// IE does not support unicode support, so we'll create one without.
|
||||
return new RegExp(pattern, 'i');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
|
||||
function getPhrasesRegexp(suspectWords, bannedWords) {
|
||||
return generateRegExp([...suspectWords, ...bannedWords]);
|
||||
}
|
||||
|
||||
// Memoized version as arguments rarely change.
|
||||
const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
|
||||
|
||||
// markPhrases looks for `supsectWords` and `bannedWords` inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
|
||||
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
|
||||
const tokens = body.split(regexp);
|
||||
return tokens.map(
|
||||
(token, i) =>
|
||||
i % 3 === 2 ? <mark key={`${keyPrefix}_${i}`}>{token}</mark> : token
|
||||
);
|
||||
}
|
||||
|
||||
// markLinks looks for links inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markLinks(body) {
|
||||
const matches = matchLinks(body);
|
||||
const content = [];
|
||||
let index = 0;
|
||||
if (matches) {
|
||||
matches.forEach((match, i) => {
|
||||
content.push(body.substring(index, match.index));
|
||||
content.push(<mark key={i}>{match.text}</mark>);
|
||||
index = match.lastIndex;
|
||||
});
|
||||
}
|
||||
content.push(body.substring(index));
|
||||
return content;
|
||||
}
|
||||
|
||||
const CommentFormatter = ({
|
||||
body,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
className = 'comment',
|
||||
...rest
|
||||
}) => {
|
||||
// Breaking the body by line break
|
||||
const textbreaks = body.split('\n');
|
||||
|
||||
return (
|
||||
<span className={`${className}-text`} {...rest}>
|
||||
{textbreaks.map((line, i) => {
|
||||
const content = markLinks(line).map((element, index) => {
|
||||
// Keep highlighted links.
|
||||
if (typeof element !== 'string') {
|
||||
return element;
|
||||
}
|
||||
|
||||
// Highlight suspect and banned phrase inside this part of text.
|
||||
return markPhrases(element, suspectWords, bannedWords, index);
|
||||
});
|
||||
|
||||
return (
|
||||
<span key={i} className={`${className}-line`}>
|
||||
{content}
|
||||
{i !== textbreaks.length - 1 && (
|
||||
<br className={`${className}-linebreak`} />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
CommentFormatter.propTypes = {
|
||||
className: PropTypes.string,
|
||||
bannedWords: PropTypes.array,
|
||||
suspectWords: PropTypes.array,
|
||||
body: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CommentFormatter;
|
||||
@@ -5,41 +5,72 @@ import Slot from 'coral-framework/components/Slot';
|
||||
import FlagLabel from 'coral-ui/components/FlagLabel';
|
||||
import cn from 'classnames';
|
||||
import styles from './CommentLabels.css';
|
||||
import { ADMIN, MODERATOR, STAFF } from 'coral-framework/constants/roles';
|
||||
|
||||
const staffRoles = ['ADMIN', 'STAFF', 'MODERATOR'];
|
||||
const staffRoles = [ADMIN, MODERATOR, STAFF];
|
||||
|
||||
function isUserFlagged(actions) {
|
||||
return actions.some((action) => action.__typename === 'FlagAction' && action.user);
|
||||
return actions.some(
|
||||
action => action.__typename === 'FlagAction' && action.user
|
||||
);
|
||||
}
|
||||
|
||||
function getUserFlaggedType(actions) {
|
||||
return actions
|
||||
.some((action) =>
|
||||
return actions.some(
|
||||
action =>
|
||||
action.__typename === 'FlagAction' &&
|
||||
action.user &&
|
||||
action.user.roles.some((role) => staffRoles.includes(role))
|
||||
) ? 'Staff' : 'User';
|
||||
staffRoles.includes(action.user.role)
|
||||
)
|
||||
? 'Staff'
|
||||
: 'User';
|
||||
}
|
||||
|
||||
function hasSuspectedWords(actions) {
|
||||
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'Matched suspect word filter');
|
||||
return actions.some(
|
||||
action =>
|
||||
action.__typename === 'FlagAction' && action.reason === 'SUSPECT_WORD'
|
||||
);
|
||||
}
|
||||
|
||||
function hasHistoryFlag(actions) {
|
||||
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'TRUST');
|
||||
return actions.some(
|
||||
action => action.__typename === 'FlagAction' && action.reason === 'TRUST'
|
||||
);
|
||||
}
|
||||
|
||||
const CommentLabels = ({comment, comment: {className, status, actions, hasParent}}) => {
|
||||
const CommentLabels = ({
|
||||
comment,
|
||||
comment: { className, status, actions, hasParent },
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(className, styles.root)}>
|
||||
<div className={styles.coreLabels}>
|
||||
{hasParent && <Label iconName="reply" className={styles.replyLabel}>reply</Label>}
|
||||
{status === 'PREMOD' && <Label iconName="query_builder" className={styles.premodLabel}>Pre-Mod</Label>}
|
||||
{isUserFlagged(actions) && <FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>}
|
||||
{hasSuspectedWords(actions) && <FlagLabel iconName="sms_failed">Suspect</FlagLabel>}
|
||||
{hasHistoryFlag(actions) && <FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>}
|
||||
{hasParent && (
|
||||
<Label iconName="reply" className={styles.replyLabel}>
|
||||
reply
|
||||
</Label>
|
||||
)}
|
||||
{status === 'PREMOD' && (
|
||||
<Label iconName="query_builder" className={styles.premodLabel}>
|
||||
Pre-Mod
|
||||
</Label>
|
||||
)}
|
||||
{isUserFlagged(actions) && (
|
||||
<FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>
|
||||
)}
|
||||
{hasSuspectedWords(actions) && (
|
||||
<FlagLabel iconName="sms_failed">Suspect</FlagLabel>
|
||||
)}
|
||||
{hasHistoryFlag(actions) && (
|
||||
<FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>
|
||||
)}
|
||||
</div>
|
||||
<Slot className={styles.slot} fill="adminCommentLabels" queryData={{comment}} />
|
||||
<Slot
|
||||
className={styles.slot}
|
||||
fill="adminCommentLabels"
|
||||
queryData={{ comment }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import styles from './CountBadge.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const CountBadge = ({count}) => {
|
||||
const CountBadge = ({ count }) => {
|
||||
let number = count;
|
||||
|
||||
// shorten large counts to abbreviations
|
||||
@@ -16,13 +16,11 @@ const CountBadge = ({count}) => {
|
||||
number = `${(number / 1e3).toFixed(1)}${t('modqueue.thousand')}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={styles.count}>{number}</span>
|
||||
);
|
||||
return <span className={styles.count}>{number}</span>;
|
||||
};
|
||||
|
||||
CountBadge.propTypes = {
|
||||
count: PropTypes.number.isRequired
|
||||
count: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default CountBadge;
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Navigation, Drawer} from 'react-mdl';
|
||||
import {IndexLink, Link} from 'react-router';
|
||||
import { Navigation, Drawer } from 'react-mdl';
|
||||
import { IndexLink, Link } from 'react-router';
|
||||
import styles from './Drawer.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import cn from 'classnames';
|
||||
|
||||
const CoralDrawer = ({handleLogout, auth = {}}) => (
|
||||
const CoralDrawer = ({ handleLogout, auth = {} }) => (
|
||||
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
|
||||
{ auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<IndexLink
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
</IndexLink>
|
||||
)
|
||||
}
|
||||
{can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<IndexLink
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.moderate')}
|
||||
</IndexLink>
|
||||
)}
|
||||
<Link
|
||||
className={cn('talk-admin-nav-stories', styles.navLink)}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.stories')}
|
||||
</Link>
|
||||
<Link
|
||||
className={cn('talk-admin-nav-community', styles.navLink)}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.community')}
|
||||
</Link>
|
||||
{
|
||||
can(auth.user, 'UPDATE_CONFIG') &&
|
||||
(
|
||||
<Link
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.configure')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<a onClick={handleLogout}>Sign Out</a>
|
||||
{can(auth.user, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.configure')}
|
||||
</Link>
|
||||
)}
|
||||
<a onClick={handleLogout}>{t('configure.sign_out')}</a>
|
||||
<span>{`v${process.env.VERSION}`}</span>
|
||||
</Navigation>
|
||||
</div> : null }
|
||||
</div>
|
||||
) : null}
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
CoralDrawer.propTypes = {
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
restricted: PropTypes.bool, // hide app elements from a logged out user
|
||||
auth: PropTypes.object
|
||||
auth: PropTypes.object,
|
||||
};
|
||||
|
||||
export default CoralDrawer;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Card} from 'coral-ui';
|
||||
import { Card } from 'coral-ui';
|
||||
|
||||
const EmptyCard = (props) => (
|
||||
<Card style={{textAlign: 'center', maxWidth: 400, margin: '0 auto'}}>
|
||||
const EmptyCard = props => (
|
||||
<Card style={{ textAlign: 'center', maxWidth: 400, margin: '0 auto' }}>
|
||||
{props.children}
|
||||
</Card>
|
||||
);
|
||||
|
||||
EmptyCard.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default EmptyCard;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import {Layout} from 'react-mdl';
|
||||
import { Layout } from 'react-mdl';
|
||||
import styles from './FullLoading.css';
|
||||
import {CoralLogo} from 'coral-ui';
|
||||
import { CoralLogo } from 'coral-ui';
|
||||
|
||||
export const FullLoading = () => (
|
||||
<Layout fixedDrawer>
|
||||
<div className={styles.layout} >
|
||||
<div className={styles.layout}>
|
||||
<h1>Loading</h1>
|
||||
<CoralLogo />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Navigation, Header, IconButton, MenuItem, Menu } from 'react-mdl';
|
||||
import { Link, IndexLink } from 'react-router';
|
||||
import styles from './Header.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { Logo } from './Logo';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
|
||||
import CommunityIndicator from '../routes/Community/containers/Indicator';
|
||||
|
||||
const CoralHeader = ({
|
||||
handleLogout,
|
||||
showShortcuts = () => {},
|
||||
auth,
|
||||
root,
|
||||
data,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.headerWrapper}>
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
|
||||
<Navigation className={styles.nav}>
|
||||
{can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<IndexLink
|
||||
id="moderateNav"
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.moderate')}
|
||||
<ModerationIndicator root={root} data={data} />
|
||||
</IndexLink>
|
||||
)}
|
||||
<Link
|
||||
id="storiesNav"
|
||||
className={cn('talk-admin-nav-stories', styles.navLink)}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.stories')}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
id="communityNav"
|
||||
className={cn('talk-admin-nav-community', styles.navLink)}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.community')}
|
||||
<CommunityIndicator root={root} data={data} />
|
||||
</Link>
|
||||
|
||||
{can(auth.user, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
id="configureNav"
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.configure')}
|
||||
</Link>
|
||||
)}
|
||||
</Navigation>
|
||||
) : null}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={cn(styles.settings, 'talk-admin-header-settings')}>
|
||||
<div>
|
||||
<IconButton
|
||||
name="settings"
|
||||
id="menu-settings"
|
||||
className="talk-admin-header-settings-button"
|
||||
/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={() => showShortcuts(true)}>
|
||||
{t('configure.shortcuts')}
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a
|
||||
href="https://github.com/coralproject/talk/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View latest version
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a
|
||||
href="https://support.coralproject.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleLogout}
|
||||
className="talk-admin-header-sign-out"
|
||||
>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>{`v${process.env.VERSION}`}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Header>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CoralHeader.propTypes = {
|
||||
auth: PropTypes.object,
|
||||
showShortcuts: PropTypes.func,
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default CoralHeader;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import {matchLinks} from '../utils';
|
||||
import { matchLinks } from '../utils';
|
||||
|
||||
export default ({text, children}) => {
|
||||
export default ({ text, children }) => {
|
||||
const hasLinks = !!matchLinks(text);
|
||||
|
||||
if (!hasLinks) {
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import styles from './Indicator.css';
|
||||
|
||||
const Indicator = () =>
|
||||
<span className={styles.indicator}></span>;
|
||||
const Indicator = () => <span className={styles.indicator} />;
|
||||
|
||||
export default Indicator;
|
||||
+6
-12
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Layout as LayoutMDL} from 'react-mdl';
|
||||
import Header from '../../containers/Header';
|
||||
import Drawer from '../Drawer';
|
||||
import { Layout as LayoutMDL } from 'react-mdl';
|
||||
import Header from '../containers/Header';
|
||||
import Drawer from './Drawer';
|
||||
import styles from './Layout.css';
|
||||
|
||||
const Layout = ({
|
||||
@@ -18,14 +18,8 @@ const Layout = ({
|
||||
showShortcuts={toggleShortcutModal}
|
||||
auth={auth}
|
||||
/>
|
||||
<Drawer
|
||||
handleLogout={handleLogout}
|
||||
restricted={restricted}
|
||||
auth={auth}
|
||||
/>
|
||||
<div className={styles.layout}>
|
||||
{children}
|
||||
</div>
|
||||
<Drawer handleLogout={handleLogout} restricted={restricted} auth={auth} />
|
||||
<div className={styles.layout}>{children}</div>
|
||||
</LayoutMDL>
|
||||
);
|
||||
|
||||
@@ -34,7 +28,7 @@ Layout.propTypes = {
|
||||
auth: PropTypes.object,
|
||||
handleLogout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
restricted: PropTypes.bool // hide elements from a user that's logged out
|
||||
restricted: PropTypes.bool, // hide elements from a user that's logged out
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -1,23 +1,23 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button} from 'coral-ui';
|
||||
import { Button } from 'coral-ui';
|
||||
import styles from './LoadMore.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
const LoadMore = ({loadMore, showLoadMore, className, ...rest}) =>
|
||||
const LoadMore = ({ loadMore, showLoadMore, className = '', ...rest }) => (
|
||||
<div {...rest} className={cn(className, styles.loadMoreContainer)}>
|
||||
{
|
||||
showLoadMore && <Button
|
||||
className={styles.loadMore}
|
||||
onClick={loadMore}>
|
||||
{showLoadMore && (
|
||||
<Button className={styles.loadMore} onClick={loadMore}>
|
||||
Load More
|
||||
</Button>
|
||||
}
|
||||
</div>;
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
LoadMore.propTypes = {
|
||||
className: PropTypes.string,
|
||||
loadMore: PropTypes.func.isRequired,
|
||||
showLoadMore: PropTypes.bool.isRequired
|
||||
showLoadMore: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default LoadMore;
|
||||
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import styles from './Logo.css';
|
||||
import {CoralLogo} from 'coral-ui';
|
||||
import { CoralLogo } from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const Logo = ({className = ''}) => (
|
||||
export const Logo = ({ className = '' }) => (
|
||||
<div className={`${styles.logo} ${className}`}>
|
||||
<h1>
|
||||
<CoralLogo className={styles.base} />
|
||||
@@ -13,5 +13,5 @@ export const Logo = ({className = ''}) => (
|
||||
);
|
||||
|
||||
Logo.propTypes = {
|
||||
className: PropTypes.string
|
||||
className: PropTypes.string,
|
||||
};
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import {Button, Icon} from 'react-mdl';
|
||||
import { Button, Icon } from 'react-mdl';
|
||||
import styles from './Modal.css';
|
||||
|
||||
export default ({open, children, onClose}) => (
|
||||
export default ({ open, children, onClose }) => (
|
||||
<div className={`${styles.container} ${!open ? styles.hide : ''}`}>
|
||||
<div className={styles.inner}>
|
||||
<Button className={styles.close} onClick={onClose}><Icon name='close' /></Button>
|
||||
<Button className={styles.close} onClick={onClose}>
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15);
|
||||
z-index: 10;
|
||||
z-index: 4;
|
||||
|
||||
.ctaHeader {
|
||||
font-size: 16px;
|
||||
|
||||
@@ -5,52 +5,75 @@ import styles from './ModerationKeysModal.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export default class ModerationKeysModal extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
hideShortcutsNote: PropTypes.func.isRequired,
|
||||
shortcutsNoteVisible: PropTypes.string.isRequired,
|
||||
queueCount: PropTypes.number.isRequired
|
||||
}
|
||||
queueCount: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
buildShortcuts = () => {
|
||||
return [
|
||||
{
|
||||
title: 'modqueue.navigation',
|
||||
shortcuts: {
|
||||
'j': 'modqueue.next_comment',
|
||||
'k': 'modqueue.prev_comment',
|
||||
j: 'modqueue.next_comment',
|
||||
k: 'modqueue.prev_comment',
|
||||
'ctrl+f': 'modqueue.toggle_search',
|
||||
't': 'modqueue.next_queue',
|
||||
t: 'modqueue.next_queue',
|
||||
[`1...${this.props.queueCount}`]: 'modqueue.jump_to_queue',
|
||||
's': 'modqueue.singleview',
|
||||
'?': 'modqueue.thismenu'
|
||||
}
|
||||
z: 'modqueue.singleview',
|
||||
'?': 'modqueue.thismenu',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'modqueue.actions',
|
||||
shortcuts: {
|
||||
'd': 'modqueue.approve',
|
||||
'f': 'modqueue.reject'
|
||||
}
|
||||
}
|
||||
d: 'modqueue.approve',
|
||||
f: 'modqueue.reject',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const {open, onClose, hideShortcutsNote, shortcutsNoteVisible} = this.props;
|
||||
render() {
|
||||
const {
|
||||
open,
|
||||
onClose,
|
||||
hideShortcutsNote,
|
||||
shortcutsNoteVisible,
|
||||
} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.callToAction} style={{display: shortcutsNoteVisible === 'show' ? 'block' : 'none'}}>
|
||||
<div onClick={hideShortcutsNote} className={styles.closeButton}>×</div>
|
||||
<div
|
||||
className={styles.callToAction}
|
||||
style={{
|
||||
display: shortcutsNoteVisible === 'show' ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
<div onClick={hideShortcutsNote} className={styles.closeButton}>
|
||||
×
|
||||
</div>
|
||||
<p className={styles.ctaHeader}>{t('modqueue.mod_faster')}</p>
|
||||
<p><strong>{t('modqueue.try_these')}:</strong></p>
|
||||
<p>
|
||||
<strong>{t('modqueue.try_these')}:</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li><span>{t('modqueue.approve')}</span> <span className={styles.smallKey}>d</span></li>
|
||||
<li><span>{t('modqueue.reject')}</span> <span className={styles.smallKey}>f</span></li>
|
||||
<li>
|
||||
<span>{t('modqueue.approve')}</span>{' '}
|
||||
<span className={styles.smallKey}>d</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>{t('modqueue.reject')}</span>{' '}
|
||||
<span className={styles.smallKey}>f</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p><span>{t('modqueue.view_more_shortcuts')}</span> <span className={styles.smallKey}>{t('modqueue.shift_key')}</span> + <span className={styles.smallKey}>/</span></p>
|
||||
<p>
|
||||
<span>{t('modqueue.view_more_shortcuts')}</span>{' '}
|
||||
<span className={styles.smallKey}>{t('modqueue.shift_key')}</span> +{' '}
|
||||
<span className={styles.smallKey}>/</span>
|
||||
</p>
|
||||
</div>
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<h3>{t('modqueue.shortcuts')}</h3>
|
||||
@@ -63,9 +86,11 @@ export default class ModerationKeysModal extends React.Component {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(shortcut.shortcuts).map((key) => (
|
||||
{Object.keys(shortcut.shortcuts).map(key => (
|
||||
<tr key={`${key}tr`}>
|
||||
<td className={styles.shortcut}><span className={styles.key}>{key}</span></td>
|
||||
<td className={styles.shortcut}>
|
||||
<span className={styles.key}>{key}</span>
|
||||
</td>
|
||||
<td>{t(shortcut.shortcuts[key])}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import React from 'react';
|
||||
import {Layout} from 'react-mdl';
|
||||
import { Layout } from 'react-mdl';
|
||||
import styles from './NotFound.css';
|
||||
|
||||
export const NotFound = () => (
|
||||
<Layout fixedDrawer>
|
||||
<div className={styles.layout} >
|
||||
<div className={styles.layout}>
|
||||
<h1>Page Not Found</h1>
|
||||
<p>The communicorn feels your pain.</p>
|
||||
<img src="https://coralproject.net/images/communicorn.jpg" alt="Communicorn"/>
|
||||
<img
|
||||
src="https://coralproject.net/images/communicorn.jpg"
|
||||
alt="Communicorn"
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -5,23 +5,22 @@
|
||||
background: white;
|
||||
padding: 10px 11px;
|
||||
box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
line-height: 24px;
|
||||
font-size: 17px;
|
||||
height: 47px;
|
||||
border-radius: 3px;
|
||||
text-transform: capitalize;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
|
||||
width: 129px;
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
margin: 0 0 .5em;
|
||||
|
||||
&:hover {
|
||||
&:not(:disabled):hover {
|
||||
color: white;
|
||||
background-color: #D03235;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
|
||||
@@ -3,16 +3,21 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import cn from 'classnames';
|
||||
import styles from './RejectButton.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
import { Icon } from 'coral-ui';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const RejectButton = ({active, minimal, onClick, className}) => {
|
||||
const RejectButton = ({ active, minimal, onClick, className, disabled }) => {
|
||||
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
|
||||
return (
|
||||
<button
|
||||
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
|
||||
className={cn(
|
||||
styles.root,
|
||||
{ [styles.minimal]: minimal, [styles.active]: active },
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled || active}
|
||||
>
|
||||
<Icon name={'close'} className={styles.icon} />
|
||||
{!minimal && text}
|
||||
@@ -25,7 +30,7 @@ RejectButton.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default RejectButton;
|
||||
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import {RadioGroup, Radio} from 'react-mdl';
|
||||
import { Dialog } from 'coral-ui';
|
||||
import { RadioGroup, Radio } from 'react-mdl';
|
||||
import styles from './SuspendUserDialog.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
import Button from 'coral-ui/components/Button';
|
||||
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
import {dateAdd} from 'coral-framework/utils';
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
import { dateAdd } from 'coral-framework/utils';
|
||||
|
||||
const initialState = {step: 0, duration: '3'};
|
||||
const initialState = { step: 0, duration: '3' };
|
||||
|
||||
function durationsToDate(hours) {
|
||||
|
||||
// Add 1 minute more to help `timeago.js` to display the correct duration.
|
||||
return dateAdd(new Date(), 'minute', hours * 60 + 1);
|
||||
}
|
||||
|
||||
class SuspendUserDialog extends React.Component {
|
||||
|
||||
state = initialState;
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
@@ -28,13 +26,13 @@ class SuspendUserDialog extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleDurationChange = (event) => {
|
||||
this.setState({duration: event.target.value});
|
||||
}
|
||||
handleDurationChange = event => {
|
||||
this.setState({ duration: event.target.value });
|
||||
};
|
||||
|
||||
handleMessageChange = (event) => {
|
||||
this.setState({message: event.target.value});
|
||||
}
|
||||
handleMessageChange = event => {
|
||||
this.setState({ message: event.target.value });
|
||||
};
|
||||
|
||||
goToStep1 = () => {
|
||||
this.setState({
|
||||
@@ -43,13 +41,12 @@ class SuspendUserDialog extends React.Component {
|
||||
'suspenduser.email_message_suspend',
|
||||
this.props.username,
|
||||
this.props.organizationName,
|
||||
timeago(durationsToDate(this.state.duration)),
|
||||
timeago(durationsToDate(this.state.duration))
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handlePerform = () => {
|
||||
|
||||
this.props.onPerform({
|
||||
message: this.state.message,
|
||||
|
||||
@@ -59,36 +56,49 @@ class SuspendUserDialog extends React.Component {
|
||||
};
|
||||
|
||||
renderStep0() {
|
||||
const {onCancel, username} = this.props;
|
||||
const {duration} = this.state;
|
||||
const { onCancel, username } = this.props;
|
||||
const { duration } = this.state;
|
||||
return (
|
||||
<section>
|
||||
<h1 className={styles.header}>
|
||||
{t('suspenduser.title_suspend')}
|
||||
</h1>
|
||||
<section className="talk-admin-suspend-user-dialog-step-0">
|
||||
<h1 className={styles.header}>{t('suspenduser.title_suspend')}</h1>
|
||||
<p className={styles.description}>
|
||||
{t('suspenduser.description_suspend', username)}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>{t('suspenduser.select_duration')}</legend>
|
||||
<legend className={styles.legend}>
|
||||
{t('suspenduser.select_duration')}
|
||||
</legend>
|
||||
<RadioGroup
|
||||
name='status filter'
|
||||
name="status filter"
|
||||
value={duration}
|
||||
childContainer='div'
|
||||
childContainer="div"
|
||||
onChange={this.handleDurationChange}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value='1'>{t('suspenduser.one_hour')}</Radio>
|
||||
<Radio value='3'>{t('suspenduser.hours', 3)}</Radio>
|
||||
<Radio value='24'>{t('suspenduser.hours', 24)}</Radio>
|
||||
<Radio value='168'>{t('suspenduser.days', 7)}</Radio>
|
||||
<Radio value="1">{t('suspenduser.one_hour')}</Radio>
|
||||
<Radio value="3">{t('suspenduser.hours', 3)}</Radio>
|
||||
<Radio value="24">{t('suspenduser.hours', 24)}</Radio>
|
||||
<Radio value="168">{t('suspenduser.days', 7)}</Radio>
|
||||
</RadioGroup>
|
||||
</fieldset>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
|
||||
<Button
|
||||
cStyle="white"
|
||||
className={styles.cancel}
|
||||
onClick={onCancel}
|
||||
raised
|
||||
>
|
||||
{t('suspenduser.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" className={cn(styles.perform, 'talk-admin-suspend-user-dialog-confirm')} onClick={this.goToStep1} raised>
|
||||
<Button
|
||||
cStyle="black"
|
||||
className={cn(
|
||||
styles.perform,
|
||||
'talk-admin-suspend-user-dialog-confirm'
|
||||
)}
|
||||
onClick={this.goToStep1}
|
||||
raised
|
||||
>
|
||||
{t('suspenduser.suspend_user')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -97,31 +107,40 @@ class SuspendUserDialog extends React.Component {
|
||||
}
|
||||
|
||||
renderStep1() {
|
||||
const {onCancel, username} = this.props;
|
||||
const {message} = this.state;
|
||||
const { message } = this.state;
|
||||
const { onCancel, username } = this.props;
|
||||
return (
|
||||
<section>
|
||||
<h1 className={styles.header}>
|
||||
{t('suspenduser.title_notify')}
|
||||
</h1>
|
||||
<section className="talk-admin-suspend-user-dialog-step-1">
|
||||
<h1 className={styles.header}>{t('suspenduser.title_notify')}</h1>
|
||||
<p className={styles.description}>
|
||||
{t('suspenduser.description_notify', username)}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>{t('suspenduser.write_message')}</legend>
|
||||
<legend className={styles.legend}>
|
||||
{t('suspenduser.write_message')}
|
||||
</legend>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={styles.messageInput}
|
||||
value={message}
|
||||
onChange={this.handleMessageChange} />
|
||||
onChange={this.handleMessageChange}
|
||||
/>
|
||||
</fieldset>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
|
||||
<Button
|
||||
cStyle="white"
|
||||
className={styles.cancel}
|
||||
onClick={onCancel}
|
||||
raised
|
||||
>
|
||||
{t('suspenduser.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
cStyle="black"
|
||||
className={cn(styles.perform, 'talk-admin-suspend-user-dialog-send')}
|
||||
className={cn(
|
||||
styles.perform,
|
||||
'talk-admin-suspend-user-dialog-send'
|
||||
)}
|
||||
onClick={this.handlePerform}
|
||||
disabled={this.state.message.length === 0}
|
||||
raised
|
||||
@@ -134,8 +153,8 @@ class SuspendUserDialog extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {open, onCancel} = this.props;
|
||||
const {step} = this.state;
|
||||
const { open, onCancel } = this.props;
|
||||
const { step } = this.state;
|
||||
return (
|
||||
<Dialog
|
||||
className={cn(styles.dialog, 'talk-admin-suspend-user-dialog')}
|
||||
@@ -143,7 +162,13 @@ class SuspendUserDialog extends React.Component {
|
||||
open={open}
|
||||
>
|
||||
<div className={styles.close}>
|
||||
<button aria-label="Close" onClick={onCancel} className={styles.closeButton}>×</button>
|
||||
<button
|
||||
aria-label="Close"
|
||||
onClick={onCancel}
|
||||
className={styles.closeButton}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{step === 0 && this.renderStep0()}
|
||||
{step === 1 && this.renderStep1()}
|
||||
|
||||
@@ -5,8 +5,9 @@ import AutosizeInput from 'react-input-autosize';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
|
||||
const autosizingRenderInput = ({onChange, value, addTag: _, ...other}) =>
|
||||
<AutosizeInput type='text' onChange={onChange} value={value} {...other} />;
|
||||
const autosizingRenderInput = ({ onChange, value, addTag: _, ...other }) => (
|
||||
<AutosizeInput type="text" onChange={onChange} value={value} {...other} />
|
||||
);
|
||||
|
||||
autosizingRenderInput.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
@@ -14,17 +15,16 @@ autosizingRenderInput.propTypes = {
|
||||
addTag: PropTypes.func,
|
||||
};
|
||||
|
||||
const TagsInputComponent = ({className = '', ...props}) => {
|
||||
const TagsInputComponent = ({ className = '', ...props }) => {
|
||||
return (
|
||||
<TagsInput
|
||||
addOnBlur={true}
|
||||
addOnPaste={true}
|
||||
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
|
||||
pasteSplit={data => data.split(',').map(d => d.trim())}
|
||||
className={cn(styles.root, 'tags-input', className)}
|
||||
focusedClassName={styles.rootFocus}
|
||||
renderInput={autosizingRenderInput}
|
||||
{...props}
|
||||
|
||||
tagProps={{
|
||||
className: styles.tag,
|
||||
classNameRemove: styles.tagRemove,
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
|
||||
.toastify__body {
|
||||
color: white;
|
||||
overflow-x: scroll;
|
||||
overflow-x: auto;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import './ToastContainer.css';
|
||||
import {defaultProps} from 'recompose';
|
||||
import {ToastContainer} from 'react-toastify';
|
||||
import { defaultProps } from 'recompose';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
|
||||
export default defaultProps({
|
||||
autoClose: 5000,
|
||||
|
||||
@@ -94,73 +94,52 @@
|
||||
width: calc(100% - 90px);
|
||||
}
|
||||
|
||||
.commentStatuses {
|
||||
.tabBar {
|
||||
padding: 0 0 0 10px;
|
||||
margin: 0;
|
||||
align-self: center;
|
||||
list-style: none;
|
||||
box-sizing: border-box;
|
||||
li {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
padding: 0 10px;
|
||||
}
|
||||
border: none;
|
||||
}
|
||||
|
||||
.active {
|
||||
.tab {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.tabButton {
|
||||
border: none;
|
||||
background: white;
|
||||
border-radius: 0;
|
||||
font-size: 1em;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tabButtonActive {
|
||||
font-weight: bold;
|
||||
border-bottom: 3px solid #F36451;
|
||||
}
|
||||
|
||||
.bulkActionGroup {
|
||||
height: 52px;
|
||||
background-color: #efefef;
|
||||
padding: 0 0 0 10px;
|
||||
display: flex;
|
||||
i {
|
||||
margin-right: 0;
|
||||
}
|
||||
.bulkAction {
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
transform: scale(.7);
|
||||
min-width: 0;
|
||||
}
|
||||
.bulkAction:last-child {
|
||||
margin-left: -10px;
|
||||
}
|
||||
.username {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.selectedCommentsInfo {
|
||||
align-self: center;
|
||||
font-weight: 500;
|
||||
margin-left: 15px;
|
||||
.actionsMenu {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.loadMore>button {
|
||||
background-color: #696969;
|
||||
&:hover {
|
||||
background-color: #404040;
|
||||
color: white;
|
||||
}
|
||||
.actionsMenuSuspended {
|
||||
background-color: #F29336;
|
||||
border-color: #F29336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toggleAll {
|
||||
padding: 0 10px 0 0;
|
||||
align-self: center;
|
||||
.actionsMenuBanned {
|
||||
background-color: #E45241;
|
||||
border-color: #E45241;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.commentList {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.bulkActionHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 52px;
|
||||
&.selected {
|
||||
background-color: #efefef;
|
||||
}
|
||||
}
|
||||
@@ -1,94 +1,68 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import Comment from '../containers/UserDetailComment';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import styles from './UserDetail.css';
|
||||
import {Icon, Drawer, Spinner} from 'coral-ui';
|
||||
import {Slot} from 'coral-framework/components';
|
||||
import AccountHistory from './AccountHistory';
|
||||
import { Slot } from 'coral-framework/components';
|
||||
import UserDetailCommentList from '../components/UserDetailCommentList';
|
||||
import {
|
||||
getReliability,
|
||||
isSuspended,
|
||||
isBanned,
|
||||
} from 'coral-framework/utils/user';
|
||||
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import LoadMore from '../components/LoadMore';
|
||||
import cn from 'classnames';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import {getReliability} from 'coral-framework/utils/user';
|
||||
import ApproveButton from './ApproveButton';
|
||||
import RejectButton from './RejectButton';
|
||||
import {getErrorMessages} from 'coral-framework/utils';
|
||||
import {
|
||||
Icon,
|
||||
Drawer,
|
||||
Spinner,
|
||||
TabBar,
|
||||
Tab,
|
||||
TabContent,
|
||||
TabPane,
|
||||
} from 'coral-ui';
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import UserInfoTooltip from './UserInfoTooltip';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export default class UserDetail extends React.Component {
|
||||
class UserDetail extends React.Component {
|
||||
rejectThenReload = async info => {
|
||||
await this.props.rejectComment(info);
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
hideUserDetail: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
changeStatus: PropTypes.func.isRequired,
|
||||
toggleSelect: PropTypes.func.isRequired,
|
||||
bulkAccept: PropTypes.func.isRequired,
|
||||
bulkReject: PropTypes.func.isRequired,
|
||||
toggleSelectAll: PropTypes.func.isRequired,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
data: PropTypes.shape({
|
||||
refetch: PropTypes.func.isRequired,
|
||||
}),
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
selectedCommentIds: PropTypes.array.isRequired,
|
||||
viewUserDetail: PropTypes.any.isRequired,
|
||||
loadMore: PropTypes.any.isRequired,
|
||||
notify: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
rejectThenReload = async (info) => {
|
||||
try {
|
||||
await this.props.rejectComment(info);
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
}
|
||||
|
||||
acceptThenReload = async (info) => {
|
||||
try {
|
||||
await this.props.acceptComment(info);
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
}
|
||||
acceptThenReload = async info => {
|
||||
await this.props.acceptComment(info);
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
bulkAcceptThenReload = async () => {
|
||||
try {
|
||||
await this.props.bulkAccept();
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
}
|
||||
await this.props.bulkAccept();
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
bulkRejectThenReload = async () => {
|
||||
try {
|
||||
await this.props.bulkReject();
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
await this.props.bulkReject();
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
}
|
||||
changeTab = tab => {
|
||||
this.props.changeTab(tab);
|
||||
};
|
||||
|
||||
showAll = () => {
|
||||
this.props.changeStatus('all');
|
||||
}
|
||||
showSuspenUserDialog = () =>
|
||||
this.props.showSuspendUserDialog({
|
||||
userId: this.props.root.user.id,
|
||||
username: this.props.root.user.username,
|
||||
});
|
||||
|
||||
showRejected = () => {
|
||||
this.props.changeStatus('rejected');
|
||||
}
|
||||
showBanUserDialog = () =>
|
||||
this.props.showBanUserDialog({
|
||||
userId: this.props.root.user.id,
|
||||
username: this.props.root.user.username,
|
||||
});
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
@@ -100,68 +74,170 @@ export default class UserDetail extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderError() {
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.props.hideUserDetail}>
|
||||
<Drawer onClose={this.props.hideUserDetail}>
|
||||
<div>{this.props.data.error.message}</div>
|
||||
</Drawer>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
|
||||
getActionMenuLabel() {
|
||||
const { root: { user } } = this.props;
|
||||
|
||||
if (isBanned(user)) {
|
||||
return 'Banned';
|
||||
} else if (isSuspended(user)) {
|
||||
return 'Suspended';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
renderLoaded() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
root: {
|
||||
user,
|
||||
totalComments,
|
||||
rejectedComments,
|
||||
comments: {nodes, hasNextPage}
|
||||
},
|
||||
root: { me, user, totalComments, rejectedComments },
|
||||
activeTab,
|
||||
selectedCommentIds,
|
||||
toggleSelect,
|
||||
hideUserDetail,
|
||||
viewUserDetail,
|
||||
loadMore,
|
||||
toggleSelectAll
|
||||
toggleSelectAll,
|
||||
unbanUser,
|
||||
unsuspendUser,
|
||||
modal,
|
||||
} = this.props;
|
||||
|
||||
let rejectedPercent = (rejectedComments / totalComments) * 100;
|
||||
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
|
||||
// if totalComments is 0, you're dividing by zero
|
||||
let rejectedPercent = rejectedComments / totalComments * 100;
|
||||
|
||||
// if totalComments is 0, you're dividing by zero, which is naughty
|
||||
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
|
||||
rejectedPercent = 0;
|
||||
}
|
||||
|
||||
const banned = isBanned(user);
|
||||
const suspended = isSuspended(user);
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={hideUserDetail}>
|
||||
<Drawer onClose={hideUserDetail}>
|
||||
<h3>{user.username}</h3>
|
||||
<ClickOutside onClickOutside={modal ? null : hideUserDetail}>
|
||||
<Drawer
|
||||
className="talk-admin-user-detail-drawer"
|
||||
onClose={hideUserDetail}
|
||||
>
|
||||
<h3
|
||||
className={cn(styles.username, 'talk-admin-user-detail-username')}
|
||||
>
|
||||
{user.username}
|
||||
</h3>
|
||||
|
||||
{user.id && (
|
||||
<ActionsMenu
|
||||
icon="person"
|
||||
className={cn(
|
||||
styles.actionsMenu,
|
||||
'talk-admin-user-detail-actions-menu'
|
||||
)}
|
||||
buttonClassNames={cn(
|
||||
{
|
||||
[styles.actionsMenuSuspended]: suspended,
|
||||
[styles.actionsMenuBanned]: banned,
|
||||
},
|
||||
'talk-admin-user-detail-actions-button'
|
||||
)}
|
||||
label={this.getActionMenuLabel()}
|
||||
>
|
||||
{suspended ? (
|
||||
<ActionsMenuItem onClick={() => unsuspendUser({ id: user.id })}>
|
||||
{t('user_detail.remove_suspension')}
|
||||
</ActionsMenuItem>
|
||||
) : (
|
||||
<ActionsMenuItem
|
||||
disabled={me.id === user.id}
|
||||
onClick={this.showSuspenUserDialog}
|
||||
>
|
||||
{t('user_detail.suspend')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
|
||||
{banned ? (
|
||||
<ActionsMenuItem onClick={() => unbanUser({ id: user.id })}>
|
||||
{t('user_detail.remove_ban')}
|
||||
</ActionsMenuItem>
|
||||
) : (
|
||||
<ActionsMenuItem
|
||||
disabled={me.id === user.id}
|
||||
onClick={this.showBanUserDialog}
|
||||
>
|
||||
{t('user_detail.ban')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
</ActionsMenu>
|
||||
)}
|
||||
|
||||
{(banned || suspended) && (
|
||||
<UserInfoTooltip
|
||||
user={user}
|
||||
banned={banned}
|
||||
suspended={suspended}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<ul className={styles.userDetailList}>
|
||||
<li>
|
||||
<Icon name="assignment_ind" />
|
||||
<span className={styles.userDetailItem}>Member Since:</span>
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.member_since')}:
|
||||
</span>
|
||||
{new Date(user.created_at).toLocaleString()}
|
||||
</li>
|
||||
|
||||
{user.profiles.map(({id}) =>
|
||||
{user.profiles.map(({ id }) => (
|
||||
<li key={id}>
|
||||
<Icon name="email" />
|
||||
<span className={styles.userDetailItem}>Email:</span>
|
||||
{id} <ButtonCopyToClipboard className={styles.copyButton} icon="content_copy" copyText={id} />
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.email')}:
|
||||
</span>
|
||||
{id}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
icon="content_copy"
|
||||
copyText={id}
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<ul className={styles.stats}>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}>Total Comments</span>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.total_comments')}
|
||||
</span>
|
||||
<span className={styles.statResult}>{totalComments}</span>
|
||||
</li>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}>Reject Rate</span>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.reject_rate')}
|
||||
</span>
|
||||
<span className={styles.statResult}>
|
||||
{rejectedPercent.toFixed(1)}%
|
||||
</span>
|
||||
</li>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}>Reports</span>
|
||||
<span className={cn(styles.statReportResult, styles[getReliability(user.reliable.flagger)])}>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.reports')}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
styles.statReportResult,
|
||||
styles[getReliability(user.reliable.flagger)]
|
||||
)}
|
||||
>
|
||||
{capitalize(getReliability(user.reliable.flagger))}
|
||||
</span>
|
||||
</li>
|
||||
@@ -171,76 +247,139 @@ export default class UserDetail extends React.Component {
|
||||
<Slot
|
||||
fill="userProfile"
|
||||
data={this.props.data}
|
||||
queryData={{root, user}}
|
||||
queryData={{ root, user }}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
<div className={(selectedCommentIds.length > 0) ? cn(styles.bulkActionHeader, styles.selected) : styles.bulkActionHeader}>
|
||||
{
|
||||
selectedCommentIds.length === 0
|
||||
? (
|
||||
<ul className={styles.commentStatuses}>
|
||||
<li className={activeTab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
|
||||
<li className={activeTab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
|
||||
</ul>
|
||||
)
|
||||
: (
|
||||
<div className={styles.bulkActionGroup}>
|
||||
<ApproveButton
|
||||
onClick={this.bulkAcceptThenReload}
|
||||
minimal
|
||||
/>
|
||||
<RejectButton
|
||||
onClick={this.bulkRejectThenReload}
|
||||
minimal
|
||||
/>
|
||||
<span className={styles.selectedCommentsInfo}> {selectedCommentIds.length} comments selected</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={styles.toggleAll}>
|
||||
<input
|
||||
type='checkbox'
|
||||
id='toogleAll'
|
||||
checked={selectedCommentIds.length > 0 && selectedCommentIds.length === nodes.length}
|
||||
onChange={(e) => {
|
||||
toggleSelectAll(nodes.map((comment) => comment.id), e.target.checked);
|
||||
}} />
|
||||
<label htmlFor='toogleAll'>Select all</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.commentList}>
|
||||
{
|
||||
nodes.map((comment) => {
|
||||
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
|
||||
return <Comment
|
||||
key={comment.id}
|
||||
user={user}
|
||||
root={root}
|
||||
data={data}
|
||||
comment={comment}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
selected={selected}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<LoadMore
|
||||
className={styles.loadMore}
|
||||
loadMore={loadMore}
|
||||
showLoadMore={hasNextPage}
|
||||
/>
|
||||
|
||||
<TabBar
|
||||
onTabClick={this.changeTab}
|
||||
activeTab={activeTab}
|
||||
className={cn(styles.tabBar, 'talk-admin-user-detail-tab-bar')}
|
||||
aria-controls="talk-admin-user-detail-content"
|
||||
tabClassNames={{
|
||||
button: styles.tabButton,
|
||||
buttonActive: styles.tabButtonActive,
|
||||
}}
|
||||
>
|
||||
<Tab
|
||||
tabId={'all'}
|
||||
className={cn(
|
||||
styles.tab,
|
||||
styles.button,
|
||||
'talk-admin-user-detail-all-tab'
|
||||
)}
|
||||
>
|
||||
{t('user_detail.all')}
|
||||
</Tab>
|
||||
<Tab
|
||||
tabId={'rejected'}
|
||||
className={cn(styles.tab, 'talk-admin-user-detail-rejected-tab')}
|
||||
>
|
||||
{t('user_detail.rejected')}
|
||||
</Tab>
|
||||
<Tab
|
||||
tabId={'history'}
|
||||
className={cn(
|
||||
styles.tab,
|
||||
styles.button,
|
||||
'talk-admin-user-detail-history-tab'
|
||||
)}
|
||||
>
|
||||
{t('user_detail.account_history')}
|
||||
</Tab>
|
||||
</TabBar>
|
||||
|
||||
<TabContent
|
||||
activeTab={activeTab}
|
||||
className="talk-admin-user-detail-content"
|
||||
>
|
||||
<TabPane
|
||||
tabId={'all'}
|
||||
className={'talk-admin-user-detail-all-tab-pane'}
|
||||
>
|
||||
<UserDetailCommentList
|
||||
user={user}
|
||||
root={root}
|
||||
data={data}
|
||||
loadMore={loadMore}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
selectedCommentIds={selectedCommentIds}
|
||||
toggleSelectAll={toggleSelectAll}
|
||||
bulkAcceptThenReload={this.bulkAcceptThenReload}
|
||||
bulkRejectThenReload={this.bulkRejectThenReload}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane
|
||||
tabId={'rejected'}
|
||||
className={'talk-admin-user-detail-rejected-tab-pane'}
|
||||
>
|
||||
<UserDetailCommentList
|
||||
user={user}
|
||||
root={root}
|
||||
data={data}
|
||||
loadMore={loadMore}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
selectedCommentIds={selectedCommentIds}
|
||||
toggleSelectAll={toggleSelectAll}
|
||||
bulkAcceptThenReload={this.bulkAcceptThenReload}
|
||||
bulkRejectThenReload={this.bulkRejectThenReload}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane
|
||||
tabId={'history'}
|
||||
className={'talk-admin-user-detail-history-tab-pane'}
|
||||
>
|
||||
<AccountHistory user={user} />
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</Drawer>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
return this.renderError();
|
||||
}
|
||||
|
||||
if (this.props.loading) {
|
||||
return this.renderLoading();
|
||||
}
|
||||
return this.renderLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
UserDetail.propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
hideUserDetail: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
changeTab: PropTypes.func.isRequired,
|
||||
toggleSelect: PropTypes.func.isRequired,
|
||||
bulkAccept: PropTypes.func.isRequired,
|
||||
bulkReject: PropTypes.func.isRequired,
|
||||
toggleSelectAll: PropTypes.func.isRequired,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
data: PropTypes.shape({
|
||||
refetch: PropTypes.func.isRequired,
|
||||
}),
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
selectedCommentIds: PropTypes.array.isRequired,
|
||||
viewUserDetail: PropTypes.any.isRequired,
|
||||
loadMore: PropTypes.any.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
modal: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default UserDetail;
|
||||
|
||||
@@ -81,6 +81,8 @@
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
/* IE 11 needs this... */
|
||||
width: 105px;
|
||||
}
|
||||
|
||||
.editedMarker {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import {Icon} from 'coral-ui';
|
||||
import { Icon } from 'coral-ui';
|
||||
import CommentDetails from './CommentDetails';
|
||||
import styles from './UserDetailComment.css';
|
||||
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
|
||||
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import CommentAnimatedEdit from './CommentAnimatedEdit';
|
||||
@@ -13,19 +13,18 @@ import CommentLabels from '../containers/CommentLabels';
|
||||
import ApproveButton from './ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
|
||||
class UserDetailComment extends React.Component {
|
||||
approve = () =>
|
||||
this.props.comment.status === 'ACCEPTED'
|
||||
? null
|
||||
: this.props.acceptComment({ commentId: this.props.comment.id });
|
||||
|
||||
approve = () => (this.props.comment.status === 'ACCEPTED'
|
||||
? null
|
||||
: this.props.acceptComment({commentId: this.props.comment.id})
|
||||
);
|
||||
|
||||
reject = () => (this.props.comment.status === 'REJECTED'
|
||||
? null
|
||||
: this.props.rejectComment({commentId: this.props.comment.id})
|
||||
);
|
||||
reject = () =>
|
||||
this.props.comment.status === 'REJECTED'
|
||||
? null
|
||||
: this.props.rejectComment({ commentId: this.props.comment.id });
|
||||
|
||||
render() {
|
||||
const {
|
||||
@@ -34,30 +33,35 @@ class UserDetailComment extends React.Component {
|
||||
toggleSelect,
|
||||
className,
|
||||
data,
|
||||
root: {settings: {wordlist: {banned, suspect}}},
|
||||
root: { settings: { wordlist: { banned, suspect } } },
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
|
||||
className={cn(className, styles.root, {
|
||||
[styles.rootSelected]: selected,
|
||||
})}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<input
|
||||
className={styles.bulkSelectInput}
|
||||
type='checkbox'
|
||||
type="checkbox"
|
||||
value={comment.id}
|
||||
checked={selected}
|
||||
onChange={(e) => toggleSelect(e.target.value, e.target.checked)} />
|
||||
onChange={e => toggleSelect(e.target.value, e.target.checked)}
|
||||
/>
|
||||
<span className={styles.created}>
|
||||
{timeago(comment.created_at)}
|
||||
</span>
|
||||
{
|
||||
(comment.editing && comment.editing.edited)
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
: null
|
||||
}
|
||||
{comment.editing && comment.editing.edited ? (
|
||||
<span>
|
||||
<span className={styles.editedMarker}>
|
||||
({t('comment.edited')})
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<div className={styles.labels}>
|
||||
<CommentLabels comment={comment} />
|
||||
@@ -65,17 +69,21 @@ class UserDetailComment extends React.Component {
|
||||
</div>
|
||||
<div className={styles.story}>
|
||||
Story: {comment.asset.title}
|
||||
{<Link to={`/admin/moderate/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
|
||||
{
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>
|
||||
{t('modqueue.moderate')}
|
||||
</Link>
|
||||
}
|
||||
</div>
|
||||
<CommentAnimatedEdit body={comment.body}>
|
||||
<div className={styles.bodyContainer}>
|
||||
<div className={styles.body}>
|
||||
<CommentBodyHighlighter
|
||||
<CommentFormatter
|
||||
suspectWords={suspect}
|
||||
bannedWords={banned}
|
||||
body={comment.body}
|
||||
className="talk-admin-user-detail-comment"
|
||||
/>
|
||||
{' '}
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}?commentId=${comment.id}`}
|
||||
@@ -106,17 +114,15 @@ class UserDetailComment extends React.Component {
|
||||
</div>
|
||||
</CommentAnimatedEdit>
|
||||
</div>
|
||||
<CommentDetails
|
||||
data={data}
|
||||
root={root}
|
||||
comment={comment}
|
||||
/>
|
||||
<CommentDetails data={data} root={root} comment={comment} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UserDetailComment.propTypes = {
|
||||
selected: PropTypes.bool,
|
||||
data: PropTypes.object,
|
||||
user: PropTypes.object.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
@@ -140,7 +146,7 @@ UserDetailComment.propTypes = {
|
||||
asset: PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
id: PropTypes.string
|
||||
id: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
.commentList {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.loadMore > button {
|
||||
background-color: #696969;
|
||||
&:hover {
|
||||
background-color: #404040;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.bulkActionHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 52px;
|
||||
justify-content: flex-end;
|
||||
|
||||
&.selected {
|
||||
background-color: #efefef;
|
||||
}
|
||||
}
|
||||
|
||||
.bulkActionGroup {
|
||||
height: 52px;
|
||||
background-color: #efefef;
|
||||
padding: 0 0 0 10px;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
i {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.bulkAction {
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
transform: scale(.7);
|
||||
min-width: 0;
|
||||
}
|
||||
.bulkAction:last-child {
|
||||
margin-left: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.selectedCommentsInfo {
|
||||
align-self: center;
|
||||
font-weight: 500;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.toggleAll {
|
||||
padding-right: 14px;
|
||||
align-self: center;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './UserDetailCommentList.css';
|
||||
import LoadMore from '../components/LoadMore';
|
||||
import Comment from '../containers/UserDetailComment';
|
||||
import RejectButton from './RejectButton';
|
||||
import ApproveButton from './ApproveButton';
|
||||
|
||||
const UserDetailCommentList = props => {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
root: { user, comments: { nodes, hasNextPage } },
|
||||
acceptComment,
|
||||
rejectComment,
|
||||
selectedCommentIds,
|
||||
toggleSelect,
|
||||
viewUserDetail,
|
||||
loadMore,
|
||||
toggleSelectAll,
|
||||
bulkAcceptThenReload,
|
||||
bulkRejectThenReload,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(styles.commentList, 'talk-admin-user-detail-comment-list')}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
selectedCommentIds.length > 0
|
||||
? cn(styles.bulkActionHeader, styles.selected)
|
||||
: styles.bulkActionHeader
|
||||
}
|
||||
>
|
||||
{selectedCommentIds.length > 0 && (
|
||||
<div className={styles.bulkActionGroup}>
|
||||
<ApproveButton onClick={bulkAcceptThenReload} minimal />
|
||||
<RejectButton onClick={bulkRejectThenReload} minimal />
|
||||
<span className={styles.selectedCommentsInfo}>
|
||||
{' '}
|
||||
{selectedCommentIds.length} comments selected
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.toggleAll}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="toogleAll"
|
||||
checked={
|
||||
selectedCommentIds.length > 0 &&
|
||||
selectedCommentIds.length === nodes.length
|
||||
}
|
||||
onChange={e => {
|
||||
toggleSelectAll(
|
||||
nodes.map(comment => comment.id),
|
||||
e.target.checked
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="toogleAll">Select all</label>
|
||||
</div>
|
||||
</div>
|
||||
{nodes.map(comment => {
|
||||
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
|
||||
return (
|
||||
<Comment
|
||||
key={comment.id}
|
||||
user={user}
|
||||
root={root}
|
||||
data={data}
|
||||
comment={comment}
|
||||
acceptComment={acceptComment}
|
||||
rejectComment={rejectComment}
|
||||
selected={selected}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<LoadMore
|
||||
className={styles.loadMore}
|
||||
loadMore={loadMore}
|
||||
showLoadMore={hasNextPage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
UserDetailCommentList.propTypes = {
|
||||
root: PropTypes.object.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
selectedCommentIds: PropTypes.array.isRequired,
|
||||
viewUserDetail: PropTypes.any.isRequired,
|
||||
loadMore: PropTypes.any.isRequired,
|
||||
toggleSelect: PropTypes.func.isRequired,
|
||||
toggleSelectAll: PropTypes.func.isRequired,
|
||||
bulkAcceptThenReload: PropTypes.func.isRequired,
|
||||
bulkRejectThenReload: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default UserDetailCommentList;
|
||||
@@ -0,0 +1,69 @@
|
||||
.userInfo {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
color: #616161;
|
||||
-ms-user-select:none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout:none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color:rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu {
|
||||
background-color: white;
|
||||
border: solid 1px #999;
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
-webkit-box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
|
||||
z-index: 10;
|
||||
top: 32px;
|
||||
right: 0px;
|
||||
width: 140px;
|
||||
text-align: left;
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.menu::before{
|
||||
content: '';
|
||||
border: 10px solid transparent;
|
||||
border-top-color: #999;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: -20px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.menu::after{
|
||||
content: '';
|
||||
border: 10px solid transparent;
|
||||
border-top-color: white;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: -19px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.descriptionList {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.strongItem {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.descriptionItem {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'coral-ui';
|
||||
import styles from './UserInfoTooltip.css';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import moment from 'moment';
|
||||
|
||||
const initialState = { menuVisible: false };
|
||||
|
||||
class UserInfoTooltip extends React.Component {
|
||||
state = initialState;
|
||||
|
||||
toogleMenu = () => {
|
||||
this.setState({ menuVisible: !this.state.menuVisible });
|
||||
};
|
||||
|
||||
hideMenu = () => {
|
||||
this.setState({ menuVisible: false });
|
||||
};
|
||||
|
||||
getLastHistoryItem = (user, status = 'banned') => {
|
||||
const userHistory = user.state.status[status].history;
|
||||
return userHistory[userHistory.length - 1];
|
||||
};
|
||||
|
||||
render() {
|
||||
const { menuVisible } = this.state;
|
||||
const { user, banned, suspended } = this.props;
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.hideMenu}>
|
||||
<div className={cn(styles.userInfo, 'talk-admin-user-info-tooltip')}>
|
||||
<span
|
||||
onClick={this.toogleMenu}
|
||||
className={cn(styles.icon, 'talk-admin-user-info-tooltip-icon')}
|
||||
>
|
||||
<Icon name="info_outline" />
|
||||
</span>
|
||||
|
||||
{menuVisible && (
|
||||
<div
|
||||
className={cn(styles.menu, 'talk-admin-user-info-tooltip-menu')}
|
||||
>
|
||||
{banned && (
|
||||
<div
|
||||
className={cn(
|
||||
styles.description,
|
||||
'talk-admin-user-info-tooltip-description-banned'
|
||||
)}
|
||||
>
|
||||
<ul
|
||||
className={cn(
|
||||
styles.descriptionList,
|
||||
'talk-admin-user-info-tooltip-description-list'
|
||||
)}
|
||||
>
|
||||
<li
|
||||
className={cn(
|
||||
styles.descriptionItem,
|
||||
'talk-admin-user-info-tooltip-description-item'
|
||||
)}
|
||||
>
|
||||
<strong className={styles.strongItem}>Banned On</strong>
|
||||
<span>
|
||||
{moment(
|
||||
new Date(
|
||||
this.getLastHistoryItem(user, 'banned').created_at
|
||||
)
|
||||
).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
className={cn(
|
||||
styles.descriptionItem,
|
||||
'talk-admin-user-info-tooltip-description-item'
|
||||
)}
|
||||
>
|
||||
<strong className={styles.strongItem}>By</strong>
|
||||
<span>
|
||||
{
|
||||
this.getLastHistoryItem(user, 'banned').assigned_by
|
||||
.username
|
||||
}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{suspended && (
|
||||
<div
|
||||
className={cn(
|
||||
styles.description,
|
||||
'talk-admin-user-info-tooltip-description-suspended'
|
||||
)}
|
||||
>
|
||||
<ul
|
||||
className={cn(
|
||||
styles.descriptionList,
|
||||
'talk-admin-user-info-tooltip-description-list'
|
||||
)}
|
||||
>
|
||||
<li
|
||||
className={cn(
|
||||
styles.descriptionItem,
|
||||
'talk-admin-user-info-tooltip-description-item'
|
||||
)}
|
||||
>
|
||||
<strong className={styles.strongItem}>Suspension</strong>
|
||||
<span />
|
||||
</li>
|
||||
<li
|
||||
className={cn(
|
||||
styles.descriptionItem,
|
||||
'talk-admin-user-info-tooltip-description-item'
|
||||
)}
|
||||
>
|
||||
<strong className={styles.strongItem}>By</strong>
|
||||
<span>
|
||||
{
|
||||
this.getLastHistoryItem(user, 'suspension')
|
||||
.assigned_by.username
|
||||
}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
className={cn(
|
||||
styles.descriptionItem,
|
||||
'talk-admin-user-info-tooltip-description-item'
|
||||
)}
|
||||
>
|
||||
<strong className={styles.strongItem}>Start</strong>
|
||||
<span>
|
||||
{moment(
|
||||
new Date(
|
||||
this.getLastHistoryItem(
|
||||
user,
|
||||
'suspension'
|
||||
).created_at
|
||||
)
|
||||
).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
className={cn(
|
||||
styles.descriptionItem,
|
||||
'talk-admin-user-info-tooltip-description-item'
|
||||
)}
|
||||
>
|
||||
<strong className={styles.strongItem}>End</strong>
|
||||
<span>
|
||||
{moment(
|
||||
new Date(
|
||||
this.getLastHistoryItem(user, 'suspension').until
|
||||
)
|
||||
).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UserInfoTooltip.propTypes = {
|
||||
user: PropTypes.object,
|
||||
banned: PropTypes.bool,
|
||||
suspended: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default UserInfoTooltip;
|
||||
@@ -1,111 +0,0 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
|
||||
import {Link, IndexLink} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {Logo} from './Logo';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import Indicator from './Indicator';
|
||||
|
||||
const CoralHeader = ({
|
||||
handleLogout,
|
||||
showShortcuts = () => {},
|
||||
auth,
|
||||
root
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.headerWrapper}>
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{
|
||||
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<Navigation className={styles.nav}>
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<IndexLink
|
||||
id='moderateNav'
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
{(root.premodCount !== 0 || root.reportedCount !== 0) && <Indicator />}
|
||||
</IndexLink>
|
||||
)
|
||||
}
|
||||
<Link
|
||||
id='storiesNav'
|
||||
className={cn('talk-admin-nav-stories', styles.navLink)}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.stories')}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
id='communityNav'
|
||||
className={cn('talk-admin-nav-community', styles.navLink)}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.community')}
|
||||
{root.flaggedUsernamesCount !== 0 && <Indicator />}
|
||||
</Link>
|
||||
|
||||
{
|
||||
can(auth.user, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
id='configureNav'
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.configure')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</Navigation>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={cn(styles.settings, 'talk-admin-header-settings')}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings" className="talk-admin-header-settings-button"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://github.com/coralproject/talk/releases" target="_blank" rel="noopener noreferrer">
|
||||
View latest version
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://support.coralproject.net" target="_blank" rel="noopener noreferrer">
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout} className="talk-admin-header-sign-out">
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Header>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CoralHeader.propTypes = {
|
||||
auth: PropTypes.object,
|
||||
showShortcuts: PropTypes.func,
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default CoralHeader;
|
||||
@@ -1 +1,2 @@
|
||||
export const ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS = 'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS';
|
||||
export const ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS =
|
||||
'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS';
|
||||
|
||||
@@ -2,8 +2,10 @@ export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
|
||||
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
|
||||
export const SHOW_SUSPENDUSER_DIALOG = 'SHOW_SUSPENDUSER_DIALOG';
|
||||
export const HIDE_SUSPENDUSER_DIALOG = 'HIDE_SUSPENDUSER_DIALOG';
|
||||
export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST = 'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST';
|
||||
export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS = 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS';
|
||||
export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST =
|
||||
'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST';
|
||||
export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS =
|
||||
'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS';
|
||||
export const COMMENT_CREATE_SUCCESS = 'COMMENT_CREATE_SUCCESS';
|
||||
export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED';
|
||||
export const COMMENT_STREAM_FETCH_SUCCESS = 'COMMENT_STREAM_FETCH_SUCCESS';
|
||||
|
||||
@@ -6,8 +6,6 @@ export const FETCH_USERS_FAILURE = `${prefix}_FETCH_USERS_FAILURE`;
|
||||
|
||||
export const SORT_UPDATE = `${prefix}_SORT_UPDATE`;
|
||||
export const SET_PAGE = `${prefix}_SET_PAGE`;
|
||||
export const SET_ROLE = `${prefix}_SET_ROLE`;
|
||||
export const SET_COMMENTER_STATUS = `${prefix}_SET_COMMENTER_STATUS`;
|
||||
|
||||
export const FETCH_FLAGGED_COMMENTERS_REQUEST = `${prefix}_FETCH_FLAGGED_COMMENTERS_REQUEST`;
|
||||
export const FETCH_FLAGGED_COMMENTERS_SUCCESS = `${prefix}_FETCH_FLAGGED_COMMENTERS_SUCCESS`;
|
||||
@@ -20,3 +18,5 @@ export const SHOW_REJECT_USERNAME_DIALOG = `${prefix}_SHOW_REJECT_USERNAME_DIALO
|
||||
export const HIDE_REJECT_USERNAME_DIALOG = `${prefix}_HIDE_REJECT_USERNAME_DIALOG`;
|
||||
|
||||
export const SET_SEARCH_VALUE = `${prefix}_SET_SEARCH_VALUE`;
|
||||
|
||||
export const SET_INDICATOR_TRACK = `${prefix}_SET_INDICATOR_TRACK`;
|
||||
|
||||
@@ -2,15 +2,16 @@ export const NEXT_STEP = 'NEXT_STEP';
|
||||
export const GO_TO_STEP = 'GO_TO_STEP';
|
||||
export const PREVIOUS_STEP = 'PREVIOUS_STEP';
|
||||
export const ADD_ERROR = 'ADD_ERROR';
|
||||
export const HAS_ERROR = 'HAS_ERROR';
|
||||
export const SHOW_ERRORS = 'SHOW_ERRORS';
|
||||
export const HAS_ERROR = 'HAS_ERROR';
|
||||
export const SHOW_ERRORS = 'SHOW_ERRORS';
|
||||
export const CLEAR_ERRORS = 'CLEAR_ERRORS';
|
||||
export const INSTALL_REQUEST = 'INSTALL_REQUEST';
|
||||
export const INSTALL_SUCCESS = 'INSTALL_SUCCESS';
|
||||
export const INSTALL_FAILURE = 'INSTALL_FAILURE';
|
||||
export const UPDATE_FORMDATA_USER = 'UPDATE_FORMDATA_USER';
|
||||
export const UPDATE_FORMDATA_SETTINGS = 'UPDATE_FORMDATA_SETTINGS';
|
||||
export const UPDATE_PERMITTED_DOMAINS_SETTINGS = 'UPDATE_PERMITTED_DOMAINS_SETTINGS';
|
||||
export const UPDATE_PERMITTED_DOMAINS_SETTINGS =
|
||||
'UPDATE_PERMITTED_DOMAINS_SETTINGS';
|
||||
|
||||
export const CHECK_INSTALL_REQUEST = 'CHECK_INSTALL_REQUEST';
|
||||
export const CHECK_INSTALL_SUCCESS = 'CHECK_INSTALL_SUCCESS';
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
export const TOGGLE_MODAL = 'TOGGLE_MODAL';
|
||||
export const SINGLE_VIEW = 'SINGLE_VIEW';
|
||||
export const HIDE_SHORTCUTS_NOTE = 'HIDE_SHORTCUTS_NOTE';
|
||||
export const SET_SORT_ORDER = 'MODERATION_SET_SORT_ORDER';
|
||||
export const SHOW_STORY_SEARCH = 'SHOW_STORY_SEARCH';
|
||||
export const HIDE_STORY_SEARCH = 'HIDE_STORY_SEARCH';
|
||||
export const STORY_SEARCH_CHANGE_VALUE = 'STORY_SEARCH_CHANGE_VALUE';
|
||||
export const MODERATION_CLEAR_STATE = 'MODERATION_CLEAR_STATE';
|
||||
const prefix = `MODERATION`;
|
||||
|
||||
export const TOGGLE_MODAL = `${prefix}_TOGGLE_MODAL`;
|
||||
export const SINGLE_VIEW = `${prefix}_SINGLE_VIEW`;
|
||||
export const HIDE_SHORTCUTS_NOTE = `${prefix}_HIDE_SHORTCUTS_NOTE`;
|
||||
export const SET_SORT_ORDER = `${prefix}_SET_SORT_ORDER`;
|
||||
export const SHOW_STORY_SEARCH = `${prefix}_SHOW_STORY_SEARCH`;
|
||||
export const HIDE_STORY_SEARCH = `${prefix}_HIDE_STORY_SEARCH`;
|
||||
export const STORY_SEARCH_CHANGE_VALUE = `${prefix}_STORY_SEARCH_CHANGE_VALUE`;
|
||||
export const CLEAR_STATE = `${prefix}_CLEAR_STATE`;
|
||||
export const SELECT_COMMENT = `${prefix}_SELECT_COMMENT`;
|
||||
export const SET_INDICATOR_TRACK = `${prefix}_SET_INDICATOR_TRACK`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL';
|
||||
export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL';
|
||||
export const CHANGE_USER_DETAIL_STATUSES = 'CHANGE_USER_DETAIL_STATUSES';
|
||||
export const CHANGE_TAB_USER_DETAIL = 'CHANGE_TAB_USER_DETAIL';
|
||||
export const SELECT_USER_DETAIL_COMMENT = 'SELECT_USER_DETAIL_COMMENT';
|
||||
export const UNSELECT_USER_DETAIL_COMMENT = 'UNSELECT_USER_DETAIL_COMMENT';
|
||||
export const CLEAR_USER_DETAIL_SELECTIONS = 'CLEAR_USER_DETAIL_SELECTIONS';
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import BanUserDialog from '../components/BanUserDialog';
|
||||
import {hideBanUserDialog} from '../actions/banUserDialog';
|
||||
import {withSetUserStatus, withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
import {compose} from 'react-apollo';
|
||||
import { hideBanUserDialog } from '../actions/banUserDialog';
|
||||
import {
|
||||
withBanUser,
|
||||
withSetCommentStatus,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
import { compose } from 'react-apollo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {getErrorMessages} from 'coral-framework/utils';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
|
||||
class BanUserDialogContainer extends Component {
|
||||
|
||||
banUser = async () => {
|
||||
const {userId, commentId, commentStatus, setUserStatus, setCommentStatus, hideBanUserDialog, notify} = this.props;
|
||||
try {
|
||||
await setUserStatus({userId, status: 'BANNED'});
|
||||
hideBanUserDialog();
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({commentId, status: 'REJECTED'});
|
||||
}
|
||||
const {
|
||||
userId,
|
||||
commentId,
|
||||
commentStatus,
|
||||
banUser,
|
||||
setCommentStatus,
|
||||
hideBanUserDialog,
|
||||
} = this.props;
|
||||
await banUser({ id: userId, message: '' });
|
||||
hideBanUserDialog();
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({ commentId, status: 'REJECTED' });
|
||||
}
|
||||
catch(err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getInfo() {
|
||||
let note = t('bandialog.note_ban_user');
|
||||
@@ -46,7 +49,17 @@ class BanUserDialogContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, commentStatus}}) => ({
|
||||
BanUserDialogContainer.propTypes = {
|
||||
banUser: PropTypes.func.isRequired,
|
||||
hideBanUserDialog: PropTypes.func,
|
||||
open: PropTypes.bool,
|
||||
username: PropTypes.string,
|
||||
commentStatus: PropTypes.string,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({
|
||||
banUserDialog: { open, userId, username, commentId, commentStatus },
|
||||
}) => ({
|
||||
open,
|
||||
userId,
|
||||
username,
|
||||
@@ -54,18 +67,17 @@ const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, com
|
||||
commentStatus,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
hideBanUserDialog,
|
||||
notify,
|
||||
}, dispatch),
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
...bindActionCreators(
|
||||
{
|
||||
hideBanUserDialog,
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
withSetUserStatus,
|
||||
withSetCommentStatus,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withBanUser,
|
||||
withSetCommentStatus
|
||||
)(BanUserDialogContainer);
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import { gql } from 'react-apollo';
|
||||
import CommentDetails from '../components/CommentDetails';
|
||||
import {getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
|
||||
const slots = [
|
||||
'adminCommentDetailArea',
|
||||
'adminCommentMoreDetails',
|
||||
];
|
||||
const slots = ['adminCommentDetailArea', 'adminCommentMoreDetails'];
|
||||
|
||||
export default withFragments({
|
||||
root: gql`
|
||||
@@ -20,5 +17,5 @@ export default withFragments({
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
}
|
||||
`
|
||||
`,
|
||||
})(CommentDetails);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import { gql } from 'react-apollo';
|
||||
import CommentLabels from '../components/CommentLabels';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import {getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
|
||||
const slots = [
|
||||
'adminCommentLabels',
|
||||
];
|
||||
const slots = ['adminCommentLabels'];
|
||||
|
||||
export default withFragments({
|
||||
root: gql`
|
||||
@@ -25,10 +23,11 @@ export default withFragments({
|
||||
}
|
||||
user {
|
||||
id
|
||||
roles
|
||||
role
|
||||
}
|
||||
created_at
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
}
|
||||
`
|
||||
`,
|
||||
})(CommentLabels);
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import { gql } from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import Header from '../components/ui/Header';
|
||||
import Header from '../components/Header';
|
||||
import CommunityIndicator from '../routes/Community/containers/Indicator';
|
||||
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
|
||||
export default withQuery(gql`
|
||||
query TalkAdmin_Header {
|
||||
__typename
|
||||
premodCount: commentCount(query: {
|
||||
statuses: [PREMOD]
|
||||
})
|
||||
reportedCount: commentCount(query: {
|
||||
statuses: [NONE, PREMOD, SYSTEM_WITHHELD],
|
||||
action_type: FLAG
|
||||
})
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
})
|
||||
}
|
||||
`, {
|
||||
options: {
|
||||
pollInterval: 10000
|
||||
export default withQuery(
|
||||
gql`
|
||||
query TalkAdmin_Header($nullID: ID) {
|
||||
...${getDefinitionName(ModerationIndicator.fragments.root)}
|
||||
...${getDefinitionName(CommunityIndicator.fragments.root)}
|
||||
}
|
||||
})(Header);
|
||||
${ModerationIndicator.fragments.root}
|
||||
${CommunityIndicator.fragments.root}
|
||||
`,
|
||||
{
|
||||
options: {
|
||||
variables: { nullID: null },
|
||||
},
|
||||
}
|
||||
)(Header);
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import Layout from '../components/ui/Layout';
|
||||
import {fetchConfig} from '../actions/config';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import Layout from '../components/Layout';
|
||||
import { fetchConfig } from '../actions/config';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
import {FullLoading} from '../components/FullLoading';
|
||||
import { FullLoading } from '../components/FullLoading';
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
|
||||
import {checkLogin, handleLogin, requestPasswordReset, logout} from '../actions/auth';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
|
||||
import {
|
||||
checkLogin,
|
||||
handleLogin,
|
||||
requestPasswordReset,
|
||||
logout,
|
||||
} from '../actions/auth';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import UserDetail from 'coral-admin/src/containers/UserDetail';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class LayoutContainer extends React.Component {
|
||||
componentWillMount() {
|
||||
const {checkLogin, fetchConfig} = this.props;
|
||||
const { checkLogin, fetchConfig } = this.props;
|
||||
|
||||
checkLogin();
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const {
|
||||
user,
|
||||
loggedIn,
|
||||
loadingUser,
|
||||
loginError,
|
||||
loginMaxExceeded,
|
||||
passwordRequestSuccess
|
||||
passwordRequestSuccess,
|
||||
} = this.props.auth;
|
||||
|
||||
const {
|
||||
@@ -61,7 +65,8 @@ class LayoutContainer extends React.Component {
|
||||
<Layout
|
||||
handleLogout={logout}
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
auth={this.props.auth} >
|
||||
auth={this.props.auth}
|
||||
>
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<UserDetail />
|
||||
@@ -70,8 +75,11 @@ class LayoutContainer extends React.Component {
|
||||
);
|
||||
} else if (loggedIn) {
|
||||
return (
|
||||
<Layout {...this.props}>
|
||||
<p>This page is for team use only. Please contact an administrator if you want to join this team.</p>
|
||||
<Layout handleLogout={logout} {...this.props}>
|
||||
<p>
|
||||
This page is for team use only. Please contact an administrator if
|
||||
you want to join this team.
|
||||
</p>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -89,22 +97,25 @@ LayoutContainer.propTypes = {
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
TALK_RECAPTCHA_PUBLIC: PropTypes.string,
|
||||
checkLogin: PropTypes.func,
|
||||
fetchConfig: PropTypes.func
|
||||
fetchConfig: PropTypes.func,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth,
|
||||
TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
checkLogin,
|
||||
fetchConfig,
|
||||
handleLogin,
|
||||
requestPasswordReset,
|
||||
toggleShortcutModal,
|
||||
logout
|
||||
}, dispatch);
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
checkLogin,
|
||||
fetchConfig,
|
||||
handleLogin,
|
||||
requestPasswordReset,
|
||||
toggleShortcutModal,
|
||||
logout,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
import React, {Component} from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import SuspendUserDialog from '../components/SuspendUserDialog';
|
||||
import {hideSuspendUserDialog} from '../actions/suspendUserDialog';
|
||||
import {withSetCommentStatus, withSuspendUser} from 'coral-framework/graphql/mutations';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
import { hideSuspendUserDialog } from '../actions/suspendUserDialog';
|
||||
import {
|
||||
withSetCommentStatus,
|
||||
withSuspendUser,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import {getErrorMessages} from 'coral-framework/utils';
|
||||
import get from 'lodash/get';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
|
||||
class SuspendUserDialogContainer extends Component {
|
||||
|
||||
suspendUser = async ({message, until}) => {
|
||||
const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser, notify} = this.props;
|
||||
suspendUser = async ({ message, until }) => {
|
||||
const {
|
||||
userId,
|
||||
username,
|
||||
commentStatus,
|
||||
commentId,
|
||||
hideSuspendUserDialog,
|
||||
setCommentStatus,
|
||||
suspendUser,
|
||||
notify,
|
||||
} = this.props;
|
||||
hideSuspendUserDialog();
|
||||
try {
|
||||
await suspendUser({id: userId, message, until});
|
||||
notify(
|
||||
'success',
|
||||
t('suspenduser.notify_suspend_until', username, timeago(until)),
|
||||
);
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({commentId, status: 'REJECTED'});
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
await suspendUser({ id: userId, message, until });
|
||||
notify(
|
||||
'success',
|
||||
t('suspenduser.notify_suspend_until', username, timeago(until))
|
||||
);
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({ commentId, status: 'REJECTED' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,14 +58,16 @@ SuspendUserDialogContainer.propTypes = {
|
||||
|
||||
const withOrganizationName = withQuery(gql`
|
||||
query CoralAdmin_SuspendUserDialog {
|
||||
__typename
|
||||
settings {
|
||||
organizationName
|
||||
__typename
|
||||
settings {
|
||||
organizationName
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId, commentStatus}}) => ({
|
||||
const mapStateToProps = ({
|
||||
suspendUserDialog: { open, userId, username, commentId, commentStatus },
|
||||
}) => ({
|
||||
open,
|
||||
userId,
|
||||
username,
|
||||
@@ -68,19 +75,19 @@ const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId,
|
||||
commentStatus,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
hideSuspendUserDialog,
|
||||
notify,
|
||||
}, dispatch),
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
...bindActionCreators(
|
||||
{
|
||||
hideSuspendUserDialog,
|
||||
notify,
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSuspendUser,
|
||||
withSetCommentStatus,
|
||||
withOrganizationName,
|
||||
withOrganizationName
|
||||
)(SuspendUserDialogContainer);
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
import React from 'react';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import UserDetail from '../components/UserDetail';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
import {
|
||||
getDefinitionName,
|
||||
getSlotFragmentSpreads,
|
||||
} from 'coral-framework/utils';
|
||||
import {
|
||||
viewUserDetail,
|
||||
hideUserDetail,
|
||||
changeUserDetailStatuses,
|
||||
changeTab,
|
||||
clearUserDetailSelections,
|
||||
toggleSelectCommentInUserDetail,
|
||||
toggleSelectAllCommentInUserDetail
|
||||
toggleSelectAllCommentInUserDetail,
|
||||
} from 'coral-admin/src/actions/userDetail';
|
||||
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
import {
|
||||
withSetCommentStatus,
|
||||
withUnbanUser,
|
||||
withUnsuspendUser,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
import UserDetailComment from './UserDetailComment';
|
||||
import update from 'immutability-helper';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
import { showBanUserDialog } from 'actions/banUserDialog';
|
||||
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
|
||||
|
||||
const commentConnectionFragment = gql`
|
||||
fragment CoralAdmin_UserDetail_CommentConnection on CommentConnection {
|
||||
@@ -30,38 +39,36 @@ const commentConnectionFragment = gql`
|
||||
${UserDetailComment.fragments.comment}
|
||||
`;
|
||||
|
||||
const slots = [
|
||||
'userProfile',
|
||||
];
|
||||
const slots = ['userProfile'];
|
||||
|
||||
class UserDetailContainer extends React.Component {
|
||||
isLoadingMore = false;
|
||||
|
||||
// status can be 'ACCEPTED' or 'REJECTED'
|
||||
bulkSetCommentStatus = async (status) => {
|
||||
const changes = this.props.selectedCommentIds.map((commentId) => {
|
||||
return this.props.setCommentStatus({commentId, status});
|
||||
bulkSetCommentStatus = async status => {
|
||||
const changes = this.props.selectedCommentIds.map(commentId => {
|
||||
return this.props.setCommentStatus({ commentId, status });
|
||||
});
|
||||
|
||||
await Promise.all(changes);
|
||||
this.props.clearUserDetailSelections(); // un-select everything
|
||||
}
|
||||
};
|
||||
|
||||
bulkReject = () => {
|
||||
return this.bulkSetCommentStatus('REJECTED');
|
||||
}
|
||||
};
|
||||
|
||||
bulkAccept = () => {
|
||||
return this.bulkSetCommentStatus('ACCEPTED');
|
||||
}
|
||||
};
|
||||
|
||||
acceptComment = ({commentId}) => {
|
||||
return this.props.setCommentStatus({commentId, status: 'ACCEPTED'});
|
||||
}
|
||||
acceptComment = ({ commentId }) => {
|
||||
return this.props.setCommentStatus({ commentId, status: 'ACCEPTED' });
|
||||
};
|
||||
|
||||
rejectComment = ({commentId}) => {
|
||||
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
|
||||
}
|
||||
rejectComment = ({ commentId }) => {
|
||||
return this.props.setCommentStatus({ commentId, status: 'REJECTED' });
|
||||
};
|
||||
|
||||
loadMore = () => {
|
||||
if (this.isLoadingMore) {
|
||||
@@ -75,24 +82,25 @@ class UserDetailContainer extends React.Component {
|
||||
author_id: this.props.data.variables.author_id,
|
||||
statuses: this.props.data.variables.statuses,
|
||||
};
|
||||
this.props.data.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables,
|
||||
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
|
||||
return update(prev, {
|
||||
comments: {
|
||||
nodes: {$push: comments.nodes},
|
||||
hasNextPage: {$set: comments.hasNextPage},
|
||||
startCursor: {$set: comments.startCursor},
|
||||
endCursor: {$set: comments.endCursor},
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
this.props.data
|
||||
.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables,
|
||||
updateQuery: (prev, { fetchMoreResult: { comments } }) => {
|
||||
return update(prev, {
|
||||
comments: {
|
||||
nodes: { $push: comments.nodes },
|
||||
hasNextPage: { $set: comments.hasNextPage },
|
||||
startCursor: { $set: comments.startCursor },
|
||||
endCursor: { $set: comments.endCursor },
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoadingMore = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(err => {
|
||||
this.isLoadingMore = false;
|
||||
throw err;
|
||||
});
|
||||
@@ -104,37 +112,67 @@ class UserDetailContainer extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
if (!this.props.userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const loading = this.props.data.loading;
|
||||
|
||||
return <UserDetail
|
||||
bulkReject={this.bulkReject}
|
||||
bulkAccept={this.bulkAccept}
|
||||
changeStatus={this.props.changeUserDetailStatuses}
|
||||
toggleSelect={this.props.toggleSelectCommentInUserDetail}
|
||||
toggleSelectAll={this.props.toggleSelectAllCommentInUserDetail}
|
||||
acceptComment={this.acceptComment}
|
||||
rejectComment={this.rejectComment}
|
||||
loading={loading}
|
||||
loadMore={this.loadMore}
|
||||
{...this.props} />;
|
||||
return (
|
||||
<UserDetail
|
||||
bulkReject={this.bulkReject}
|
||||
bulkAccept={this.bulkAccept}
|
||||
changeTab={this.props.changeTab}
|
||||
toggleSelect={this.props.toggleSelectCommentInUserDetail}
|
||||
toggleSelectAll={this.props.toggleSelectAllCommentInUserDetail}
|
||||
acceptComment={this.acceptComment}
|
||||
rejectComment={this.rejectComment}
|
||||
loading={loading}
|
||||
error={this.props.data && this.props.data.error}
|
||||
loadMore={this.loadMore}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UserDetailContainer.propTypes = {
|
||||
changeTab: PropTypes.func,
|
||||
toggleSelectCommentInUserDetail: PropTypes.func,
|
||||
toggleSelectAllCommentInUserDetail: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
setCommentStatus: PropTypes.func,
|
||||
clearUserDetailSelections: PropTypes.func,
|
||||
selectedCommentIds: PropTypes.array,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, author_id: $author_id, statuses: $statuses}) {
|
||||
...CoralAdmin_Moderation_CommentConnection
|
||||
query CoralAdmin_Moderation_LoadMore(
|
||||
$limit: Int = 10
|
||||
$cursor: Cursor
|
||||
$author_id: ID!
|
||||
$statuses: [COMMENT_STATUS!]
|
||||
) {
|
||||
comments(
|
||||
query: {
|
||||
limit: $limit
|
||||
cursor: $cursor
|
||||
author_id: $author_id
|
||||
statuses: $statuses
|
||||
}
|
||||
) {
|
||||
...CoralAdmin_UserDetail_CommentConnection
|
||||
}
|
||||
}
|
||||
${commentConnectionFragment}
|
||||
`;
|
||||
|
||||
export const withUserDetailQuery = withQuery(gql`
|
||||
export const withUserDetailQuery = withQuery(
|
||||
gql`
|
||||
query CoralAdmin_UserDetail($author_id: ID!, $statuses: [COMMENT_STATUS!]) {
|
||||
user(id: $author_id) {
|
||||
id
|
||||
@@ -147,8 +185,48 @@ export const withUserDetailQuery = withQuery(gql`
|
||||
reliable {
|
||||
flagger
|
||||
}
|
||||
state {
|
||||
status {
|
||||
suspension {
|
||||
until
|
||||
history {
|
||||
until
|
||||
created_at
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
banned {
|
||||
status
|
||||
history {
|
||||
status
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
created_at
|
||||
}
|
||||
}
|
||||
username {
|
||||
status
|
||||
history {
|
||||
status
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
created_at
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'user')}
|
||||
}
|
||||
me {
|
||||
id
|
||||
}
|
||||
totalComments: commentCount(query: {author_id: $author_id, statuses: []})
|
||||
rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]})
|
||||
comments: comments(query: {
|
||||
@@ -162,37 +240,46 @@ export const withUserDetailQuery = withQuery(gql`
|
||||
}
|
||||
${UserDetailComment.fragments.root}
|
||||
${commentConnectionFragment}
|
||||
`, {
|
||||
options: ({userId, statuses}) => {
|
||||
return {
|
||||
variables: {author_id: userId, statuses},
|
||||
fetchPolicy: 'network-only',
|
||||
};
|
||||
},
|
||||
skip: (ownProps) => !ownProps.userId,
|
||||
});
|
||||
`,
|
||||
{
|
||||
options: ({ userId, statuses }) => {
|
||||
return {
|
||||
variables: { author_id: userId, statuses },
|
||||
fetchPolicy: 'network-only',
|
||||
};
|
||||
},
|
||||
skip: ownProps => !ownProps.userId,
|
||||
}
|
||||
);
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
const mapStateToProps = state => ({
|
||||
userId: state.userDetail.userId,
|
||||
selectedCommentIds: state.userDetail.selectedCommentIds,
|
||||
statuses: state.userDetail.statuses,
|
||||
activeTab: state.userDetail.activeTab,
|
||||
modal: state.ui.modal,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
changeUserDetailStatuses,
|
||||
clearUserDetailSelections,
|
||||
toggleSelectCommentInUserDetail,
|
||||
viewUserDetail,
|
||||
hideUserDetail,
|
||||
toggleSelectAllCommentInUserDetail,
|
||||
notify
|
||||
}, dispatch)
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
...bindActionCreators(
|
||||
{
|
||||
showBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
changeTab,
|
||||
clearUserDetailSelections,
|
||||
toggleSelectCommentInUserDetail,
|
||||
viewUserDetail,
|
||||
hideUserDetail,
|
||||
toggleSelectAllCommentInUserDetail,
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withUserDetailQuery,
|
||||
withSetCommentStatus,
|
||||
withUnbanUser,
|
||||
withUnsuspendUser
|
||||
)(UserDetailContainer);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user