mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2564aa98bb | ||
|
|
612ed3a16d | ||
|
|
a5acf8ba82 | ||
|
|
00b46fe0ea | ||
|
|
a722456359 | ||
|
|
943c9decba | ||
|
|
77396ccc31 | ||
|
|
e1a5a64b39 | ||
|
|
44e3d0b811 | ||
|
|
07f5f42870 | ||
|
|
123935c8bc | ||
|
|
2d4adb60be | ||
|
|
f8297e9652 | ||
|
|
74e8c28f45 | ||
|
|
89258638d4 | ||
|
|
902670cdac | ||
|
|
f11cd3c676 | ||
|
|
f088b4d459 | ||
|
|
c0252e160b | ||
|
|
98cb34f554 | ||
|
|
b2ef7fa795 | ||
|
|
ffef2f640d | ||
|
|
26bae69454 | ||
|
|
c07eaf0edb | ||
|
|
9c2ea693b9 | ||
|
|
7c339aa510 | ||
|
|
2ed6bb4067 | ||
|
|
692d92f645 | ||
|
|
7bb4b03daa | ||
|
|
e132f1d652 | ||
|
|
644788ea31 | ||
|
|
e368c50687 | ||
|
|
4569c1bd4f | ||
|
|
d6c46cb6d1 | ||
|
|
faeadeb61d | ||
|
|
d161733cdc | ||
|
|
2fd6d370c8 | ||
|
|
824abcdfa3 | ||
|
|
36c4c6a59f | ||
|
|
99235df566 | ||
|
|
af0e9bc880 | ||
|
|
23c8e2a0e6 | ||
|
|
c17fcfe5ef | ||
|
|
f81d5f8957 | ||
|
|
db55689f75 | ||
|
|
f140efe1bd | ||
|
|
fe638a4c54 | ||
|
|
b61afbc1d6 | ||
|
|
82e95dc4a1 | ||
|
|
3b7d006d1e | ||
|
|
b5daf5882b | ||
|
|
11f5460604 | ||
|
|
6e90dd6d62 | ||
|
|
635f6d7720 | ||
|
|
a4b40f2c46 | ||
|
|
14541b48a1 | ||
|
|
c7c5167311 | ||
|
|
e39c377e08 | ||
|
|
2887b20e38 | ||
|
|
0a631cfcae | ||
|
|
12c54c39db | ||
|
|
5ad92972b1 | ||
|
|
201f6fd8ae | ||
|
|
b0a6417848 | ||
|
|
f80e4af683 | ||
|
|
7f9f864aeb | ||
|
|
e9f5fcda2a | ||
|
|
b1c9d07ca7 | ||
|
|
53c32518b4 | ||
|
|
9d08dc2490 | ||
|
|
e5c385ae21 | ||
|
|
5e5e0028c9 | ||
|
|
cb7e7eae97 | ||
|
|
e98432b17e | ||
|
|
5434413308 | ||
|
|
8b4f21ff4c | ||
|
|
4072c39f05 | ||
|
|
9b9476d44f | ||
|
|
2e72fec4d7 | ||
|
|
1f489ff0e5 | ||
|
|
f582cc2534 | ||
|
|
a2536eb41b | ||
|
|
c66fb053e7 | ||
|
|
797f0fd0e3 | ||
|
|
02ec1f69ee | ||
|
|
37638cab86 | ||
|
|
4ce6206516 | ||
|
|
67b0624c90 | ||
|
|
9348985191 | ||
|
|
0df7df9dd6 | ||
|
|
ffab9ca8d5 | ||
|
|
b21eae78b0 | ||
|
|
8f3bfd2bd4 | ||
|
|
27661a9294 | ||
|
|
2c3f5385ae | ||
|
|
7bedfefecb | ||
|
|
5ab78491b5 | ||
|
|
a83261397d | ||
|
|
c8d1871418 | ||
|
|
081cdfa145 | ||
|
|
fecfcf91ef | ||
|
|
90a0446afa | ||
|
|
ec5c7f32b0 | ||
|
|
21e4cf5865 | ||
|
|
6894060648 | ||
|
|
95d14fc5a6 | ||
|
|
46c031b35d | ||
|
|
ddced5b8c9 | ||
|
|
c786805505 | ||
|
|
75125993b9 | ||
|
|
393005fce6 | ||
|
|
952b253969 | ||
|
|
ad06d5136b | ||
|
|
18016ad23a | ||
|
|
d987724c7d | ||
|
|
87e90e956d | ||
|
|
9140688c65 | ||
|
|
1053c55d19 | ||
|
|
eb67d86163 | ||
|
|
da92a8e7c2 | ||
|
|
d82932306d | ||
|
|
a76464ea16 | ||
|
|
b76f1956dc | ||
|
|
7823b65c7d | ||
|
|
38f98c7c43 | ||
|
|
30e6f1d169 | ||
|
|
df6f6a72ef | ||
|
|
c09381ff84 | ||
|
|
a172f5be51 | ||
|
|
2fec9e3c0f | ||
|
|
eb586f217b | ||
|
|
e77bdf0b90 | ||
|
|
3e045291ee | ||
|
|
93c1ecd12d | ||
|
|
a570626787 | ||
|
|
e92f75d4ad | ||
|
|
057d8cabd2 | ||
|
|
4f901d0d56 | ||
|
|
9af0c5e01a | ||
|
|
afd3b1b3fe | ||
|
|
44c6798959 | ||
|
|
e14d6007a7 | ||
|
|
1aa4f83c89 | ||
|
|
8cfccb7b00 | ||
|
|
c05bee2316 | ||
|
|
f4365eb848 | ||
|
|
96006a0196 | ||
|
|
9436188487 | ||
|
|
733d8e1eae | ||
|
|
3108f3dac3 | ||
|
|
1e2c617775 | ||
|
|
12d66a49d0 | ||
|
|
22945d49df | ||
|
|
b8c6305939 | ||
|
|
7911e366e5 | ||
|
|
1ba5fd0028 | ||
|
|
7856c9d947 | ||
|
|
7c9e2297df | ||
|
|
153c5acce1 | ||
|
|
bb42cffc17 | ||
|
|
0dffc15836 | ||
|
|
e4490e20a7 | ||
|
|
f9bc5f79ec | ||
|
|
5ec65c86ce | ||
|
|
8a5db167ff | ||
|
|
48595a1bbf | ||
|
|
ce63942902 | ||
|
|
0ecf1cedc3 | ||
|
|
7f899b0440 | ||
|
|
88b2798718 | ||
|
|
e3227322a5 | ||
|
|
a767ab73da | ||
|
|
3e65eac7a0 | ||
|
|
1b6b0a4b87 | ||
|
|
42951aaf0b | ||
|
|
6b96fcfad1 | ||
|
|
fe0c32898d | ||
|
|
091d787e34 | ||
|
|
7ee2794344 | ||
|
|
cc66f1b459 | ||
|
|
50caddee56 | ||
|
|
28dafd3e07 | ||
|
|
38c290958c | ||
|
|
2f3d7c5329 | ||
|
|
e51e8c543f | ||
|
|
4b7cc9ffcc | ||
|
|
59ddcea592 | ||
|
|
faecd80543 | ||
|
|
9e2f6f1f63 | ||
|
|
e73c113e0d | ||
|
|
6ae68a4f90 | ||
|
|
f0931b4dc6 | ||
|
|
62687f307a | ||
|
|
a83ea98978 | ||
|
|
7aadaa6f8b | ||
|
|
46f17f5381 | ||
|
|
de7ab8f7ac | ||
|
|
5302d6d935 | ||
|
|
3d0d5df139 | ||
|
|
575a3030f9 | ||
|
|
73058c624f | ||
|
|
5cb71eaee8 | ||
|
|
60677e3d2a | ||
|
|
7991188f8d | ||
|
|
13796fb268 | ||
|
|
308449a67c | ||
|
|
c733b7760d | ||
|
|
a56240675d | ||
|
|
1381395dde | ||
|
|
9a1374070f | ||
|
|
1e8ff9de7e | ||
|
|
ddb555e07f | ||
|
|
d1a3a67b35 | ||
|
|
df60ac4714 | ||
|
|
ca31dedca8 | ||
|
|
cb7213ea58 | ||
|
|
2d7d505913 | ||
|
|
a42d668329 | ||
|
|
645aaa09ae | ||
|
|
8b04da9cf4 | ||
|
|
acf792089d | ||
|
|
38469a7e69 | ||
|
|
763f753928 | ||
|
|
1a27f0e00f | ||
|
|
6d8185c33e | ||
|
|
3f83a8f1a9 | ||
|
|
f5eac91e0e | ||
|
|
a68513227a | ||
|
|
ce360a47f8 | ||
|
|
1bcf31d22d | ||
|
|
ceba418e28 | ||
|
|
0961797f8f | ||
|
|
0342b6c826 | ||
|
|
c2b0a60d8a | ||
|
|
bf89409f60 | ||
|
|
994923cae0 | ||
|
|
2a839250ed | ||
|
|
dcf09adc1c | ||
|
|
7ff4a2e75d | ||
|
|
2d1bf32464 | ||
|
|
0811c9302d | ||
|
|
a5f7a7cd75 | ||
|
|
4dd20260a8 | ||
|
|
5eafb672ef | ||
|
|
61e601a2a5 | ||
|
|
a9e3115bd9 | ||
|
|
a23bad49c6 | ||
|
|
89037a2e05 | ||
|
|
f428bfa6c9 | ||
|
|
b6951488c4 | ||
|
|
1ee7c81675 | ||
|
|
a934722672 | ||
|
|
35f8b0dc5f | ||
|
|
a882b36501 | ||
|
|
23f4724f99 | ||
|
|
9fa1f3d958 | ||
|
|
f85480c0ae | ||
|
|
fccbcce7a8 | ||
|
|
ef0c0ed628 | ||
|
|
ffc6a72017 | ||
|
|
b719168b18 | ||
|
|
4eac739568 | ||
|
|
8ecf243d2d | ||
|
|
54ea0b1a3f | ||
|
|
cb4d2998f6 | ||
|
|
e0f9bb4165 | ||
|
|
eec0e18600 | ||
|
|
78d4740abe | ||
|
|
01ed9880f4 | ||
|
|
c402627280 | ||
|
|
647b5ed745 | ||
|
|
a540dfe8d4 | ||
|
|
19dfe86118 | ||
|
|
15111c81a2 | ||
|
|
cea6f7b8e8 | ||
|
|
7552e34a15 | ||
|
|
7da7db1eb4 | ||
|
|
b3e15de9f3 | ||
|
|
9bff90bd4d | ||
|
|
22f473a9ea | ||
|
|
663a46896e | ||
|
|
bfa195d7cb | ||
|
|
9028c1b4eb | ||
|
|
d43d359048 | ||
|
|
ec2c62a607 | ||
|
|
8b044954d9 | ||
|
|
d47287fbd5 | ||
|
|
74f778aef7 | ||
|
|
9566f83eac | ||
|
|
170ca044fe | ||
|
|
ccf978f87c | ||
|
|
84e4091ea6 | ||
|
|
65e56d36a1 | ||
|
|
1f813da68b | ||
|
|
7c7fe1b3cc | ||
|
|
0ea2660c97 | ||
|
|
262ec5b0f8 | ||
|
|
f7b06dc57c | ||
|
|
c3884ed0ed | ||
|
|
f744598dfd | ||
|
|
64d760bffc | ||
|
|
c81a17ac25 | ||
|
|
7dfb2bded1 | ||
|
|
077bc5ad33 | ||
|
|
3b26e855bc | ||
|
|
7a0e04ae56 | ||
|
|
7125250cd6 | ||
|
|
5036a2665a | ||
|
|
b738961e1c | ||
|
|
b2e871f4a6 | ||
|
|
77b7ddf337 | ||
|
|
3804a7afae | ||
|
|
d38dbc87b6 | ||
|
|
a7a80a452b | ||
|
|
1b09825602 | ||
|
|
f49591225f | ||
|
|
cf1c93f395 | ||
|
|
4b9990e154 | ||
|
|
58a5d5eea8 | ||
|
|
5028f4adf4 | ||
|
|
44ec469fa5 | ||
|
|
7c7a8d6b64 | ||
|
|
e77a8ce3c7 | ||
|
|
f2a67f04f9 | ||
|
|
0a602b79c3 | ||
|
|
b8452b535b | ||
|
|
6c19b90652 | ||
|
|
0cbf6e24f7 | ||
|
|
833e78f99a | ||
|
|
64580696fb | ||
|
|
e5156f7fc1 | ||
|
|
de90bdbd19 | ||
|
|
00f365ab38 | ||
|
|
9f62754fd7 | ||
|
|
1dcdccecbc | ||
|
|
dc49d23dc8 | ||
|
|
8eaac3eaa2 | ||
|
|
87d14f781f | ||
|
|
5e13af8282 | ||
|
|
6a5f6e3ce1 | ||
|
|
fabc8b8c46 | ||
|
|
0808f97fb5 | ||
|
|
11bd1a9771 | ||
|
|
686eb614f7 | ||
|
|
a9abd55895 | ||
|
|
165d07cae9 | ||
|
|
af4b01c2ae | ||
|
|
25c11ad690 | ||
|
|
ef815005a8 | ||
|
|
abb8b775b9 | ||
|
|
d228d49c7d | ||
|
|
6289f8021e | ||
|
|
7ea6d133c2 | ||
|
|
ae71f5ddc3 | ||
|
|
fe673c390d | ||
|
|
2022be4134 | ||
|
|
1ff6c5bdcc | ||
|
|
df141925fb | ||
|
|
98e2cb3387 | ||
|
|
d4536cb1a2 | ||
|
|
b126cc3cb3 | ||
|
|
f8031be865 | ||
|
|
ae81d5cb3c | ||
|
|
4aa6b53022 | ||
|
|
9ce6ab108a | ||
|
|
418e68aa8a | ||
|
|
76e1d55601 | ||
|
|
198b347ff0 | ||
|
|
34a19c532d | ||
|
|
4c1ebde3cc | ||
|
|
83388b0791 | ||
|
|
8afbe9d30d | ||
|
|
6b60ceb52e | ||
|
|
e71eb28e42 | ||
|
|
33aea66ff7 | ||
|
|
c3bcf7dd63 | ||
|
|
2b2e2f9b5f | ||
|
|
4cfdb6574b | ||
|
|
b63fce4ebc | ||
|
|
b81c2e480e | ||
|
|
d2525ac8cf | ||
|
|
d38128bad9 | ||
|
|
1220ce464e | ||
|
|
1ca40a3592 | ||
|
|
ad4ea08a6a | ||
|
|
dc3adf0413 | ||
|
|
ac2699856f | ||
|
|
c0f66277b3 | ||
|
|
db37a0b38e | ||
|
|
cadfb57cd2 | ||
|
|
2284692931 | ||
|
|
2827ca5229 | ||
|
|
f159d8a488 | ||
|
|
15f1733fa1 | ||
|
|
776f29a39d | ||
|
|
c52a97338c | ||
|
|
abda77a7e7 | ||
|
|
06cf33521f | ||
|
|
cdfef8c989 | ||
|
|
2e8863285d | ||
|
|
7360a43898 | ||
|
|
45cf72256b | ||
|
|
17bc396b06 | ||
|
|
ada5e6f747 | ||
|
|
20b182f455 | ||
|
|
b60c879599 | ||
|
|
0f93d6ce8a | ||
|
|
ed5810aceb | ||
|
|
b8ffe51812 | ||
|
|
d008947b41 | ||
|
|
bf298b311a | ||
|
|
b1d9ca0640 | ||
|
|
c0ac5b0799 | ||
|
|
2039cc5b9e | ||
|
|
bb2284909f | ||
|
|
f018d7f597 | ||
|
|
6794827a77 | ||
|
|
6956b4cd31 | ||
|
|
83ded4bda9 | ||
|
|
1fea2f77c1 | ||
|
|
80cfc663e3 | ||
|
|
a3cda1cf1c | ||
|
|
0013c36686 | ||
|
|
d947e103fc | ||
|
|
6d70a7b20e | ||
|
|
b9243938bd | ||
|
|
ca422f0109 | ||
|
|
c62e7328c5 | ||
|
|
df286a31ad | ||
|
|
ca069317e9 | ||
|
|
7c7799d2c0 | ||
|
|
8d30129f47 | ||
|
|
a9ec160f95 | ||
|
|
85836a3965 | ||
|
|
b10ed4e43e | ||
|
|
1e00d75e3c | ||
|
|
66b3d4ab00 | ||
|
|
1ab662efda | ||
|
|
d100128843 | ||
|
|
89e7f54a32 | ||
|
|
e14d380d66 | ||
|
|
fd87ef3b85 | ||
|
|
b260945822 | ||
|
|
1521ec43eb | ||
|
|
5ebdfa8c23 | ||
|
|
6913fe05ae | ||
|
|
41b2af76c4 | ||
|
|
9f1bce1221 | ||
|
|
528c2a9491 | ||
|
|
948d362c40 | ||
|
|
30842362cf | ||
|
|
976bd1e9ad | ||
|
|
7bdd28cb9c | ||
|
|
e16dd14e47 | ||
|
|
c9f2c27a69 | ||
|
|
c4a91d225e | ||
|
|
d1fc5668fb | ||
|
|
aad245fcbf | ||
|
|
49ad48d70f | ||
|
|
800fd63549 | ||
|
|
c08d040d1d | ||
|
|
1e8d2ce3a6 | ||
|
|
a22d0f7536 | ||
|
|
53cac48e3b | ||
|
|
27216cbfdb | ||
|
|
ddadd6c5d3 | ||
|
|
10936d9f41 | ||
|
|
1d5f1e676f | ||
|
|
c2f8f41e59 | ||
|
|
567f8bf94e | ||
|
|
11b2bc265a | ||
|
|
76b125f042 | ||
|
|
d2a2f94531 | ||
|
|
0df4fd1e37 | ||
|
|
5ceda4f2d2 | ||
|
|
0523a1f828 | ||
|
|
12d3273d3d | ||
|
|
e6330cd8c3 | ||
|
|
bc67bc7b78 | ||
|
|
a91a2d3fa3 | ||
|
|
32a8d78eb8 | ||
|
|
203707124c | ||
|
|
a39b7237e6 | ||
|
|
190e93c8d7 | ||
|
|
02a75c590f | ||
|
|
8cdf289b13 | ||
|
|
e435c323ec | ||
|
|
f92754a700 | ||
|
|
5c8130bece | ||
|
|
8ceeb4b275 | ||
|
|
3ec81099f7 | ||
|
|
c9b82008de | ||
|
|
5d2b9439d8 | ||
|
|
b8f24b952b | ||
|
|
b6d284495e | ||
|
|
a527990413 | ||
|
|
325626fecf | ||
|
|
15c7a7cad7 | ||
|
|
b972f4305e |
+2
-1
@@ -6,7 +6,7 @@ node_modules
|
||||
scripts
|
||||
!scripts/generateIntrospectionResult.js
|
||||
|
||||
# documentation should not be visable in production.
|
||||
# documentation should not be visible in production.
|
||||
docs
|
||||
|
||||
# static assets are rebuild in the docker container.
|
||||
@@ -14,6 +14,7 @@ dist
|
||||
|
||||
# tests are not run in the docker container.
|
||||
test
|
||||
__tests__
|
||||
|
||||
# we won't use the .git folder in production.
|
||||
.git
|
||||
|
||||
+9
-1
@@ -1,6 +1,5 @@
|
||||
dist
|
||||
docs
|
||||
client/lib
|
||||
**/*.html
|
||||
plugins/*
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
@@ -14,5 +13,14 @@ plugins/*
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
!plugins/talk-plugin-sort-most-loved
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
|
||||
node_modules
|
||||
|
||||
+35
-26
@@ -7,51 +7,53 @@
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"plugins": [
|
||||
"promise",
|
||||
"json"
|
||||
],
|
||||
"rules": {
|
||||
"indent": ["error",
|
||||
2
|
||||
],
|
||||
"no-console": [
|
||||
0
|
||||
],
|
||||
"no-console": "off",
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"no-template-curly-in-string": [1],
|
||||
"no-unsafe-negation": [1],
|
||||
"array-callback-return": [1],
|
||||
"no-template-curly-in-string": "warn",
|
||||
"no-unsafe-negation": "warn",
|
||||
"array-callback-return": "warn",
|
||||
"arrow-parens": ["warn", "always"],
|
||||
"template-curly-spacing": "warn",
|
||||
"eqeqeq": [2, "smart"],
|
||||
"no-eval": [2],
|
||||
"no-global-assign": [2],
|
||||
"no-implied-eval": [2],
|
||||
"eqeqeq": ["error", "smart"],
|
||||
"no-eval": "error",
|
||||
"no-global-assign": "error",
|
||||
"no-implied-eval": "error",
|
||||
"lines-around-comment": ["warn", {"beforeLineComment": true}],
|
||||
"spaced-comment": ["warn", "always", { "line": { "exceptions": ["-", "="] } }],
|
||||
"no-script-url": [2],
|
||||
"no-throw-literal": [2],
|
||||
"yoda": [1],
|
||||
"no-path-concat": [2],
|
||||
"eol-last": [1],
|
||||
"no-nested-ternary": [1],
|
||||
"no-tabs": [2],
|
||||
"no-unneeded-ternary": [1],
|
||||
"object-curly-spacing": [1],
|
||||
"no-script-url": "error",
|
||||
"no-throw-literal": "error",
|
||||
"yoda": "warn",
|
||||
"no-path-concat": "error",
|
||||
"eol-last": "warn",
|
||||
"no-nested-ternary": "warn",
|
||||
"no-tabs": "error",
|
||||
"no-unneeded-ternary": "warn",
|
||||
"object-curly-spacing": "warn",
|
||||
"space-infix-ops": ["error"],
|
||||
"space-in-parens": ["error", "never"],
|
||||
"space-unary-ops": ["error", {
|
||||
"words": true,
|
||||
"nonwords": false
|
||||
}],
|
||||
"no-const-assign": [2],
|
||||
"no-duplicate-imports": [2],
|
||||
"prefer-template": [1],
|
||||
"no-const-assign": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"prefer-template": "warn",
|
||||
"comma-spacing": ["error", {
|
||||
"after": true
|
||||
}],
|
||||
"no-var": [2],
|
||||
"no-lonely-if": [2],
|
||||
"curly": [2],
|
||||
"no-var": "error",
|
||||
"no-lonely-if": "error",
|
||||
"curly": "error",
|
||||
"no-unused-vars": ["error", {
|
||||
"argsIgnorePattern": "^_|next",
|
||||
"varsIgnorePattern": "^_"
|
||||
@@ -61,6 +63,13 @@
|
||||
}],
|
||||
"newline-per-chained-call": ["error", {
|
||||
"ignoreChainWithDepth": 2
|
||||
}]
|
||||
}],
|
||||
"promise/no-return-wrap": "error",
|
||||
"promise/param-names": "error",
|
||||
"promise/catch-or-return": "error",
|
||||
"promise/no-native": "off",
|
||||
"promise/no-nesting": "warn",
|
||||
"promise/no-promise-in-callback": "warn",
|
||||
"promise/no-callback-in-promise": "warn"
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -14,7 +14,6 @@ client/coral-framework/graphql/introspection.json
|
||||
*.swp
|
||||
*.DS_STORE
|
||||
|
||||
test/e2e/reports
|
||||
coverage/
|
||||
|
||||
plugins.json
|
||||
@@ -30,5 +29,14 @@ plugins/*
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
!plugins/talk-plugin-sort-most-loved
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
|
||||
**/node_modules/*
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
FROM node:8
|
||||
FROM node:8-alpine
|
||||
|
||||
# Create app directory
|
||||
RUN mkdir -p /usr/src/app
|
||||
@@ -12,6 +12,9 @@ EXPOSE 5000
|
||||
# Bundle app source
|
||||
COPY . /usr/src/app
|
||||
|
||||
# Ensure the runtime of the container is in production mode.
|
||||
ENV NODE_ENV production
|
||||
|
||||
# Install app dependencies and build static assets.
|
||||
RUN yarn global add node-gyp && \
|
||||
yarn install --frozen-lockfile && \
|
||||
@@ -19,7 +22,4 @@ RUN yarn global add node-gyp && \
|
||||
yarn build && \
|
||||
yarn cache clean
|
||||
|
||||
# Ensure the runtime of the container is in production mode.
|
||||
ENV NODE_ENV production
|
||||
|
||||
CMD ["yarn", "start"]
|
||||
|
||||
+2
-5
@@ -7,8 +7,5 @@ ONBUILD COPY . /usr/src/app
|
||||
# we need to have webpack available. We then build the new dependancies and
|
||||
# clear out the development dependancies again. After this we of course need to
|
||||
# clear out the yarn cache, this saves quite a lot of size.
|
||||
ONBUILD RUN NODE_ENV=development yarn install --frozen-lockfile && \
|
||||
NODE_ENV=production cli plugins reconcile && \
|
||||
NODE_ENV=production yarn build && \
|
||||
NODE_ENV=production yarn install --production --force && \
|
||||
yarn cache clean
|
||||
ONBUILD RUN cli plugins reconcile && \
|
||||
yarn build
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
# Talk [](https://circleci.com/gh/coralproject/talk)
|
||||
[](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore)
|
||||
|
||||
Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here.](https://coralproject.net/products/talk.html)
|
||||
|
||||
Third party licenses are available via the `/client/3rdpartylicenses.txt`
|
||||
endpoint when the server is running with built assets.
|
||||
|
||||
## Documentation
|
||||
## Important Links
|
||||
|
||||
See our [Talk Documentation & Guides](https://coralproject.github.io/talk/).
|
||||
- Developer Documentation & Setup Guides: https://coralproject.github.io/talk/
|
||||
|
||||
- Pivotal Tracker Backlog & Release Schedule: https://www.pivotaltracker.com/n/projects/1863625
|
||||
|
||||
## Learn More about Coral
|
||||
|
||||
- Community Forums: https://community.coralproject.net/
|
||||
|
||||
- Website: https://coralproject.net
|
||||
|
||||
- Blog: https://blog.coralproject.net
|
||||
|
||||
- Community Guides for Journalism: https://guides.coralproject.net/
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -3,71 +3,42 @@ const bodyParser = require('body-parser');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
const helmet = require('helmet');
|
||||
const authentication = require('./middleware/authentication');
|
||||
const {passport} = require('./services/passport');
|
||||
const plugins = require('./services/plugins');
|
||||
const pubsub = require('./services/pubsub');
|
||||
const i18n = require('./services/i18n');
|
||||
const enabled = require('debug').enabled;
|
||||
const errors = require('./errors');
|
||||
const {createGraphOptions} = require('./graph');
|
||||
const apollo = require('graphql-server-express');
|
||||
const accepts = require('accepts');
|
||||
const compression = require('compression');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const {ROOT_URL} = require('./config');
|
||||
const {BASE_URL, BASE_PATH, MOUNT_PATH} = require('./url');
|
||||
const routes = require('./routes');
|
||||
const debug = require('debug')('talk:app');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Middleware declarations.
|
||||
//==============================================================================
|
||||
// APPLICATION WIDE MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
// Add the logging middleware only if we aren't testing.
|
||||
if (app.get('env') !== 'test') {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
app.use(morgan('dev'));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// APP MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
// 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);
|
||||
|
||||
// We disable frameward on helmet to allow crossdomain injection of the embed
|
||||
// Enable a suite of security good practices through helmet. We disable
|
||||
// frameguard to allow crossdomain injection of the embed.
|
||||
app.use(helmet({
|
||||
frameguard: false
|
||||
frameguard: false,
|
||||
}));
|
||||
|
||||
// Compress the responses if appropriate.
|
||||
app.use(compression());
|
||||
|
||||
// Parse the cookies on the request.
|
||||
app.use(cookieParser());
|
||||
|
||||
// Parse the body json if it's there.
|
||||
app.use(bodyParser.json());
|
||||
|
||||
//==============================================================================
|
||||
// STATIC FILES
|
||||
//==============================================================================
|
||||
|
||||
// If the application is in production mode, then add gzip rewriting for the
|
||||
// content.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.get('*.js', (req, res, next) => {
|
||||
const accept = accepts(req);
|
||||
if (accept.encoding(['gzip']) === 'gzip') {
|
||||
|
||||
// Adjsut the headers on the request by adding a content type header
|
||||
// because express won't be able to detect the mime-type with the .gz
|
||||
// extension and we need to decalre support for the gzip encoding.
|
||||
res.set('Content-Type', 'application/javascript');
|
||||
res.set('Content-Encoding', 'gzip');
|
||||
|
||||
// Rewrite the url so that the gzip version will be served instead.
|
||||
req.url = `${req.url}.gz`;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
app.use('/client', express.static(path.join(__dirname, 'dist')));
|
||||
app.use('/public', express.static(path.join(__dirname, 'public')));
|
||||
|
||||
//==============================================================================
|
||||
// VIEW CONFIGURATION
|
||||
//==============================================================================
|
||||
@@ -75,124 +46,19 @@ app.use('/public', express.static(path.join(__dirname, 'public')));
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
// Set the BASE_URL as the ROOT_URL.
|
||||
app.locals.BASE_URL = ROOT_URL;
|
||||
if (app.locals.BASE_URL[app.locals.BASE_URL.length - 1] !== '/') {
|
||||
app.locals.BASE_URL += '/';
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// PASSPORT MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
const passportDebug = require('debug')('talk:passport');
|
||||
|
||||
// Install the passport plugins.
|
||||
plugins.get('server', 'passport').forEach((plugin) => {
|
||||
passportDebug(`added plugin '${plugin.plugin.name}'`);
|
||||
|
||||
// Pass the passport.js instance to the plugin to allow it to inject it's
|
||||
// functionality.
|
||||
plugin.passport(passport);
|
||||
});
|
||||
|
||||
// Setup the PassportJS Middleware.
|
||||
app.use(passport.initialize());
|
||||
|
||||
// Attach the authentication middleware, this will be responsible for decoding
|
||||
// (if present) the JWT on the request.
|
||||
app.use('/api', authentication);
|
||||
|
||||
const pubsubClient = pubsub.createClientFactory();
|
||||
|
||||
// To handle dependancy injection safer, we inject the pubsub handle onto the
|
||||
// request object.
|
||||
app.use('/api', (req, res, next) => {
|
||||
|
||||
// Attach the pubsub handle to the requests.
|
||||
req.pubsub = pubsubClient();
|
||||
|
||||
// Forward on the request.
|
||||
next();
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// GraphQL Router
|
||||
//==============================================================================
|
||||
|
||||
// GraphQL endpoint.
|
||||
app.use('/api/v1/graph/ql', apollo.graphqlExpress(createGraphOptions));
|
||||
|
||||
// Only include the graphiql tool if we aren't in production mode.
|
||||
if (app.get('env') !== 'production') {
|
||||
|
||||
// Interactive graphiql interface.
|
||||
app.use('/api/v1/graph/iql', (req, res) => {
|
||||
res.render('graphiql', {
|
||||
endpointURL: '/api/v1/graph/ql'
|
||||
});
|
||||
});
|
||||
|
||||
// GraphQL documention.
|
||||
app.get('/admin/docs', (req, res) => {
|
||||
res.render('admin/docs');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// ROUTES
|
||||
//==============================================================================
|
||||
|
||||
app.use('/', require('./routes'));
|
||||
// Apply the BASE_PATH, BASE_URL, and MOUNT_PATH on the app.locals, which will
|
||||
// make them available on the templates and the routers.
|
||||
app.locals.BASE_URL = BASE_URL;
|
||||
app.locals.BASE_PATH = BASE_PATH;
|
||||
app.locals.MOUNT_PATH = MOUNT_PATH;
|
||||
|
||||
//==============================================================================
|
||||
// ERROR HANDLING
|
||||
//==============================================================================
|
||||
debug(`mounting routes on the ${MOUNT_PATH} path`);
|
||||
|
||||
// Catch 404 and forward to error handler.
|
||||
app.use((req, res, next) => {
|
||||
next(errors.ErrNotFound);
|
||||
});
|
||||
|
||||
// General error handler. Respond with the message and error if we have it while
|
||||
// returning a status code that makes sense.
|
||||
app.use('/api', (err, req, res, next) => {
|
||||
if (err !== errors.ErrNotFound) {
|
||||
if (app.get('env') !== 'test' || enabled('talk:errors')) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (err instanceof errors.APIError) {
|
||||
res.status(err.status).json({
|
||||
message: err.message,
|
||||
error: err
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({});
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/', (err, req, res, next) => {
|
||||
if (err !== errors.ErrNotFound) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
i18n.init(req);
|
||||
|
||||
if (err instanceof errors.APIError) {
|
||||
res.status(err.status);
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
error: app.get('env') === 'development' ? err : {}
|
||||
});
|
||||
} else {
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
error: app.get('env') === 'development' ? err : {}
|
||||
});
|
||||
}
|
||||
});
|
||||
// Actually apply the routes.
|
||||
app.use(MOUNT_PATH, routes);
|
||||
|
||||
module.exports = app;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
{
|
||||
"name": "The Coral Project: Talk",
|
||||
"env": {
|
||||
"TALK_SESSION_SECRET": {
|
||||
"TALK_JWT_SECRET": {
|
||||
"description": "The session secret",
|
||||
"generator": "secret"
|
||||
},
|
||||
"TALK_ROOT_URL": {
|
||||
"description": "Please copy the App Name you choose above. If you did not choose one, please do so now and copy it here. Talk on Heroku will not work without this setting.",
|
||||
"value":"https://<COPY APP NAME HERE>.herokuapp.com",
|
||||
"required": true
|
||||
},
|
||||
"TALK_FACEBOOK_APP_ID": {
|
||||
"value": "",
|
||||
"required": true
|
||||
@@ -14,8 +19,7 @@
|
||||
"required": true
|
||||
},
|
||||
"NODE_ENV": "production",
|
||||
"TALK_SMTP_PORT": "2525",
|
||||
"REWRITE_ENV": "TALK_PORT:PORT,TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:POSTMARK_SMTP_SERVER,TALK_SMTP_USERNAME:POSTMARK_API_TOKEN,TALK_SMTP_PASSWORD:POSTMARK_API_TOKEN",
|
||||
"REWRITE_ENV": "TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD",
|
||||
"NPM_CONFIG_PRODUCTION": "false"
|
||||
},
|
||||
"addons": [{
|
||||
@@ -25,8 +29,8 @@
|
||||
"plan": "rediscloud:30",
|
||||
"as": "REDIS"
|
||||
}, {
|
||||
"plan": "postmark:10k",
|
||||
"as": "POSTMARK"
|
||||
"plan": "mailgun:starter",
|
||||
"as": "MAILGUN"
|
||||
}],
|
||||
"image": "heroku/nodejs",
|
||||
"success_url": "/admin/install"
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
// const util = require('./util');
|
||||
const program = require('./commander');
|
||||
const {head, map} = require('lodash');
|
||||
const Matcher = require('did-you-mean');
|
||||
|
||||
program
|
||||
.command('serve', 'serve the application')
|
||||
@@ -16,12 +17,28 @@ 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'
|
||||
)
|
||||
.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);
|
||||
|
||||
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(`\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
|
||||
@@ -30,6 +47,8 @@ program
|
||||
*/
|
||||
process.once('exit', () => {
|
||||
if (
|
||||
|
||||
// program.runningCommand &&
|
||||
program.runningCommand.killed === false &&
|
||||
program.runningCommand.exitCode === null
|
||||
) {
|
||||
|
||||
+13
-8
@@ -78,18 +78,23 @@ function rangeJobsByState(state = 'complete', limit) {
|
||||
/**
|
||||
* Cleans up the jobs that are in the queue.
|
||||
*/
|
||||
function cleanupJobs(options) {
|
||||
async function cleanupJobs(options) {
|
||||
const n = 100;
|
||||
|
||||
Promise.all([
|
||||
rangeJobsByState('complete', n),
|
||||
options.stuck ? rangeJobsByState('failed', n) : false
|
||||
])
|
||||
.then((joblists) => joblists.filter((jobs) => jobs).map(removeJobs))
|
||||
.then(() => {
|
||||
try {
|
||||
const joblists = await Promise.all([
|
||||
rangeJobsByState('complete', n),
|
||||
options.stuck ? rangeJobsByState('failed', n) : false
|
||||
]);
|
||||
|
||||
await joblists.filter((jobs) => jobs).map(removeJobs);
|
||||
|
||||
util.shutdown();
|
||||
console.log('Removed old jobs');
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ function versionMatch(name, version) {
|
||||
}
|
||||
}
|
||||
|
||||
const EXTERNAL = /^\w[a-z\-0-9\.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
|
||||
const EXTERNAL = /^\w[a-z\-0-9.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
|
||||
|
||||
function reconcilePackages({quiet = false, upgradeRemote = false}) {
|
||||
const fetchable = [];
|
||||
|
||||
+32
-6
@@ -94,7 +94,7 @@ const performSetup = async () => {
|
||||
name: 'requireEmailConfirmation',
|
||||
default: settings.requireEmailConfirmation,
|
||||
message: 'Should emails always be confirmed'
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
// Update the settings that were changed.
|
||||
@@ -104,6 +104,32 @@ const performSetup = async () => {
|
||||
}
|
||||
});
|
||||
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'inputWhitelistedDomains',
|
||||
default: true,
|
||||
message: 'Would you like to specify a whitelisted domain'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'whitelistedDomain',
|
||||
message: 'Whitelisted Domain',
|
||||
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');
|
||||
|
||||
let user = await inquirer.prompt([
|
||||
@@ -147,7 +173,11 @@ const performSetup = async () => {
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
filter: (confirmPassword) => {
|
||||
filter: (confirmPassword, {password}) => {
|
||||
if (password !== confirmPassword) {
|
||||
return Promise.reject(new Error('Passwords do not match'));
|
||||
}
|
||||
|
||||
return UsersService
|
||||
.isValidPassword(confirmPassword)
|
||||
.catch((err) => {
|
||||
@@ -157,10 +187,6 @@ const performSetup = async () => {
|
||||
},
|
||||
]);
|
||||
|
||||
if (user.password !== user.confirmPassword) {
|
||||
return Promise.reject(new Error('Passwords do not match'));
|
||||
}
|
||||
|
||||
let {user: newUser} = await SetupService.setup({
|
||||
settings: settings.toObject(),
|
||||
user: {
|
||||
|
||||
+38
-45
@@ -98,39 +98,32 @@ function getUserCreateAnswers(options) {
|
||||
/**
|
||||
* Prompts for input and registers a user based on those.
|
||||
*/
|
||||
function createUser(options) {
|
||||
getUserCreateAnswers(options)
|
||||
.then((answers) => {
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
return Promise.reject(new Error('Passwords do not match'));
|
||||
}
|
||||
async function createUser(options) {
|
||||
try {
|
||||
const answers = await getUserCreateAnswers(options);
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
throw new Error('Passwords do not match');
|
||||
}
|
||||
|
||||
return answers;
|
||||
})
|
||||
.then((answers) => {
|
||||
return UsersService
|
||||
.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim())
|
||||
.then((user) => {
|
||||
console.log(`Created user ${user.id}.`);
|
||||
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}.`);
|
||||
});
|
||||
}));
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
});
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,21 +162,21 @@ function passwd(userID) {
|
||||
validate: validateRequired('Confirm Password is required')
|
||||
}
|
||||
])
|
||||
.then((answers) => {
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
return Promise.reject(new Error('Password mismatch'));
|
||||
}
|
||||
.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);
|
||||
});
|
||||
return UsersService.changePassword(userID, answers.password);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Password changed.');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/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();
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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'),
|
||||
];
|
||||
@@ -40,8 +40,6 @@ 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
|
||||
# Run the e2e test suite.
|
||||
# - E2E_REPORT_PATH=$CIRCLE_TEST_REPORTS/e2e yarn e2e
|
||||
|
||||
deployment:
|
||||
release:
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
"transform-react-jsx",
|
||||
"syntax-dynamic-import"
|
||||
]
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": "../.eslintrc.json",
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
|
||||
import {history} from 'coral-framework/helpers/router';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Configure from 'routes/Configure';
|
||||
import Dashboard from 'routes/Dashboard';
|
||||
@@ -37,32 +37,24 @@ const routes = (
|
||||
<Route path='moderate' component={ModerationLayout}>
|
||||
<IndexRoute components={Moderation} />
|
||||
|
||||
<Route path='all' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='new' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='approved' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='premod' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='rejected' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='reported' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
|
||||
<Route path=':id' components={Moderation} />
|
||||
<Route path=':tabOrId' components={Moderation} />
|
||||
|
||||
<Route path=':tab' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</div>
|
||||
);
|
||||
|
||||
const AppRouter = () => <Router history={history} routes={routes}/>;
|
||||
class AppRouter extends React.Component {
|
||||
static contextTypes = {
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
render() {
|
||||
return <Router history={this.context.history} routes={routes} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default AppRouter;
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
UPDATE_ASSETS
|
||||
} from '../constants/assets';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
/**
|
||||
@@ -17,9 +16,9 @@ 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 = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch) => {
|
||||
export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch, _, {rest}) => {
|
||||
dispatch({type: FETCH_ASSETS_REQUEST});
|
||||
return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`)
|
||||
return rest(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`)
|
||||
.then(({result, count}) =>
|
||||
dispatch({type: FETCH_ASSETS_SUCCESS,
|
||||
assets: result,
|
||||
@@ -34,9 +33,9 @@ export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filte
|
||||
|
||||
// Update an asset state
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const updateAssetState = (id, closedAt) => (dispatch) => {
|
||||
dispatch({type: UPDATE_ASSET_STATE_REQUEST});
|
||||
return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
|
||||
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);
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import bowser from 'bowser';
|
||||
import * as actions from '../constants/auth';
|
||||
import coralApi from 'coral-framework/helpers/request';
|
||||
import * as Storage from 'coral-framework/helpers/storage';
|
||||
import {handleAuthToken} from 'coral-framework/actions/auth';
|
||||
import {resetWebsocket} from 'coral-framework/services/client';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
//==============================================================================
|
||||
// SIGN IN
|
||||
//==============================================================================
|
||||
|
||||
export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => {
|
||||
export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client, storage}) => {
|
||||
dispatch({type: actions.LOGIN_REQUEST});
|
||||
|
||||
const params = {
|
||||
@@ -27,18 +24,18 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch) =>
|
||||
};
|
||||
}
|
||||
|
||||
return coralApi('/auth/local', params)
|
||||
return rest('/auth/local', params)
|
||||
.then(({user, token}) => {
|
||||
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios) {
|
||||
Storage.removeItem('token');
|
||||
if (!bowser.safari && !bowser.ios && storage) {
|
||||
storage.removeItem('token');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
dispatch(handleAuthToken(token));
|
||||
resetWebsocket();
|
||||
client.resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -84,11 +81,11 @@ const forgotPasswordFailure = (error) => ({
|
||||
error,
|
||||
});
|
||||
|
||||
export const requestPasswordReset = (email) => (dispatch) => {
|
||||
export const requestPasswordReset = (email) => (dispatch, _, {rest}) => {
|
||||
dispatch(forgotPasswordRequest(email));
|
||||
const redirectUri = location.href;
|
||||
|
||||
return coralApi('/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) => {
|
||||
console.error(error);
|
||||
@@ -116,18 +113,18 @@ const checkLoginFailure = (error) => ({
|
||||
error
|
||||
});
|
||||
|
||||
export const checkLogin = () => (dispatch) => {
|
||||
export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
|
||||
dispatch(checkLoginRequest());
|
||||
return coralApi('/auth')
|
||||
return rest('/auth')
|
||||
.then(({user}) => {
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios) {
|
||||
Storage.removeItem('token');
|
||||
if (!bowser.safari && !bowser.ios && storage) {
|
||||
storage.removeItem('token');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
resetWebsocket();
|
||||
client.resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -136,3 +133,33 @@ export const checkLogin = () => (dispatch) => {
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// LOGOUT
|
||||
//==============================================================================
|
||||
|
||||
export const logout = () => (dispatch, _, {rest, client, storage}) => {
|
||||
return rest('/auth', {method: 'DELETE'}).then(() => {
|
||||
if (storage) {
|
||||
storage.removeItem('token');
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({type: actions.LOGOUT});
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// AUTH TOKEN
|
||||
//==============================================================================
|
||||
|
||||
export const handleAuthToken = (token) => (dispatch, _, {storage}) => {
|
||||
if (storage) {
|
||||
storage.setItem('exp', jwtDecode(token).exp);
|
||||
storage.setItem('token', token);
|
||||
}
|
||||
dispatch({type: 'HANDLE_AUTH_TOKEN'});
|
||||
};
|
||||
|
||||
|
||||
@@ -14,13 +14,12 @@ import {
|
||||
HIDE_REJECT_USERNAME_DIALOG
|
||||
} from '../constants/community';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const fetchAccounts = (query = {}) => (dispatch) => {
|
||||
export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => {
|
||||
|
||||
dispatch(requestFetchAccounts());
|
||||
coralApi(`/users?${qs.stringify(query)}`)
|
||||
rest(`/users?${qs.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>{
|
||||
dispatch({
|
||||
type: FETCH_COMMENTERS_SUCCESS,
|
||||
@@ -51,18 +50,18 @@ export const newPage = () => ({
|
||||
type: COMMENTERS_NEW_PAGE
|
||||
});
|
||||
|
||||
export const setRole = (id, role) => (dispatch) => {
|
||||
return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_ROLE, id, role});
|
||||
});
|
||||
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) => {
|
||||
return coralApi(`/users/${id}/status`, {method: 'POST', body: {status}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_COMMENTER_STATUS, id, status});
|
||||
});
|
||||
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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import coralApi from 'coral-framework/helpers/request';
|
||||
import * as actions from '../constants/install';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
@@ -27,19 +26,19 @@ const validation = (formData, dispatch, next) => {
|
||||
|
||||
// Required Validation
|
||||
const empty = validKeys
|
||||
.filter((name) => {
|
||||
const cond = !formData[name].length;
|
||||
.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());
|
||||
@@ -93,23 +92,23 @@ const validation = (formData, dispatch, next) => {
|
||||
};
|
||||
|
||||
export const submitSettings = () => (dispatch, getState) => {
|
||||
const settingsFormData = getState().install.toJS().data.settings;
|
||||
const settingsFormData = getState().install.data.settings;
|
||||
validation(settingsFormData, dispatch, function() {
|
||||
dispatch(nextStep());
|
||||
});
|
||||
};
|
||||
|
||||
export const submitUser = () => (dispatch, getState) => {
|
||||
const userFormData = getState().install.toJS().data.user;
|
||||
const userFormData = getState().install.data.user;
|
||||
validation(userFormData, dispatch, function() {
|
||||
dispatch(nextStep());
|
||||
});
|
||||
};
|
||||
|
||||
export const finishInstall = () => (dispatch, getState) => {
|
||||
const data = getState().install.toJS().data;
|
||||
export const finishInstall = () => (dispatch, getState, {rest}) => {
|
||||
const data = getState().install.data;
|
||||
dispatch(installRequest());
|
||||
return coralApi('/setup', {method: 'POST', body: data})
|
||||
return rest('/setup', {method: 'POST', body: data})
|
||||
.then(() => {
|
||||
dispatch(installSuccess());
|
||||
dispatch(nextStep());
|
||||
@@ -129,9 +128,9 @@ 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) => (dispatch) => {
|
||||
export const checkInstall = (next) => (dispatch, _, {rest}) => {
|
||||
dispatch(checkInstallRequest());
|
||||
coralApi('/setup')
|
||||
rest('/setup')
|
||||
.then(({installed}) => {
|
||||
dispatch(checkInstallSuccess(installed));
|
||||
if (installed) {
|
||||
|
||||
@@ -4,15 +4,17 @@ export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open});
|
||||
export const singleView = () => ({type: actions.SINGLE_VIEW});
|
||||
|
||||
// hide shortcuts note
|
||||
export const hideShortcutsNote = () => {
|
||||
export const hideShortcutsNote = () => (dispatch, _, {storage}) => {
|
||||
try {
|
||||
window.localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
if (storage) {
|
||||
storage.setItem('coral:shortcutsNote', 'hide');
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
// above will fail in Safari private mode
|
||||
}
|
||||
|
||||
return {type: actions.HIDE_SHORTCUTS_NOTE};
|
||||
dispatch({type: actions.HIDE_SHORTCUTS_NOTE});
|
||||
};
|
||||
|
||||
export const setSortOrder = (order) => ({
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const SETTINGS_LOADING = 'SETTINGS_LOADING';
|
||||
@@ -14,9 +13,9 @@ export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
|
||||
export const WORDLIST_UPDATED = 'WORDLIST_UPDATED';
|
||||
export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED';
|
||||
|
||||
export const fetchSettings = () => (dispatch) => {
|
||||
export const fetchSettings = () => (dispatch, _, {rest}) => {
|
||||
dispatch({type: SETTINGS_LOADING});
|
||||
coralApi('/settings')
|
||||
rest('/settings')
|
||||
.then((settings) => {
|
||||
dispatch({type: SETTINGS_RECEIVED, settings});
|
||||
})
|
||||
@@ -41,13 +40,13 @@ export const updateDomainlist = (listName, list) => {
|
||||
return {type: DOMAINLIST_UPDATED, listName, list};
|
||||
};
|
||||
|
||||
export const saveSettingsToServer = () => (dispatch, getState) => {
|
||||
let settings = getState().settings.toJS();
|
||||
export const saveSettingsToServer = () => (dispatch, getState, {rest}) => {
|
||||
let settings = getState().settings;
|
||||
if (settings.charCount) {
|
||||
settings.charCount = parseInt(settings.charCount);
|
||||
}
|
||||
dispatch({type: SAVE_SETTINGS_LOADING});
|
||||
coralApi('/settings', {method: 'PUT', body: settings})
|
||||
rest('/settings', {method: 'PUT', body: settings})
|
||||
.then(() => {
|
||||
dispatch({type: SAVE_SETTINGS_SUCCESS, settings});
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import * as userTypes from '../constants/users';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -7,9 +6,9 @@ import t from 'coral-framework/services/i18n';
|
||||
*/
|
||||
// change status of a user
|
||||
export const userStatusUpdate = (status, userId, commentId) => {
|
||||
return (dispatch) => {
|
||||
return (dispatch, _, {rest}) => {
|
||||
dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
|
||||
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
|
||||
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);
|
||||
@@ -21,8 +20,8 @@ export const userStatusUpdate = (status, userId, commentId) => {
|
||||
|
||||
// change status of a user
|
||||
export const sendNotificationEmail = (userId, subject, body) => {
|
||||
return (dispatch) => {
|
||||
return coralApi(`/users/${userId}/email`, {method: 'POST', body: {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();
|
||||
@@ -33,8 +32,8 @@ export const sendNotificationEmail = (userId, subject, body) => {
|
||||
|
||||
// let a user edit their username
|
||||
export const enableUsernameEdit = (userId) => {
|
||||
return (dispatch) => {
|
||||
return coralApi(`/users/${userId}/username-enable`, {method: 'POST'})
|
||||
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();
|
||||
|
||||
@@ -69,23 +69,23 @@ class AdminLogin extends React.Component {
|
||||
);
|
||||
const requestPasswordForm = (
|
||||
this.props.passwordRequestSuccess
|
||||
? <p className={styles.passwordRequestSuccess} onClick={() => {
|
||||
location.href = location.href;
|
||||
}}>
|
||||
? <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>
|
||||
: <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}>
|
||||
|
||||
@@ -8,18 +8,16 @@ export default ({suspectWords, bannedWords, body, ...rest}) => {
|
||||
const links = linkify.getMatches(body);
|
||||
const linkText = links ? links.map((link) => link.raw) : [];
|
||||
|
||||
// since words are checked against word boundaries on the backend,
|
||||
// should be the behavior on the front end as well.
|
||||
// currently the highlighter plugin does not support out of the box.
|
||||
const searchWords = [...suspectWords, ...bannedWords]
|
||||
.filter((w) => {
|
||||
return new RegExp(`(^|\\s)${w}(\\s|$)`, 'i').test(body);
|
||||
})
|
||||
.concat(linkText);
|
||||
const searchWords = [
|
||||
...suspectWords,
|
||||
...bannedWords,
|
||||
...linkText
|
||||
];
|
||||
|
||||
return (
|
||||
<Highlighter
|
||||
{...rest}
|
||||
autoEscape={true}
|
||||
searchWords={searchWords}
|
||||
textToHighlight={body}
|
||||
/>
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
color: white;
|
||||
background: grey;
|
||||
box-sizing: border-box;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
padding: 2px 5px;
|
||||
font-size: 12px;
|
||||
height: 28px;
|
||||
|
||||
height: 24px;
|
||||
letter-spacing: 0.4px;
|
||||
line-height: 22px;
|
||||
|
||||
> i {
|
||||
font-size: 14px;
|
||||
vertical-align: text-top;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.flagBox {
|
||||
border-top: 1px solid rgba(66, 66, 66, 0.12);
|
||||
|
||||
margin-top: 10px;
|
||||
.container {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
@@ -61,9 +61,11 @@ class FlagBox extends Component {
|
||||
<ul>
|
||||
{actionList.map((action, j) =>
|
||||
<li key={`${i}_${j}`} className={styles.subDetail}>
|
||||
<a className={styles.username} onClick={() => viewUserDetail(action.user.id)}>
|
||||
{action.user.username}
|
||||
</a>
|
||||
{action.user &&
|
||||
<a className={styles.username} onClick={() => viewUserDetail(action.user.id)}>
|
||||
{action.user.username}
|
||||
</a>
|
||||
}
|
||||
{action.message}
|
||||
</li>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
.loadMoreContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loadMore {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #FFF;
|
||||
max-width: 660px;
|
||||
margin-bottom: 30px;
|
||||
background-color: #2376D8;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.loadMore:hover {
|
||||
background-color: #4399FF;
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -1,9 +1,10 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Button} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
import styles from './LoadMore.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
const LoadMore = ({loadMore, showLoadMore}) =>
|
||||
<div className={styles.loadMoreContainer}>
|
||||
const LoadMore = ({loadMore, showLoadMore, className, ...rest}) =>
|
||||
<div {...rest} className={cn(className, styles.loadMoreContainer)}>
|
||||
{
|
||||
showLoadMore && <Button
|
||||
className={styles.loadMore}
|
||||
@@ -186,7 +186,6 @@
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.minimal {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import {Badge} from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ReplyBadge = () => (
|
||||
<Badge icon="reply">
|
||||
{t('modqueue.reply')}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
export default ReplyBadge;
|
||||
@@ -65,7 +65,7 @@ class SuspendUserDialog extends React.Component {
|
||||
{t('suspenduser.title_suspend')}
|
||||
</h1>
|
||||
<p className={styles.description}>
|
||||
{t('suspenduser.description_suspend', username)}
|
||||
{t('suspenduser.description_suspend', username)}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>{t('suspenduser.select_duration')}</legend>
|
||||
@@ -103,7 +103,7 @@ class SuspendUserDialog extends React.Component {
|
||||
{t('suspenduser.title_notify')}
|
||||
</h1>
|
||||
<p className={styles.description}>
|
||||
{t('suspenduser.description_notify', username)}
|
||||
{t('suspenduser.description_notify', username)}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>{t('suspenduser.write_message')}</legend>
|
||||
|
||||
@@ -1,6 +1,78 @@
|
||||
.copyButton {
|
||||
float: right;
|
||||
top: -10px;
|
||||
background-color: white;
|
||||
border: solid 1px;
|
||||
padding: 2px 6px;
|
||||
height: auto;
|
||||
line-height: initial;
|
||||
min-width: auto;
|
||||
letter-spacing: normal;
|
||||
font-size: 0.9em;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.userDetailList {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.userDetailItem {
|
||||
margin: 0 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
margin: 15px 0 5px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.stat {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.stat:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.statItem, .statReportResult {
|
||||
padding: 3px 5px;
|
||||
background-color: #D8D8D8;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.4px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.statResult {
|
||||
font-size: 1.5em;
|
||||
padding: 5px 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.statReportResult {
|
||||
color: white;
|
||||
margin: 5px 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.statReportResult.reliable {
|
||||
background-color: #749C48;
|
||||
}
|
||||
|
||||
.statReportResult.neutral {
|
||||
background-color: #616161;
|
||||
}
|
||||
|
||||
.statReportResult.unreliable {
|
||||
background-color: #F44336;
|
||||
}
|
||||
|
||||
.memberSince {
|
||||
@@ -8,27 +80,9 @@
|
||||
}
|
||||
|
||||
.small {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
|
||||
.stat {
|
||||
margin: 0 4px 10px 0px;
|
||||
}
|
||||
|
||||
.stat:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stat p:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
color: #888888;
|
||||
font-size: 0.9em;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.profileEmail {
|
||||
@@ -79,3 +133,12 @@
|
||||
margin-left: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.loadMore > button {
|
||||
background-color: #696969;
|
||||
|
||||
&:hover {
|
||||
background-color: #404040;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Comment from './UserDetailComment';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Comment from '../containers/UserDetailComment';
|
||||
import styles from './UserDetail.css';
|
||||
import {Button, Drawer, Spinner} from 'coral-ui';
|
||||
import {Icon, Button, Drawer, Spinner} from 'coral-ui';
|
||||
import {Slot} from 'coral-framework/components';
|
||||
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
|
||||
import {actionsMap} from '../utils/moderationQueueActionsMap';
|
||||
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';
|
||||
|
||||
export default class UserDetail extends React.Component {
|
||||
|
||||
@@ -55,11 +60,12 @@ export default class UserDetail extends React.Component {
|
||||
|
||||
renderLoaded() {
|
||||
const {
|
||||
root,
|
||||
root: {
|
||||
user,
|
||||
totalComments,
|
||||
rejectedComments,
|
||||
comments: {nodes}
|
||||
comments: {nodes, hasNextPage}
|
||||
},
|
||||
activeTab,
|
||||
selectedCommentIds,
|
||||
@@ -70,15 +76,9 @@ export default class UserDetail extends React.Component {
|
||||
bulkReject,
|
||||
hideUserDetail,
|
||||
viewUserDetail,
|
||||
loadMore,
|
||||
} = this.props;
|
||||
|
||||
const localProfile = user.profiles.find((p) => p.provider === 'local');
|
||||
|
||||
let profile;
|
||||
if (localProfile) {
|
||||
profile = localProfile.id;
|
||||
}
|
||||
|
||||
let rejectedPercent = (rejectedComments / totalComments) * 100;
|
||||
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
|
||||
|
||||
@@ -92,57 +92,76 @@ export default class UserDetail extends React.Component {
|
||||
<h3>{user.username}</h3>
|
||||
|
||||
<div>
|
||||
{profile && <input className={styles.profileEmail} readOnly type="text" ref={(ref) => this.profile = ref} value={profile} />}
|
||||
<ButtonCopyToClipboard className={styles.copyButton} copyText={profile} />
|
||||
<ul className={styles.userDetailList}>
|
||||
<li>
|
||||
<Icon name="assignment_ind"/>
|
||||
<span className={styles.userDetailItem}>Member Since:</span>
|
||||
{new Date(user.created_at).toLocaleString()}
|
||||
</li>
|
||||
|
||||
{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} />
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<ul className={styles.stats}>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}> Total Comments </span>
|
||||
<spam className={styles.statResult}> {totalComments} </spam>
|
||||
</li>
|
||||
<li className={styles.stat}>
|
||||
<spam className={styles.statItem}> Reject Rate </spam>
|
||||
<spam className={styles.statResult}> {`${(rejectedPercent).toFixed(1)}%`} </spam>
|
||||
</li>
|
||||
<li className={styles.stat}>
|
||||
<spam className={styles.statItem}> Reports </spam>
|
||||
<spam className={cn(styles.statReportResult, styles[getReliability(user.reliable.flagger)])}>
|
||||
{capitalize(getReliability(user.reliable.flagger))}
|
||||
</spam>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p className={styles.small}>
|
||||
Data represents the last six months of activity
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Slot
|
||||
fill="userProfile"
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
user={user}
|
||||
queryData={root, user}
|
||||
/>
|
||||
<p className={styles.memberSince}><strong>Member since</strong> {new Date(user.created_at).toLocaleString()}</p>
|
||||
|
||||
<hr/>
|
||||
<p>
|
||||
<strong>Account summary</strong>
|
||||
<br/><small className={styles.small}>Data represents the last six months of activity</small>
|
||||
</p>
|
||||
<div className={styles.stats}>
|
||||
<div className={styles.stat}>
|
||||
<p>Total Comments</p>
|
||||
<p>{totalComments}</p>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<p>Reject Rate</p>
|
||||
<p>{`${(rejectedPercent).toFixed(1)}%`}</p>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
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}>
|
||||
<Button
|
||||
onClick={bulkAccept}
|
||||
className={styles.bulkAction}
|
||||
cStyle='approve'
|
||||
icon='done'>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={bulkReject}
|
||||
className={styles.bulkAction}
|
||||
cStyle='reject'
|
||||
icon='close'>
|
||||
</Button>
|
||||
{`${selectedCommentIds.length} comments selected`}
|
||||
</div>
|
||||
)
|
||||
? (
|
||||
<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}>
|
||||
<Button
|
||||
onClick={bulkAccept}
|
||||
className={styles.bulkAction}
|
||||
cStyle='approve'
|
||||
icon='done'>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={bulkReject}
|
||||
className={styles.bulkAction}
|
||||
cStyle='reject'
|
||||
icon='close'>
|
||||
</Button>
|
||||
{`${selectedCommentIds.length} comments selected`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div>
|
||||
@@ -167,6 +186,11 @@ export default class UserDetail extends React.Component {
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<LoadMore
|
||||
className={styles.loadMore}
|
||||
loadMore={loadMore}
|
||||
showLoadMore={hasNextPage}
|
||||
/>
|
||||
</Drawer>
|
||||
</ClickOutside>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.root:last-child {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.rootSelected {
|
||||
background-color: #ecf4ff;
|
||||
}
|
||||
@@ -51,7 +55,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.commentType {
|
||||
.badgeBar {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {Link} from 'react-router';
|
||||
|
||||
import {Icon} from 'coral-ui';
|
||||
import FlagBox from './FlagBox';
|
||||
import ReplyBadge from './ReplyBadge';
|
||||
import styles from './UserDetailComment.css';
|
||||
import CommentType from './CommentType';
|
||||
import {getActionSummary} from 'coral-framework/utils';
|
||||
@@ -53,10 +54,14 @@ class UserDetailComment extends React.Component {
|
||||
</span>
|
||||
{
|
||||
(comment.editing && comment.editing.edited)
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
: null
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
: null
|
||||
}
|
||||
<CommentType type={commentType} className={styles.commentType}/>
|
||||
|
||||
<div className={styles.badgeBar}>
|
||||
{comment.hasParent && <ReplyBadge/>}
|
||||
<CommentType type={commentType}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.story}>
|
||||
Story: {comment.asset.title}
|
||||
@@ -116,10 +121,10 @@ class UserDetailComment extends React.Component {
|
||||
</div>
|
||||
{flagActions && flagActions.length
|
||||
? <FlagBox
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
: null}
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -13,9 +13,9 @@ const CoralHeader = ({
|
||||
}) => (
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{
|
||||
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<div>
|
||||
{
|
||||
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
id='dashboardNav'
|
||||
@@ -63,36 +63,36 @@ const CoralHeader = ({
|
||||
</Navigation>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={styles.settings}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<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">
|
||||
}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={styles.settings}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<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">
|
||||
View latest version
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank">
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank">
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
|
||||
|
||||
@@ -10,16 +10,18 @@
|
||||
.logo span {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
font-size: 18px;
|
||||
font-size: 26px;
|
||||
vertical-align: middle;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.logo {
|
||||
background: #E5E5E5;
|
||||
background: #696969;
|
||||
height: 100%;
|
||||
width: 128px;
|
||||
z-index: 10;
|
||||
border-right: 1px #757575 solid;
|
||||
}
|
||||
|
||||
.base {
|
||||
|
||||
@@ -3,12 +3,11 @@ import {connect} from 'react-redux';
|
||||
import Layout from '../components/ui/Layout';
|
||||
import {fetchConfig} from '../actions/config';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
import {logout} from 'coral-framework/actions/auth';
|
||||
import {FullLoading} from '../components/FullLoading';
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
|
||||
import {checkLogin, handleLogin, requestPasswordReset} from '../actions/auth';
|
||||
import {checkLogin, handleLogin, requestPasswordReset, logout} from '../actions/auth';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import UserDetail from 'coral-admin/src/containers/UserDetail';
|
||||
|
||||
@@ -56,10 +55,10 @@ class LayoutContainer extends Component {
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
{...this.props}
|
||||
>
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<UserDetail />
|
||||
{this.props.children}
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<UserDetail />
|
||||
{this.props.children}
|
||||
</Layout>
|
||||
);
|
||||
} else if (loggedIn) {
|
||||
@@ -74,10 +73,8 @@ class LayoutContainer extends Component {
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth.toJS(),
|
||||
TALK_RECAPTCHA_PUBLIC: state.config
|
||||
.get('data')
|
||||
.get('TALK_RECAPTCHA_PUBLIC', null)
|
||||
auth: state.auth,
|
||||
TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
@@ -5,22 +5,24 @@ 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 * as notification from 'coral-admin/src/services/notification';
|
||||
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';
|
||||
|
||||
class SuspendUserDialogContainer extends Component {
|
||||
|
||||
suspendUser = async ({message, until}) => {
|
||||
const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser} = this.props;
|
||||
const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser, notify} = this.props;
|
||||
hideSuspendUserDialog();
|
||||
try {
|
||||
const result = await suspendUser({id: userId, message, until});
|
||||
if (result.data.suspendUser.errors) {
|
||||
throw result.data.suspendUser.errors;
|
||||
}
|
||||
notification.success(
|
||||
notify(
|
||||
'success',
|
||||
t('suspenduser.notify_suspend_until', username, timeago(until)),
|
||||
);
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
@@ -33,7 +35,7 @@ class SuspendUserDialogContainer extends Component {
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
notification.showMutationErrors(err);
|
||||
notify('error', getErrorMessages(err));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,8 +54,9 @@ class SuspendUserDialogContainer extends Component {
|
||||
|
||||
const withOrganizationName = withQuery(gql`
|
||||
query CoralAdmin_SuspendUserDialog {
|
||||
__typename
|
||||
settings {
|
||||
organizationName
|
||||
organizationName
|
||||
}
|
||||
}
|
||||
`);
|
||||
@@ -69,6 +72,7 @@ const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId,
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
hideSuspendUserDialog,
|
||||
notify,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from 'coral-admin/src/actions/userDetail';
|
||||
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
import UserDetailComment from './UserDetailComment';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
const commentConnectionFragment = gql`
|
||||
fragment CoralAdmin_Moderation_CommentConnection on CommentConnection {
|
||||
@@ -32,6 +33,7 @@ const slots = [
|
||||
];
|
||||
|
||||
class UserDetailContainer extends React.Component {
|
||||
isLoadingMore = false;
|
||||
|
||||
// status can be 'ACCEPTED' or 'REJECTED'
|
||||
bulkSetCommentStatus = (status) => {
|
||||
@@ -40,7 +42,6 @@ class UserDetailContainer extends React.Component {
|
||||
});
|
||||
|
||||
Promise.all(changes).then(() => {
|
||||
this.props.data.refetch(); // some comments may have moved out of this tab
|
||||
this.props.clearUserDetailSelections(); // un-select everything
|
||||
});
|
||||
}
|
||||
@@ -61,12 +62,53 @@ class UserDetailContainer extends React.Component {
|
||||
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
if (this.isLoadingMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoadingMore = true;
|
||||
const variables = {
|
||||
limit: 10,
|
||||
cursor: this.props.root.comments.endCursor,
|
||||
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},
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoadingMore = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.isLoadingMore = false;
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
if (this.props.userId === null && next.userId) {
|
||||
next.data.refetch();
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
if (!this.props.userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const loading = !('user' in this.props.root) || this.props.root.user.id !== this.props.userId;
|
||||
const loading = this.props.data.loading;
|
||||
|
||||
return <UserDetail
|
||||
bulkReject={this.bulkReject}
|
||||
@@ -76,10 +118,20 @@ class UserDetailContainer extends React.Component {
|
||||
acceptComment={this.acceptComment}
|
||||
rejectComment={this.rejectComment}
|
||||
loading={loading}
|
||||
loadMore={this.loadMore}
|
||||
{...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
${commentConnectionFragment}
|
||||
`;
|
||||
|
||||
export const withUserDetailQuery = withQuery(gql`
|
||||
query CoralAdmin_UserDetail($author_id: ID!, $statuses: [COMMENT_STATUS!]) {
|
||||
user(id: $author_id) {
|
||||
@@ -90,6 +142,9 @@ export const withUserDetailQuery = withQuery(gql`
|
||||
id
|
||||
provider
|
||||
}
|
||||
reliable {
|
||||
flagger
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'user')}
|
||||
}
|
||||
totalComments: commentCount(query: {author_id: $author_id})
|
||||
@@ -117,8 +172,8 @@ const mapStateToProps = (state) => ({
|
||||
selectedCommentIds: state.userDetail.selectedCommentIds,
|
||||
statuses: state.userDetail.statuses,
|
||||
activeTab: state.userDetail.activeTab,
|
||||
bannedWords: state.settings.toJS().wordlist.banned,
|
||||
suspectWords: state.settings.toJS().wordlist.suspect,
|
||||
bannedWords: state.settings.wordlist.banned,
|
||||
suspectWords: state.settings.wordlist.suspect,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
@@ -9,6 +9,7 @@ export default withFragments({
|
||||
body
|
||||
created_at
|
||||
status
|
||||
hasParent
|
||||
asset {
|
||||
id
|
||||
title
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
import {add} from 'coral-framework/services/graphqlRegistry';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
const extension = {
|
||||
export default {
|
||||
mutations: {
|
||||
SetUserStatus: () => ({
|
||||
refetchQueries: ['CoralAdmin_Community'],
|
||||
SetUserStatus: ({variables: {status, userId}}) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_FlaggedAccounts: (prev) => {
|
||||
if (status !== 'APPROVED') {
|
||||
return prev;
|
||||
}
|
||||
const updated = update(prev, {
|
||||
users: {
|
||||
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
}),
|
||||
RejectUsername: () => ({
|
||||
refetchQueries: ['CoralAdmin_Community'],
|
||||
RejectUsername: ({variables: {input: {id: userId}}}) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_FlaggedAccounts: (prev) => {
|
||||
const updated = update(prev, {
|
||||
users: {
|
||||
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
add(extension);
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import {ApolloProvider} from 'react-apollo';
|
||||
import smoothscroll from 'smoothscroll-polyfill';
|
||||
import EventEmitter from 'eventemitter2';
|
||||
import EventEmitterProvider from 'coral-framework/components/EventEmitterProvider';
|
||||
|
||||
import {getClient} from 'coral-framework/services/client';
|
||||
import store from './services/store';
|
||||
|
||||
import TalkProvider from 'coral-framework/components/TalkProvider';
|
||||
import {createContext} from 'coral-framework/services/bootstrap';
|
||||
import reducers from './reducers';
|
||||
import App from './components/App';
|
||||
|
||||
import 'react-mdl/extra/material.js';
|
||||
import './graphql';
|
||||
import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins';
|
||||
import graphqlExtension from './graphql';
|
||||
import pluginsConfig from 'pluginsConfig';
|
||||
import {toast} from 'react-toastify';
|
||||
import {createNotificationService} from './services/notification';
|
||||
import {hideShortcutsNote} from './actions/moderation';
|
||||
|
||||
const eventEmitter = new EventEmitter();
|
||||
function hidrateStore({store, storage}) {
|
||||
if (storage && storage.getItem('coral:shortcutsNote') === 'hide') {
|
||||
store.dispatch(hideShortcutsNote());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: pass redux actions through the emitter.
|
||||
|
||||
loadPluginsTranslations();
|
||||
injectPluginsReducers();
|
||||
smoothscroll.polyfill();
|
||||
|
||||
const notification = createNotificationService(toast);
|
||||
const context = createContext({reducers, graphqlExtension, pluginsConfig, notification});
|
||||
|
||||
// hidrate Store with external data.
|
||||
hidrateStore(context);
|
||||
|
||||
render(
|
||||
<EventEmitterProvider eventEmitter={eventEmitter}>
|
||||
<ApolloProvider client={getClient()} store={store}>
|
||||
<App />
|
||||
</ApolloProvider>
|
||||
</EventEmitterProvider>
|
||||
<TalkProvider {...context}>
|
||||
<App />
|
||||
</TalkProvider>
|
||||
, document.querySelector('#root')
|
||||
);
|
||||
|
||||
@@ -1,35 +1,40 @@
|
||||
import {Map, List, fromJS} from 'immutable';
|
||||
import * as actions from '../constants/assets';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
const initialState = Map({
|
||||
byId: Map(),
|
||||
ids: List(),
|
||||
assets: List()
|
||||
});
|
||||
const initialState = {
|
||||
byId: {},
|
||||
ids: [],
|
||||
assets: []
|
||||
};
|
||||
|
||||
export default function assets (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.FETCH_ASSETS_SUCCESS:
|
||||
return replaceAssets(action, state);
|
||||
case actions.FETCH_ASSETS_SUCCESS: {
|
||||
const assets = action.assets.reduce((prev, curr) => {
|
||||
prev[curr.id] = curr;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
return update(state, {
|
||||
byId: {$set: assets},
|
||||
count: {$set: action.count},
|
||||
ids: {$set: Object.keys(assets)},
|
||||
});
|
||||
}
|
||||
case actions.UPDATE_ASSET_STATE_REQUEST:
|
||||
return state
|
||||
.setIn(['byId', action.id, 'closedAt'], action.closedAt);
|
||||
return update(state, {
|
||||
byId: {
|
||||
[action.id]: {
|
||||
closedAt: {$set: action.closedAt},
|
||||
},
|
||||
},
|
||||
});
|
||||
case actions.UPDATE_ASSETS:
|
||||
return state
|
||||
.set('assets', List(action.assets));
|
||||
return update(state, {
|
||||
assets: {$set: action.assets},
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
const replaceAssets = (action, state) => {
|
||||
const assets = fromJS(action.assets.reduce((prev, curr) => {
|
||||
prev[curr.id] = curr;
|
||||
return prev;
|
||||
}, {}));
|
||||
|
||||
return state
|
||||
.set('byId', assets)
|
||||
.set('count', action.count)
|
||||
.set('ids', List(assets.keys()));
|
||||
};
|
||||
|
||||
@@ -1,43 +1,63 @@
|
||||
import {Map} from 'immutable';
|
||||
import * as actions from '../constants/auth';
|
||||
|
||||
const initialState = Map({
|
||||
const initialState = {
|
||||
loggedIn: false,
|
||||
user: null,
|
||||
loginError: null,
|
||||
loginMaxExceeded: false,
|
||||
passwordRequestSuccess: null
|
||||
});
|
||||
};
|
||||
|
||||
export default function auth (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CHECK_LOGIN_REQUEST:
|
||||
return state
|
||||
.set('loadingUser', true);
|
||||
return {
|
||||
...state,
|
||||
loadingUser: true,
|
||||
};
|
||||
case actions.CHECK_LOGIN_FAILURE:
|
||||
return state
|
||||
.set('loggedIn', false)
|
||||
.set('loadingUser', false)
|
||||
.set('user', null);
|
||||
return {
|
||||
...state,
|
||||
loggedIn: false,
|
||||
loadingUser: false,
|
||||
user: null,
|
||||
};
|
||||
case actions.CHECK_LOGIN_SUCCESS:
|
||||
return state
|
||||
.set('loggedIn', true)
|
||||
.set('loadingUser', false)
|
||||
.set('user', action.user);
|
||||
return {
|
||||
...state,
|
||||
loggedIn: true,
|
||||
loadingUser: false,
|
||||
user: action.user,
|
||||
};
|
||||
case actions.LOGOUT:
|
||||
return initialState;
|
||||
case actions.LOGIN_SUCCESS:
|
||||
return state.set('loginMaxExceeded', false).set('loginError', null);
|
||||
return {
|
||||
...state,
|
||||
loginMaxExceeded: false,
|
||||
loginError: null,
|
||||
};
|
||||
case actions.LOGIN_FAILURE:
|
||||
return state.set('loginError', action.message);
|
||||
return {
|
||||
...state,
|
||||
loginError: action.message,
|
||||
};
|
||||
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
|
||||
return state.set('passwordRequestSuccess', null);
|
||||
return {
|
||||
...state,
|
||||
passwordRequestSuccess: null,
|
||||
};
|
||||
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
|
||||
return state.set('passwordRequestSuccess', 'If you have a registered account, a password reset link was sent to that email.');
|
||||
return {
|
||||
...state,
|
||||
passwordRequestSuccess: 'If you have a registered account, a password reset link was sent to that email.',
|
||||
};
|
||||
case actions.LOGIN_MAXIMUM_EXCEEDED:
|
||||
return state
|
||||
.set('loginMaxExceeded', true)
|
||||
.set('loginError', action.message);
|
||||
return {
|
||||
...state,
|
||||
loginMaxExceeded: true,
|
||||
loginError: action.message,
|
||||
};
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import {Map} from 'immutable';
|
||||
|
||||
import {
|
||||
FETCH_COMMENTERS_REQUEST,
|
||||
FETCH_COMMENTERS_FAILURE,
|
||||
@@ -13,8 +11,8 @@ import {
|
||||
HIDE_REJECT_USERNAME_DIALOG
|
||||
} from '../constants/community';
|
||||
|
||||
const initialState = Map({
|
||||
community: Map(),
|
||||
const initialState = {
|
||||
community: {},
|
||||
isFetchingPeople: false,
|
||||
errorPeople: '',
|
||||
accounts: [],
|
||||
@@ -22,72 +20,87 @@ const initialState = Map({
|
||||
ascPeople: false,
|
||||
totalPagesPeople: 0,
|
||||
pagePeople: 0,
|
||||
user: Map({}),
|
||||
user: {},
|
||||
banDialog: false,
|
||||
rejectUsernameDialog: false
|
||||
});
|
||||
};
|
||||
|
||||
export default function community (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case FETCH_COMMENTERS_REQUEST :
|
||||
return state
|
||||
.set('isFetchingPeople', true);
|
||||
return {
|
||||
...state,
|
||||
isFetchingPeople: true,
|
||||
};
|
||||
case FETCH_COMMENTERS_FAILURE :
|
||||
return state
|
||||
.set('isFetchingPeople', false)
|
||||
.set('errorPeople', action.error);
|
||||
|
||||
return {
|
||||
...state,
|
||||
isFetchingPeople: false,
|
||||
errorPeople: action.error,
|
||||
};
|
||||
case FETCH_COMMENTERS_SUCCESS : {
|
||||
const {accounts, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line
|
||||
return state
|
||||
.merge({
|
||||
isFetchingPeople: false,
|
||||
errorPeople: '',
|
||||
pagePeople: page,
|
||||
countPeople: count,
|
||||
limitPeople: limit,
|
||||
totalPagesPeople: totalPages,
|
||||
...rest
|
||||
})
|
||||
.set('accounts', accounts); // Sets to normal array
|
||||
return {
|
||||
...state,
|
||||
isFetchingPeople: false,
|
||||
errorPeople: '',
|
||||
pagePeople: page,
|
||||
countPeople: count,
|
||||
limitPeople: limit,
|
||||
totalPagesPeople: totalPages,
|
||||
...rest,
|
||||
accounts, // Sets to normal array
|
||||
};
|
||||
}
|
||||
case SET_ROLE : {
|
||||
const commenters = state.get('accounts');
|
||||
const commenters = state.accounts;
|
||||
const idx = commenters.findIndex((el) => el.id === action.id);
|
||||
|
||||
commenters[idx].roles[0] = action.role;
|
||||
return state.set('accounts', commenters.map((id) => id));
|
||||
return {
|
||||
...state,
|
||||
accounts: commenters.map((id) => id),
|
||||
};
|
||||
}
|
||||
case SET_COMMENTER_STATUS: {
|
||||
const commenters = state.get('accounts');
|
||||
const commenters = state.accounts;
|
||||
const idx = commenters.findIndex((el) => el.id === action.id);
|
||||
|
||||
commenters[idx].status = action.status;
|
||||
return state.set('accounts', commenters.map((id) => id));
|
||||
return {
|
||||
...state,
|
||||
accounts: commenters.map((id) => id),
|
||||
};
|
||||
|
||||
}
|
||||
case SORT_UPDATE :
|
||||
return state
|
||||
.set('fieldPeople', action.sort.field)
|
||||
.set('ascPeople', !state.get('ascPeople'));
|
||||
return {
|
||||
...state,
|
||||
fieldPeople: action.sort.field,
|
||||
ascPeople: !state.ascPeople,
|
||||
};
|
||||
case HIDE_BANUSER_DIALOG:
|
||||
return state
|
||||
.set('banDialog', false);
|
||||
return {
|
||||
...state,
|
||||
banDialog: false,
|
||||
};
|
||||
case SHOW_BANUSER_DIALOG:
|
||||
return state
|
||||
.merge({
|
||||
user: Map(action.user),
|
||||
banDialog: true
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
user: action.user,
|
||||
banDialog: true,
|
||||
};
|
||||
case HIDE_REJECT_USERNAME_DIALOG:
|
||||
return state
|
||||
.set('rejectUsernameDialog', false);
|
||||
return {
|
||||
...state,
|
||||
rejectUsernameDialog: false,
|
||||
};
|
||||
case SHOW_REJECT_USERNAME_DIALOG:
|
||||
return state
|
||||
.merge({
|
||||
user: Map(action.user),
|
||||
rejectUsernameDialog: true
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
user: action.user,
|
||||
rejectUsernameDialog: true
|
||||
};
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import {Map} from 'immutable';
|
||||
|
||||
import * as actions from '../actions/config';
|
||||
|
||||
const initialState = Map({
|
||||
data: Map({})
|
||||
});
|
||||
const initialState = {
|
||||
data: {}
|
||||
};
|
||||
|
||||
export default function config (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CONFIG_UPDATED:
|
||||
return state.set('data', Map(action.data));
|
||||
return {
|
||||
...state,
|
||||
data: action.data,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
import {Map, List} from 'immutable';
|
||||
|
||||
import * as actions from '../constants/install';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
const initialState = Map({
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
data: Map({
|
||||
settings: Map({
|
||||
data: {
|
||||
settings: {
|
||||
organizationName: '',
|
||||
domains: Map({
|
||||
whitelist: List()
|
||||
})
|
||||
}),
|
||||
user: Map({
|
||||
domains: {
|
||||
whitelist: [],
|
||||
}
|
||||
},
|
||||
user: {
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
}),
|
||||
errors: Map({
|
||||
}
|
||||
},
|
||||
errors: {
|
||||
organizationName: '',
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
}),
|
||||
},
|
||||
showErrors: false,
|
||||
hasError: false,
|
||||
error: null,
|
||||
@@ -44,57 +43,91 @@ const initialState = Map({
|
||||
installRequest: null,
|
||||
installRequestError: null,
|
||||
alreadyInstalled: false
|
||||
});
|
||||
};
|
||||
|
||||
export default function install (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.NEXT_STEP:
|
||||
return state
|
||||
.set('step', state.get('step') + 1);
|
||||
return {
|
||||
...state,
|
||||
step: state.step + 1,
|
||||
};
|
||||
case actions.PREVIOUS_STEP:
|
||||
return state
|
||||
.set('step', state.get('step') - 1);
|
||||
return {
|
||||
...state,
|
||||
step: state.step - 1,
|
||||
};
|
||||
case actions.GO_TO_STEP:
|
||||
return state
|
||||
.set('step', action.step);
|
||||
return {
|
||||
...state,
|
||||
step: action.step,
|
||||
};
|
||||
case actions.UPDATE_PERMITTED_DOMAINS_SETTINGS:
|
||||
return state
|
||||
.setIn(['data', 'settings', 'domains', 'whitelist'], action.value);
|
||||
return update(state, {
|
||||
data: {
|
||||
settings: {
|
||||
domains: {
|
||||
whitelist: {$set: action.value},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
case actions.UPDATE_FORMDATA_SETTINGS:
|
||||
return state
|
||||
.setIn(['data', 'settings', action.name], action.value);
|
||||
return update(state, {
|
||||
data: {
|
||||
settings: {
|
||||
[action.name]: {$set: action.value},
|
||||
},
|
||||
},
|
||||
});
|
||||
case actions.UPDATE_FORMDATA_USER:
|
||||
return state
|
||||
.setIn(['data', 'user', action.name], action.value);
|
||||
return update(state, {
|
||||
data: {
|
||||
user: {
|
||||
[action.name]: {$set: action.value},
|
||||
},
|
||||
},
|
||||
});
|
||||
case actions.HAS_ERROR:
|
||||
return state
|
||||
.merge({
|
||||
hasError: true,
|
||||
showErrors: true
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
hasError: true,
|
||||
showErrors: true,
|
||||
};
|
||||
case actions.ADD_ERROR:
|
||||
return state
|
||||
.setIn(['errors', action.name], action.error);
|
||||
return update(state, {
|
||||
errors: {
|
||||
[action.name]: {$set: action.error},
|
||||
},
|
||||
});
|
||||
case actions.CLEAR_ERRORS:
|
||||
return state
|
||||
.set('errors', Map());
|
||||
return {
|
||||
...state,
|
||||
errors: {},
|
||||
};
|
||||
case actions.INSTALL_REQUEST:
|
||||
return state
|
||||
.set('isLoading', true);
|
||||
return {
|
||||
...state,
|
||||
isLoading: true,
|
||||
};
|
||||
case actions.INSTALL_SUCCESS:
|
||||
return state
|
||||
.set('isLoading', false)
|
||||
.set('installRequest', 'SUCCESS');
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
installRequest: 'SUCCESS',
|
||||
};
|
||||
case actions.INSTALL_FAILURE:
|
||||
return state
|
||||
.merge({
|
||||
isLoading: false,
|
||||
installRequest: 'FAILURE',
|
||||
installRequestError: action.error
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
installRequest: 'FAILURE',
|
||||
installRequestError: action.error
|
||||
};
|
||||
case actions.CHECK_INSTALL_SUCCESS:
|
||||
return state
|
||||
.set('alreadyInstalled', action.installed);
|
||||
return {
|
||||
...state,
|
||||
alreadyInstalled: action.installed,
|
||||
};
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,57 @@
|
||||
import {fromJS} from 'immutable';
|
||||
import * as actions from '../constants/moderation';
|
||||
|
||||
const initialState = fromJS({
|
||||
const initialState = {
|
||||
singleView: false,
|
||||
modalOpen: false,
|
||||
storySearchVisible: false,
|
||||
storySearchString: '',
|
||||
shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show',
|
||||
sortOrder: 'REVERSE_CHRONOLOGICAL',
|
||||
});
|
||||
shortcutsNoteVisible: 'show',
|
||||
sortOrder: 'DESC',
|
||||
};
|
||||
|
||||
export default function moderation (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.MODERATION_CLEAR_STATE:
|
||||
return initialState;
|
||||
return {
|
||||
...initialState,
|
||||
shortcutsNoteVisible: state.shortcutsNoteVisible,
|
||||
};
|
||||
case actions.TOGGLE_MODAL:
|
||||
return state
|
||||
.set('modalOpen', action.open);
|
||||
return {
|
||||
...state,
|
||||
modalOpen: action.open,
|
||||
};
|
||||
case actions.SINGLE_VIEW:
|
||||
return state
|
||||
.set('singleView', !state.get('singleView'));
|
||||
return {
|
||||
...state,
|
||||
singleView: !state.singleView,
|
||||
};
|
||||
case actions.HIDE_SHORTCUTS_NOTE:
|
||||
return state
|
||||
.set('shortcutsNoteVisible', 'hide');
|
||||
return {
|
||||
...state,
|
||||
shortcutsNoteVisible: 'hide',
|
||||
};
|
||||
case actions.SHOW_STORY_SEARCH:
|
||||
return state.set('storySearchVisible', true);
|
||||
return {
|
||||
...state,
|
||||
storySearchVisible: true,
|
||||
};
|
||||
case actions.HIDE_STORY_SEARCH:
|
||||
return state.set('storySearchVisible', false);
|
||||
return {
|
||||
...state,
|
||||
storySearchVisible: false,
|
||||
};
|
||||
case actions.STORY_SEARCH_CHANGE_VALUE:
|
||||
return state.set('storySearchString', action.value);
|
||||
return {
|
||||
...state,
|
||||
storySearchString: action.value,
|
||||
};
|
||||
case actions.SET_SORT_ORDER:
|
||||
return state.set('sortOrder', action.order);
|
||||
default :
|
||||
return {
|
||||
...state,
|
||||
sortOrder: action.order,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Map, List} from 'immutable';
|
||||
import * as actions from '../actions/settings';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
// this is initialized here because
|
||||
// currently you have to reload the dashboard to get new stats
|
||||
@@ -10,63 +10,82 @@ const DASHBOARD_WINDOW_MINUTES = 5;
|
||||
let then = new Date();
|
||||
then.setMinutes(then.getMinutes() - DASHBOARD_WINDOW_MINUTES);
|
||||
|
||||
const initialState = Map({
|
||||
wordlist: Map({
|
||||
banned: List(),
|
||||
suspect: List()
|
||||
}),
|
||||
const initialState = {
|
||||
wordlist: {
|
||||
banned: [],
|
||||
suspect: []
|
||||
},
|
||||
dashboardWindowStart: then.toISOString(),
|
||||
dashboardWindowEnd: new Date().toISOString(),
|
||||
domains: Map({
|
||||
whitelist: List()
|
||||
}),
|
||||
domains: {
|
||||
whitelist: []
|
||||
},
|
||||
saveSettingsError: null,
|
||||
fetchSettingsError: null,
|
||||
fetchingSettings: false
|
||||
});
|
||||
};
|
||||
|
||||
export default function settings (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.SETTINGS_LOADING:
|
||||
return state
|
||||
.set('fetchingSettings', true)
|
||||
.set('fetchSettingsError', null);
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: true,
|
||||
fetchSettingsError: null,
|
||||
};
|
||||
case actions.SETTINGS_RECEIVED:
|
||||
return state.merge({
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
});
|
||||
};
|
||||
case actions.SETTINGS_FETCH_ERROR:
|
||||
return state
|
||||
.set('fetchingSettings', false)
|
||||
.set('fetchSettingsError', action.error);
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: action.error,
|
||||
};
|
||||
case actions.SETTINGS_UPDATED:
|
||||
return state.merge({
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
});
|
||||
};
|
||||
case actions.SAVE_SETTINGS_LOADING:
|
||||
return state
|
||||
.set('fetchingSettings', true)
|
||||
.set('saveSettingsError', null);
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: true,
|
||||
saveSettingsError: null,
|
||||
};
|
||||
case actions.SAVE_SETTINGS_SUCCESS:
|
||||
return state.merge({
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
});
|
||||
};
|
||||
case actions.SAVE_SETTINGS_FAILED:
|
||||
return state
|
||||
.set('fetchingSettings', false)
|
||||
.set('fetchSettingsError', action.error);
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: action.error,
|
||||
};
|
||||
case actions.WORDLIST_UPDATED:
|
||||
return state
|
||||
.setIn(['wordlist', action.listName], action.list);
|
||||
return update(state, {
|
||||
wordlist: {
|
||||
[action.listName]: {
|
||||
$set: action.list
|
||||
}
|
||||
}
|
||||
});
|
||||
case actions.DOMAINLIST_UPDATED:
|
||||
return state
|
||||
.setIn(['domains', action.listName], action.list);
|
||||
return update(state, {
|
||||
domains: {
|
||||
[action.listName]: {$set: action.list},
|
||||
}
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import styles from './Community.css';
|
||||
import styles from './ActionButton.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../../../utils/moderationQueueActionsMap';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
// TODO: Needs refactoring.
|
||||
const ActionButton = ({type = '', user, ...props}) => {
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -1,299 +0,0 @@
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
padding-bottom: 200px;
|
||||
}
|
||||
|
||||
.leftColumn {
|
||||
padding: 42px 56px;
|
||||
width: 234px;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
width: calc(100% - 300px);
|
||||
padding: 34px 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mainFlaggedContent {
|
||||
width: 100%;
|
||||
padding: 34px 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.roleButton {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
width: 100%;
|
||||
padding: 9px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
background: white;
|
||||
box-sizing: border-box;
|
||||
height: 40px;
|
||||
|
||||
i {
|
||||
color: #A1A1A1
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
padding: 0 2px 0 15px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 8px 0;
|
||||
list-style: none;
|
||||
display: block;
|
||||
|
||||
&.singleView .listItem {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.singleView .listItem.activeItem {
|
||||
display: block;
|
||||
height: 100%;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5em;
|
||||
border: none;
|
||||
|
||||
.actions {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 25%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 50%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listItem {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 18px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
transition: all 200ms;
|
||||
padding: 10px 0 0;
|
||||
min-height: 220px;
|
||||
|
||||
.container {
|
||||
display: block;
|
||||
padding: 0 14px;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.context {
|
||||
a {
|
||||
color: #f36451;
|
||||
text-decoration: underline;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 50px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.author {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
min-width: 230px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.itemBody {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 16px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #757575;
|
||||
font-size: 40px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.created {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 0px;
|
||||
flex: 1;
|
||||
color: black;
|
||||
max-width: 500px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
color: rgba(255, 0, 0, .5);
|
||||
padding-top: 15px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.flagCount{
|
||||
font-size: 12px;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #444;
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@media (--big-viewport) {
|
||||
.listItem {
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
&.activeItem {
|
||||
border: 2px solid #333;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.hasLinks {
|
||||
color: #f00;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.banned {
|
||||
color: #f00;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.ban {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.banButton {
|
||||
width: 114px;
|
||||
letter-spacing: 1px;
|
||||
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.flaggedByCount {
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.flaggedByCount i {
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.flaggedBy {
|
||||
display: inline;
|
||||
padding: 3px;
|
||||
font-size: 14px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.flaggedByLabel {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.flaggedReasons {
|
||||
margin-left: 24px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
p.flaggedByReason {
|
||||
font-size: 1tpx;
|
||||
margin: 0px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.button {
|
||||
composes: buttonReset from 'coral-framework/styles/reset.css';
|
||||
vertical-align: text-bottom;
|
||||
&:hover {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
|
||||
|
||||
import CommunityMenu from './CommunityMenu';
|
||||
import People from './People';
|
||||
import FlaggedAccounts from './FlaggedAccounts';
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
import RejectUsernameDialog from './RejectUsernameDialog';
|
||||
|
||||
export default class Community extends Component {
|
||||
@@ -54,7 +54,7 @@ export default class Community extends Component {
|
||||
}
|
||||
|
||||
getTabContent(searchValue, props) {
|
||||
const {community, root: {users}, viewUserDetail} = props;
|
||||
const {community} = props;
|
||||
const activeTab = props.route.path === ':id' ? 'flagged' : props.route.path;
|
||||
|
||||
if (activeTab === 'people') {
|
||||
@@ -76,16 +76,7 @@ export default class Community extends Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FlaggedAccounts
|
||||
commenters={users}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
showRejectUsernameDialog={props.showRejectUsernameDialog}
|
||||
approveUser={props.approveUser}
|
||||
rejectUsername={props.rejectUsername}
|
||||
currentUser={this.props.currentUser}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
<FlaggedAccounts />
|
||||
<RejectUsernameDialog
|
||||
open={community.rejectUsernameDialog}
|
||||
handleClose={props.hideRejectUsernameDialog}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
.tab {
|
||||
flex: 1;
|
||||
color: white;
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
letter-spacing: 1px;
|
||||
transition: border-bottom 200ms;
|
||||
}
|
||||
|
||||
.tabBar {
|
||||
background-color: rgba(44, 44, 44, 0.89);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: white;
|
||||
box-sizing: border-box;
|
||||
border-bottom: solid 5px #F36451;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './styles.css';
|
||||
import styles from './CommunityMenu.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
.container {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
padding-bottom: 200px;
|
||||
}
|
||||
|
||||
.mainFlaggedContent {
|
||||
width: 100%;
|
||||
padding: 34px 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.userLeave {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.userLeaveActive {
|
||||
opacity: 0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.userEnter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.userEnterActive {
|
||||
opacity: 1.0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
@@ -1,38 +1,74 @@
|
||||
import React from 'react';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './Community.css';
|
||||
import EmptyCard from 'coral-admin/src/components/EmptyCard';
|
||||
import User from './User';
|
||||
import LoadMore from '../../../components/LoadMore';
|
||||
import FlaggedUser from '../containers/FlaggedUser';
|
||||
import {CSSTransitionGroup} from 'react-transition-group';
|
||||
import styles from './FlaggedAccounts.css';
|
||||
|
||||
const FlaggedAccounts = (props) => {
|
||||
const {commenters} = props;
|
||||
const hasResults = commenters && !!commenters.length;
|
||||
class FlaggedAccounts extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
users,
|
||||
loadMore,
|
||||
showBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
showRejectUsernameDialog,
|
||||
approveUser,
|
||||
me,
|
||||
viewUserDetail,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.mainFlaggedContent}>
|
||||
{
|
||||
hasResults
|
||||
? commenters.map((commenter, index) => {
|
||||
return <User
|
||||
user={commenter}
|
||||
key={index}
|
||||
index={index}
|
||||
modActionButtons={['APPROVE', 'REJECT']}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
showRejectUsernameDialog={props.showRejectUsernameDialog}
|
||||
approveUser={props.approveUser}
|
||||
currentUser={props.currentUser}
|
||||
viewUserDetail={props.viewUserDetail}
|
||||
/>;
|
||||
})
|
||||
: <EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
|
||||
}
|
||||
const hasResults = users.nodes && !!users.nodes.length;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.mainFlaggedContent}>
|
||||
{
|
||||
hasResults
|
||||
? <CSSTransitionGroup
|
||||
component={'ul'}
|
||||
className={styles.list}
|
||||
transitionName={{
|
||||
enter: styles.userEnter,
|
||||
enterActive: styles.userEnterActive,
|
||||
leave: styles.userLeave,
|
||||
leaveActive: styles.userLeaveActive,
|
||||
}}
|
||||
transitionEnter={true}
|
||||
transitionLeave={true}
|
||||
transitionEnterTimeout={1000}
|
||||
transitionLeaveTimeout={1000}
|
||||
>
|
||||
{
|
||||
users.nodes.map((user) => {
|
||||
return (
|
||||
<FlaggedUser
|
||||
user={user}
|
||||
key={user.id}
|
||||
modActionButtons={['APPROVE', 'REJECT']}
|
||||
showBanUserDialog={showBanUserDialog}
|
||||
showSuspendUserDialog={showSuspendUserDialog}
|
||||
showRejectUsernameDialog={showRejectUsernameDialog}
|
||||
approveUser={approveUser}
|
||||
me={me}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</CSSTransitionGroup>
|
||||
: <EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
|
||||
}
|
||||
<LoadMore
|
||||
loadMore={loadMore}
|
||||
showLoadMore={users.hasNextPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FlaggedAccounts;
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.root {
|
||||
composes: mdl-shadow--2dp mdl-card from global;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 18px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
transition: box-shadow 200ms, margin-bottom 200ms;
|
||||
padding: 10px 0 0;
|
||||
min-height: 220px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
}
|
||||
}
|
||||
|
||||
.rootSelected {
|
||||
composes: mdl-shadow--16dp from global;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: block;
|
||||
padding: 0 14px;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flaggedByCount {
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.flagIcon {
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.flaggedBy {
|
||||
display: inline;
|
||||
padding: 3px;
|
||||
font-size: 14px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.flaggedByLabel {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.flaggedReasons {
|
||||
margin-left: 24px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.flaggedByReason {
|
||||
font-size: 1tpx;
|
||||
margin: 0px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
|
||||
@media (--big-viewport) {
|
||||
.root {
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
&.rootSelected {
|
||||
border: 2px solid #333;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
composes: buttonReset from 'coral-framework/styles/reset.css';
|
||||
vertical-align: text-bottom;
|
||||
&:hover {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import React from 'react';
|
||||
import styles from './FlaggedUser.css';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
|
||||
// TODO: Should not rely on plugin.
|
||||
import {username} from 'talk-plugin-flags/helpers/flagReasons';
|
||||
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
// TODO: Should work with custom flags too.
|
||||
const shortReasons = {
|
||||
[username.other]: t('community.other'),
|
||||
[username.spam]: t('community.spam_ads'),
|
||||
[username.offensive]: t('community.offensive'),
|
||||
[username.nolike]: t('community.dont_like_username'),
|
||||
[username.impersonating]: t('community.impersonating'),
|
||||
};
|
||||
|
||||
// Render a single user for the list
|
||||
class User extends React.Component {
|
||||
|
||||
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
|
||||
userId: this.props.user.id,
|
||||
username: this.props.user.username,
|
||||
});
|
||||
|
||||
showBanUserDialog = () => this.props.showBanUserDialog({
|
||||
userId: this.props.user.id,
|
||||
username: this.props.user.username,
|
||||
});
|
||||
|
||||
viewAuthorDetail = () => this.props.viewUserDetail(this.props.user.id);
|
||||
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
modActionButtons,
|
||||
viewUserDetail,
|
||||
selected,
|
||||
approveUser,
|
||||
showRejectUsernameDialog,
|
||||
me,
|
||||
className,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.author}>
|
||||
<button
|
||||
onClick={this.viewAuthorDetail}
|
||||
className={styles.button}>
|
||||
{user.username}
|
||||
</button>
|
||||
{me.id !== user.id &&
|
||||
<ActionsMenu icon="not_interested">
|
||||
<ActionsMenuItem
|
||||
disabled={user.status === 'BANNED'}
|
||||
onClick={this.showSuspenUserDialog}>
|
||||
Suspend User
|
||||
</ActionsMenuItem>
|
||||
<ActionsMenuItem
|
||||
disabled={user.status === 'BANNED'}
|
||||
onClick={this.showBanUserDialog}>
|
||||
Ban User
|
||||
</ActionsMenuItem>
|
||||
</ActionsMenu>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.body}>
|
||||
<div className={styles.flagged}>
|
||||
<div className={styles.flaggedByCount}>
|
||||
<i className={cn('material-icons', styles.flagIcon)}>flag</i>
|
||||
<span className={styles.flaggedByLabel}>
|
||||
{t('community.flags')}({ user.actions.length })
|
||||
</span>:
|
||||
{ user.action_summaries.map(
|
||||
(action, i) => {
|
||||
return <span className={styles.flaggedBy} key={i}>
|
||||
{shortReasons[action.reason]} ({action.count})
|
||||
</span>;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.flaggedReasons}>
|
||||
{ user.action_summaries.map(
|
||||
(action_sum, i) => {
|
||||
return <div key={i}>
|
||||
<span className={styles.flaggedByLabel}>
|
||||
{shortReasons[action_sum.reason]} ({action_sum.count})
|
||||
</span>
|
||||
{user.actions.map(
|
||||
|
||||
// find the action by action_sum.reason
|
||||
(action, j) => {
|
||||
if (action.reason === action_sum.reason) {
|
||||
return <p className={styles.flaggedByReason} key={j}>
|
||||
{action.user &&
|
||||
<button onClick={() => {viewUserDetail(action.user.id);}} className={styles.button}>
|
||||
{action.user.username}
|
||||
</button>
|
||||
}
|
||||
: {action.message ? action.message : 'n/a'}
|
||||
</p>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
)}
|
||||
</div>;
|
||||
}
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
<div className={styles.actions}>
|
||||
{modActionButtons.map((action, i) =>
|
||||
<ActionButton key={i}
|
||||
type={action.toUpperCase()}
|
||||
user={user}
|
||||
approveUser={approveUser}
|
||||
showRejectUsernameDialog={showRejectUsernameDialog}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default User;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './Community.css';
|
||||
import styles from './styles.css';
|
||||
import Table from '../containers/Table';
|
||||
import {Pager, Icon} from 'coral-ui';
|
||||
import EmptyCard from '../../../components/EmptyCard';
|
||||
@@ -45,12 +45,12 @@ const People = ({commenters, searchValue, onSearchChange, ...props}) => {
|
||||
<div className={styles.mainContent}>
|
||||
{
|
||||
hasResults
|
||||
? <Table
|
||||
? <Table
|
||||
headers={tableHeaders}
|
||||
commenters={commenters}
|
||||
onHeaderClickHandler={props.onHeaderClickHandler}
|
||||
/>
|
||||
: <EmptyCard>{t('community.no_results')}</EmptyCard>
|
||||
: <EmptyCard>{t('community.no_results')}</EmptyCard>
|
||||
}
|
||||
<Pager
|
||||
totalPages={props.totalPages}
|
||||
|
||||
@@ -50,9 +50,9 @@ class RejectUsernameDialog extends Component {
|
||||
const next = () => this.setState({stage: stage + 1});
|
||||
const suspend = () => {
|
||||
rejectUsername({id: user.user.id, message: this.state.email})
|
||||
.then(() => {
|
||||
this.props.handleClose();
|
||||
});
|
||||
.then(() => {
|
||||
this.props.handleClose();
|
||||
});
|
||||
};
|
||||
|
||||
const suspendModalActions = [
|
||||
@@ -71,21 +71,21 @@ class RejectUsernameDialog extends Component {
|
||||
const {stage} = this.state;
|
||||
|
||||
return <Dialog
|
||||
className={styles.suspendDialog}
|
||||
id="rejectUsernameDialog"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
title={t('reject_username.suspend_user')}>
|
||||
<div className={styles.title}>
|
||||
{t(stages[stage].title, t('reject_username.username'))}
|
||||
</div>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.description}>
|
||||
{t(stages[stage].description, t('reject_username.username'))}
|
||||
</div>
|
||||
{
|
||||
stage === 1 &&
|
||||
className={styles.suspendDialog}
|
||||
id="rejectUsernameDialog"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
title={t('reject_username.suspend_user')}>
|
||||
<div className={styles.title}>
|
||||
{t(stages[stage].title, t('reject_username.username'))}
|
||||
</div>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.description}>
|
||||
{t(stages[stage].description, t('reject_username.username'))}
|
||||
</div>
|
||||
{
|
||||
stage === 1 &&
|
||||
<div className={styles.writeContainer}>
|
||||
<div className={styles.emailMessage}>{t('reject_username.write_message')}</div>
|
||||
<div className={styles.emailContainer}>
|
||||
@@ -96,16 +96,16 @@ class RejectUsernameDialog extends Component {
|
||||
onChange={this.onEmailChange}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className={styles.modalButtons}>
|
||||
{Object.keys(stages[stage].options).map((key, i) => (
|
||||
<Button key={i} onClick={this.onActionClick(stage, i)}>
|
||||
{t(stages[stage].options[key], t('reject_username.username'))}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>;
|
||||
}
|
||||
<div className={styles.modalButtons}>
|
||||
{Object.keys(stages[stage].options).map((key, i) => (
|
||||
<Button key={i} onClick={this.onActionClick(stage, i)}>
|
||||
{t(stages[stage].options[key], t('reject_username.username'))}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onComm
|
||||
<tr>
|
||||
{headers.map((header, i) =>(
|
||||
<th
|
||||
key={i}
|
||||
className="mdl-data-table__cell--non-numeric"
|
||||
onClick={() => onHeaderClickHandler({field: header.field})}>
|
||||
key={i}
|
||||
className="mdl-data-table__cell--non-numeric"
|
||||
onClick={() => onHeaderClickHandler({field: header.field})}>
|
||||
{header.title}
|
||||
</th>
|
||||
))}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './Community.css';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
import {username} from 'talk-plugin-flags/helpers/flagReasons';
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const shortReasons = {
|
||||
[username.other]: t('community.other'),
|
||||
[username.spam]: t('community.spam_ads'),
|
||||
[username.offensive]: t('community.offensive'),
|
||||
[username.nolike]: t('community.dont_like_username'),
|
||||
[username.impersonating]: t('community.impersonating'),
|
||||
};
|
||||
|
||||
// Render a single user for the list
|
||||
const User = (props) => {
|
||||
const {user, modActionButtons} = props;
|
||||
let userStatus = user.status;
|
||||
|
||||
const showSuspenUserDialog = () => props.showSuspendUserDialog({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
});
|
||||
|
||||
const showBanUserDialog = () => props.showBanUserDialog({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
});
|
||||
|
||||
// Do not display unless the user status is 'pending' or 'banned'.
|
||||
// This means that they have already been reviewed and approved.
|
||||
return (userStatus === 'PENDING' || userStatus === 'BANNED') &&
|
||||
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
<button onClick={() => {props.viewUserDetail(user.id);}} className={styles.button}>{user.username}</button>
|
||||
{props.currentUser.id !== user.id &&
|
||||
<ActionsMenu icon="not_interested">
|
||||
<ActionsMenuItem
|
||||
disabled={user.status === 'BANNED'}
|
||||
onClick={showSuspenUserDialog}>
|
||||
Suspend User</ActionsMenuItem>
|
||||
<ActionsMenuItem
|
||||
disabled={user.status === 'BANNED'}
|
||||
onClick={showBanUserDialog}>
|
||||
Ban User
|
||||
</ActionsMenuItem>
|
||||
</ActionsMenu>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.itemBody}>
|
||||
<div className={styles.body}>
|
||||
<div className={styles.flaggedByCount}>
|
||||
<i className="material-icons">flag</i><span className={styles.flaggedByLabel}>{t('community.flags')}({ user.actions.length })</span>:
|
||||
{ user.action_summaries.map(
|
||||
(action, i) => {
|
||||
return <span className={styles.flaggedBy} key={i}>
|
||||
{shortReasons[action.reason]} ({action.count})
|
||||
</span>;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.flaggedReasons}>
|
||||
{ user.action_summaries.map(
|
||||
(action_sum, i) => {
|
||||
return <div key={i}>
|
||||
<span className={styles.flaggedByLabel}>
|
||||
{shortReasons[action_sum.reason]} ({action_sum.count})
|
||||
</span>
|
||||
{user.actions.map(
|
||||
|
||||
// find the action by action_sum.reason
|
||||
(action, j) => {
|
||||
if (action.reason === action_sum.reason) {
|
||||
return <p className={styles.flaggedByReason} key={j}>
|
||||
{action.user &&
|
||||
<button onClick={() => {props.viewUserDetail(action.user.id);}} className={styles.button}>
|
||||
{action.user.username}
|
||||
</button>
|
||||
}
|
||||
: {action.message ? action.message : 'n/a'}
|
||||
</p>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
)}
|
||||
</div>;
|
||||
}
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{modActionButtons.map((action, i) =>
|
||||
<ActionButton key={i}
|
||||
type={action.toUpperCase()}
|
||||
user={user}
|
||||
approveUser={props.approveUser}
|
||||
showRejectUsernameDialog={props.showRejectUsernameDialog}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>;
|
||||
};
|
||||
|
||||
export default User;
|
||||
@@ -1,119 +1,56 @@
|
||||
/**
|
||||
* deprecated as this file contains styles from multiple components. Please remove this file
|
||||
* when styles have been refactored.
|
||||
*/
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.listContainer {
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tabBar {
|
||||
background-color: rgba(44, 44, 44, 0.89);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
color: white;
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
letter-spacing: 1px;
|
||||
transition: border-bottom 200ms;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: white;
|
||||
box-sizing: border-box;
|
||||
border-bottom: solid 5px #F36451;
|
||||
}
|
||||
|
||||
.active > span {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.active:after {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.showShortcuts {
|
||||
position: absolute;
|
||||
right: 130px;
|
||||
.container {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
|
||||
span {
|
||||
margin-left: 7px;
|
||||
}
|
||||
padding-bottom: 200px;
|
||||
}
|
||||
|
||||
@media (--big-viewport) {
|
||||
.tab {
|
||||
flex: none;
|
||||
}
|
||||
.leftColumn {
|
||||
padding: 42px 56px;
|
||||
width: 234px;
|
||||
}
|
||||
|
||||
.approve {
|
||||
margin-top: 10px;
|
||||
.mainContent {
|
||||
width: calc(100% - 300px);
|
||||
padding: 34px 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.notFound {
|
||||
position: relative;
|
||||
margin: 20px auto;
|
||||
text-align: center;
|
||||
padding: 68px 45px;
|
||||
vertical-align: middle;
|
||||
min-width: 500px;
|
||||
|
||||
a {
|
||||
color: rgb(244, 126, 107);
|
||||
font-weight: 500;
|
||||
|
||||
&.goToStreams {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
.roleButton {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #2c2c2c;
|
||||
color: white;
|
||||
margin-bottom: -1px;
|
||||
.searchBox {
|
||||
width: 100%;
|
||||
padding: 9px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
background: white;
|
||||
box-sizing: border-box;
|
||||
height: 40px;
|
||||
|
||||
.settingsButton {
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
i {
|
||||
color: #A1A1A1
|
||||
}
|
||||
|
||||
.moderateAsset {
|
||||
a {
|
||||
text-align: center;
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
color: white;
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
letter-spacing: 1px;
|
||||
transition: opacity 200ms;
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
opacity: .8;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
padding: 0 2px 0 15px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.list {
|
||||
padding: 8px 0;
|
||||
list-style: none;
|
||||
@@ -134,15 +71,12 @@ span {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 25%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 50%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +93,7 @@ span {
|
||||
min-height: 220px;
|
||||
|
||||
.container {
|
||||
display: block;
|
||||
padding: 0 14px;
|
||||
min-height: 180px;
|
||||
}
|
||||
@@ -184,9 +119,9 @@ span {
|
||||
right: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 40px 18px;
|
||||
padding: 50px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
@@ -194,6 +129,8 @@ span {
|
||||
justify-content: space-between;
|
||||
|
||||
.author {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
min-width: 230px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -221,18 +158,12 @@ span {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 0px;
|
||||
flex: 1;
|
||||
color: black;
|
||||
max-width: 500px;
|
||||
word-wrap: break-word;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
@@ -245,11 +176,8 @@ span {
|
||||
font-size: 12px;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.empty {
|
||||
color: #444;
|
||||
margin-top: 50px;
|
||||
@@ -301,34 +229,14 @@ span {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.Comment {
|
||||
.moderateArticle {
|
||||
font-size: 12px;
|
||||
a {
|
||||
display: inline-block;
|
||||
color: #679af3;
|
||||
text-decoration: none;
|
||||
font-size: 1em;
|
||||
font-weight: 400;
|
||||
letter-spacing: .5px;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
.banButton {
|
||||
width: 114px;
|
||||
letter-spacing: 1px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: .9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flagBox {
|
||||
max-width: 480px;
|
||||
border-top: 1px solid rgba(66, 66, 66, 0.12);
|
||||
h3 {
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import {Spinner} from 'coral-ui';
|
||||
import {compose} from 'react-apollo';
|
||||
|
||||
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
|
||||
|
||||
import {showBanUserDialog} from 'actions/banUserDialog';
|
||||
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
|
||||
|
||||
import {
|
||||
fetchAccounts,
|
||||
updateSorting,
|
||||
newPage,
|
||||
showRejectUsernameDialog,
|
||||
hideRejectUsernameDialog
|
||||
} from '../../../actions/community';
|
||||
import {viewUserDetail} from '../../../actions/userDetail';
|
||||
|
||||
import Community from '../components/Community';
|
||||
|
||||
@@ -27,85 +19,26 @@ class CommunityContainer extends Component {
|
||||
this.props.fetchAccounts({});
|
||||
}
|
||||
|
||||
approveUser = ({userId}) => {
|
||||
return this.props.setUserStatus({userId, status: 'APPROVED'});
|
||||
}
|
||||
|
||||
banUser = ({userId}) => {
|
||||
return this.props.setUserStatus({userId, status: 'BANNED'});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (!('users' in this.props.root)) {
|
||||
return <div><Spinner/></div>;
|
||||
}
|
||||
return (
|
||||
<Community {...this.props} approveUser={this.approveUser} banUser={this.banUser}/>
|
||||
<Community {...this.props} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const withCommunityQuery = withQuery(gql`
|
||||
query CoralAdmin_Community($action_type: ACTION_TYPE) {
|
||||
users(query:{action_type: $action_type}){
|
||||
id
|
||||
username
|
||||
status
|
||||
roles
|
||||
actions{
|
||||
id
|
||||
created_at
|
||||
... on FlagAction {
|
||||
reason
|
||||
message
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, {
|
||||
options: ({params: {action_type = 'FLAG'}}) => {
|
||||
return {
|
||||
variables: {
|
||||
action_type: action_type
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
community: state.community.toJS(),
|
||||
currentUser: state.auth.toJS().user,
|
||||
community: state.community,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
fetchAccounts,
|
||||
showBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
showRejectUsernameDialog,
|
||||
hideRejectUsernameDialog,
|
||||
updateSorting,
|
||||
newPage,
|
||||
viewUserDetail,
|
||||
}, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withCommunityQuery,
|
||||
withSetUserStatus,
|
||||
withRejectUsername,
|
||||
)(CommunityContainer);
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import {Spinner} from 'coral-ui';
|
||||
|
||||
import {withSetUserStatus} from 'coral-framework/graphql/mutations';
|
||||
import {showBanUserDialog} from 'actions/banUserDialog';
|
||||
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
|
||||
import {showRejectUsernameDialog} from '../../../actions/community';
|
||||
import {viewUserDetail} from '../../../actions/userDetail';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
import {appendNewNodes} from 'plugin-api/beta/client/utils';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
import FlaggedAccounts from '../components/FlaggedAccounts';
|
||||
import FlaggedUser from '../containers/FlaggedUser';
|
||||
|
||||
class FlaggedAccountsContainer extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
approveUser = ({userId}) => {
|
||||
return this.props.setUserStatus({userId, status: 'APPROVED'});
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
return this.props.data.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables: {
|
||||
limit: 5,
|
||||
cursor: this.props.root.users.endCursor,
|
||||
},
|
||||
updateQuery: (previous, {fetchMoreResult:{users}}) => {
|
||||
const updated = update(previous, {
|
||||
users: {
|
||||
nodes: {
|
||||
$apply: (nodes) => appendNewNodes(nodes, users.nodes),
|
||||
},
|
||||
hasNextPage: {$set: users.hasNextPage},
|
||||
endCursor: {$set: users.endCursor},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (this.props.data.loading) {
|
||||
return <div><Spinner/></div>;
|
||||
}
|
||||
return (
|
||||
<FlaggedAccounts
|
||||
showBanUserDialog={this.props.showBanUserDialog}
|
||||
showSuspendUserDialog={this.props.showSuspendUserDialog}
|
||||
showRejectUsernameDialog={this.props.showRejectUsernameDialog}
|
||||
viewUserDetail={this.props.viewUserDetail}
|
||||
approveUser={this.approveUser}
|
||||
loadMore={this.loadMore}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
users={this.props.root.users}
|
||||
me={this.props.root.me}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query TalkAdmin_LoadMoreFlaggedAccounts($limit: Int, $cursor: Cursor) {
|
||||
users(query:{action_type: FLAG, statuses: [PENDING], limit: $limit, cursor: $cursor}){
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
__typename
|
||||
...${getDefinitionName(FlaggedUser.fragments.user)}
|
||||
}
|
||||
}
|
||||
}
|
||||
${FlaggedUser.fragments.user}
|
||||
`;
|
||||
|
||||
export const withFlaggedAccountsyQuery = withQuery(gql`
|
||||
query TalkAdmin_FlaggedAccounts {
|
||||
...${getDefinitionName(FlaggedUser.fragments.root)}
|
||||
users(query:{action_type: FLAG, statuses: [PENDING], limit: 10}){
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
__typename
|
||||
...${getDefinitionName(FlaggedUser.fragments.user)}
|
||||
}
|
||||
}
|
||||
me {
|
||||
__typename
|
||||
...${getDefinitionName(FlaggedUser.fragments.me)}
|
||||
}
|
||||
}
|
||||
${FlaggedUser.fragments.root}
|
||||
${FlaggedUser.fragments.user}
|
||||
${FlaggedUser.fragments.me}
|
||||
`, {
|
||||
options: {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
showBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
showRejectUsernameDialog,
|
||||
viewUserDetail,
|
||||
}, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withFlaggedAccountsyQuery,
|
||||
withSetUserStatus,
|
||||
)(FlaggedAccountsContainer);
|
||||
@@ -0,0 +1,42 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import FlaggedUser from '../components/FlaggedUser';
|
||||
import {withFragments} from 'plugin-api/beta/client/hocs';
|
||||
|
||||
export default withFragments({
|
||||
root: gql`
|
||||
fragment TalkAdminCommunity_FlaggedUser_root on RootQuery {
|
||||
__typename
|
||||
}
|
||||
`,
|
||||
me: gql`
|
||||
fragment TalkAdminCommunity_FlaggedUser_me on User {
|
||||
id
|
||||
}
|
||||
`,
|
||||
user: gql`
|
||||
fragment TalkAdminCommunity_FlaggedUser_user on User {
|
||||
id
|
||||
username
|
||||
status
|
||||
roles
|
||||
actions{
|
||||
id
|
||||
created_at
|
||||
... on FlagAction {
|
||||
reason
|
||||
message
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
})(FlaggedUser);
|
||||
@@ -23,7 +23,7 @@ class TableContainer extends Component {
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
commenters: state.community.get('accounts'),
|
||||
commenters: state.community.accounts,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
|
||||
*/
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
|
||||
|
||||
@@ -111,20 +111,20 @@ export default class Configure extends Component {
|
||||
(bool, error) => this.state.errors[error] ? false : bool, this.state.changed);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
<List onChange={this.changeSection} activeItem={activeSection}>
|
||||
<Item itemId='stream' icon='speaker_notes'>
|
||||
{t('configure.stream_settings')}
|
||||
</Item>
|
||||
<Item itemId='moderation' icon='thumbs_up_down'>
|
||||
{t('configure.moderation_settings')}
|
||||
</Item>
|
||||
<Item itemId='tech' icon='code'>
|
||||
{t('configure.tech_settings')}
|
||||
</Item>
|
||||
</List>
|
||||
<div className={styles.saveBox}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
<List onChange={this.changeSection} activeItem={activeSection}>
|
||||
<Item itemId='stream' icon='speaker_notes'>
|
||||
{t('configure.stream_settings')}
|
||||
</Item>
|
||||
<Item itemId='moderation' icon='thumbs_up_down'>
|
||||
{t('configure.moderation_settings')}
|
||||
</Item>
|
||||
<Item itemId='tech' icon='code'>
|
||||
{t('configure.tech_settings')}
|
||||
</Item>
|
||||
</List>
|
||||
<div className={styles.saveBox}>
|
||||
{
|
||||
showSave ?
|
||||
<Button
|
||||
@@ -136,25 +136,25 @@ export default class Configure extends Component {
|
||||
>
|
||||
{t('configure.save_changes')}
|
||||
</Button>
|
||||
:
|
||||
:
|
||||
<Button
|
||||
raised
|
||||
disabled
|
||||
icon='check'
|
||||
full
|
||||
>
|
||||
{t('configure.save_changes')}
|
||||
</Button>
|
||||
{t('configure.save_changes')}
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
{ this.props.saveFetchingError }
|
||||
{ this.props.fetchSettingsError }
|
||||
{ section }
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
{ this.props.saveFetchingError }
|
||||
{ this.props.fetchSettingsError }
|
||||
{ section }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {Component} from 'react';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {join} from 'path';
|
||||
import styles from './Configure.css';
|
||||
import {Button, Card} from 'coral-ui';
|
||||
import {BASE_URL} from 'coral-framework/constants/url';
|
||||
@@ -25,7 +26,7 @@ class EmbedLink extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const coralJsUrl = [BASE_URL, '/embed.js'].join('');
|
||||
const coralJsUrl = join(BASE_URL, '/embed.js');
|
||||
const nonce = String(Math.random()).slice(2);
|
||||
const streamElementId = `coral_talk_${nonce}`;
|
||||
const embedText = `
|
||||
|
||||
@@ -4,7 +4,7 @@ import t from 'coral-framework/services/i18n';
|
||||
import styles from './Configure.css';
|
||||
import {Checkbox, Textfield} from 'react-mdl';
|
||||
import {Card, Icon, TextArea} from 'coral-ui';
|
||||
import MarkdownEditor from 'coral-admin/src/components/MarkdownEditor';
|
||||
import MarkdownEditor from 'coral-framework/components/MarkdownEditor';
|
||||
|
||||
const TIMESTAMPS = {
|
||||
weeks: 60 * 60 * 24 * 7,
|
||||
@@ -93,15 +93,15 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
|
||||
value={settings.charCount}
|
||||
disabled={settings.charCountEnable ? '' : 'disabled'}
|
||||
/>
|
||||
<span>{t('configure.comment_count_text_post')}</span>
|
||||
{
|
||||
errors.charCount &&
|
||||
<span>{t('configure.comment_count_text_post')}</span>
|
||||
{
|
||||
errors.charCount &&
|
||||
<span className={styles.settingsError}>
|
||||
<br/>
|
||||
<Icon name="error_outline"/>
|
||||
{t('configure.comment_count_error')}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -23,8 +23,8 @@ class ConfigureContainer extends Component {
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth.toJS(),
|
||||
settings: state.settings.toJS()
|
||||
auth: state.auth,
|
||||
settings: state.settings
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
|
||||
@@ -14,19 +14,19 @@ const ActivityWidget = ({assets}) => {
|
||||
<div className={styles.widgetTable}>
|
||||
{
|
||||
assets.length
|
||||
? assets.map((asset) => {
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/flagged/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{asset.commentCount}</p>
|
||||
<Link className={styles.linkToAsset} to={`${asset.url}#coralStreamEmbed_iframe`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</Link>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{t('dashboard.no_activity')}</div>
|
||||
? assets.map((asset) => {
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{asset.commentCount}</p>
|
||||
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</a>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{t('dashboard.no_activity')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,17 +5,24 @@ import {Icon} from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
const refreshIntervalSeconds = 60 * 5;
|
||||
|
||||
// TODO: refactor out storage code into redux.
|
||||
|
||||
class CountdownTimer extends React.Component {
|
||||
|
||||
static contextTypes = {
|
||||
storage: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
handleTimeout: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
const {storage} = context;
|
||||
try {
|
||||
if (window.localStorage.getItem('coral:dashboardNote') === null) {
|
||||
window.localStorage.setItem('coral:dashboardNote', 'show');
|
||||
if (storage && storage.getItem('coral:dashboardNote') === null) {
|
||||
storage.setItem('coral:dashboardNote', 'show');
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -24,7 +31,7 @@ class CountdownTimer extends React.Component {
|
||||
|
||||
this.state = {
|
||||
secondsUntilRefresh: refreshIntervalSeconds,
|
||||
dashboardNote: window.localStorage.getItem('coral:dashboardNote') || 'show'
|
||||
dashboardNote: (storage && storage.getItem('coral:dashboardNote')) || 'show'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,8 +61,11 @@ class CountdownTimer extends React.Component {
|
||||
}
|
||||
|
||||
dismissNote = () => {
|
||||
const {storage} = this.context;
|
||||
try {
|
||||
window.localStorage.setItem('coral:dashboardNote', 'hide');
|
||||
if (storage) {
|
||||
storage.setItem('coral:dashboardNote', 'hide');
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
// when setItem fails in Safari Private mode
|
||||
@@ -64,7 +74,8 @@ class CountdownTimer extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const hideReloadNote = window.localStorage.getItem('coral:dashboardNote') === 'hide' ||
|
||||
const {storage} = this.context;
|
||||
const hideReloadNote = (storage && storage.getItem('coral:dashboardNote') === 'hide') ||
|
||||
this.state.dashboardNote === 'hide'; // for Safari Incognito
|
||||
return (
|
||||
<p
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
|
||||
*/
|
||||
|
||||
.Dashboard {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -16,24 +16,24 @@ const FlagWidget = ({assets}) => {
|
||||
<div className={styles.widgetTable}>
|
||||
{
|
||||
assets.length
|
||||
? assets.map((asset) => {
|
||||
let flagSummary = null;
|
||||
if (asset.action_summaries) {
|
||||
flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary');
|
||||
}
|
||||
? assets.map((asset) => {
|
||||
let flagSummary = null;
|
||||
if (asset.action_summaries) {
|
||||
flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/flagged/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
|
||||
<Link className={styles.linkToAsset} to={`${asset.url}#coralStreamEmbed_iframe`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</Link>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{t('dashboard.no_flags')}</div>
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/reported/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
|
||||
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</a>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{t('dashboard.no_flags')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,20 +15,20 @@ const LikeWidget = ({assets}) => {
|
||||
<div className={styles.widgetTable}>
|
||||
{
|
||||
assets.length
|
||||
? assets.map((asset) => {
|
||||
const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary');
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/flagged/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
|
||||
<Link className={styles.linkToAsset} to={`${asset.url}#coralStreamEmbed_iframe`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</Link>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{t('dashboard.no_likes')}</div>
|
||||
? assets.map((asset) => {
|
||||
const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary');
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
|
||||
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</a>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{t('dashboard.no_likes')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--row-height: 60px;
|
||||
}
|
||||
|
||||
@@ -22,10 +22,10 @@ class DashboardContainer extends React.Component {
|
||||
|
||||
export const witDashboardQuery = withQuery(gql`
|
||||
query CoralAdmin_Dashboard($from: Date!, $to: Date!) {
|
||||
assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
|
||||
assetsByFlag: assetMetrics(from: $from, to: $to, sortBy: FLAG) {
|
||||
...CoralAdmin_Metrics
|
||||
}
|
||||
assetsByActivity: assetMetrics(from: $from, to: $to, sort: ACTIVITY) {
|
||||
assetsByActivity: assetMetrics(from: $from, to: $to, sortBy: ACTIVITY) {
|
||||
...CoralAdmin_Metrics
|
||||
}
|
||||
}
|
||||
@@ -42,20 +42,20 @@ export const witDashboardQuery = withQuery(gql`
|
||||
}
|
||||
}
|
||||
`, {
|
||||
options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
|
||||
return {
|
||||
variables: {
|
||||
from: dashboardWindowStart,
|
||||
to: dashboardWindowEnd
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
|
||||
return {
|
||||
variables: {
|
||||
from: dashboardWindowStart,
|
||||
to: dashboardWindowEnd
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
settings: state.settings.toJS(),
|
||||
moderation: state.moderation.toJS()
|
||||
settings: state.settings,
|
||||
moderation: state.moderation
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+6
-6
@@ -1,12 +1,12 @@
|
||||
.Install {
|
||||
.install {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
padding: 50px 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
.header {
|
||||
font-size: 2em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {Component} from 'react';
|
||||
import styles from './style.css';
|
||||
import styles from './Install.css';
|
||||
import {Wizard, WizardNav} from 'coral-ui';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
|
||||
@@ -37,11 +37,11 @@ export default class Install extends Component {
|
||||
|
||||
return (
|
||||
<Layout restricted={true}>
|
||||
<div className={styles.Install}>
|
||||
<div className={styles.install}>
|
||||
{
|
||||
!install.alreadyInstalled ? (
|
||||
<div>
|
||||
<h2>Welcome to the Coral Project</h2>
|
||||
<h2 className={styles.header}>Welcome to the Coral Project</h2>
|
||||
{ install.step !== 0 ? <WizardNav items={install.navItems} currentStep={install.step} icon='check'/> : null }
|
||||
<Wizard
|
||||
currentStep={install.step}
|
||||
|
||||
@@ -19,7 +19,7 @@ const InitialStep = (props) => {
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.email}
|
||||
noValidate
|
||||
/>
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className={styles.textField}
|
||||
@@ -29,7 +29,7 @@ const InitialStep = (props) => {
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.username}
|
||||
/>
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className={styles.textField}
|
||||
@@ -39,7 +39,7 @@ const InitialStep = (props) => {
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.password}
|
||||
/>
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className={styles.textField}
|
||||
@@ -49,13 +49,13 @@ const InitialStep = (props) => {
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.confirmPassword}
|
||||
/>
|
||||
/>
|
||||
|
||||
{
|
||||
!props.install.isLoading ?
|
||||
<Button cStyle='black' type="submit" full>{t('install.create.save')}</Button>
|
||||
:
|
||||
<Spinner />
|
||||
<Button cStyle='black' type="submit" full>{t('install.create.save')}</Button>
|
||||
:
|
||||
<Spinner />
|
||||
}
|
||||
{props.install.installRequest === 'FAILURE' && <div className={styles.error}>Error: {props.install.installRequestError}</div>}
|
||||
</form>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @TODO: deprecated as this file contains styles from multiple components. Please remove this file
|
||||
* when styles have been refactored.
|
||||
*/
|
||||
|
||||
.step {
|
||||
padding: 20px 0;
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ InstallContainer.contextTypes = {
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
install: state.install.toJS()
|
||||
install: state.install
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, {PropTypes} from 'react';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
import {Icon} from 'coral-ui';
|
||||
import ReplyBadge from 'coral-admin/src/components/ReplyBadge';
|
||||
import FlagBox from 'coral-admin/src/components/FlagBox';
|
||||
import styles from './styles.css';
|
||||
import CommentType from 'coral-admin/src/components/CommentType';
|
||||
@@ -20,6 +21,31 @@ import t, {timeago} from 'coral-framework/services/i18n';
|
||||
|
||||
class Comment extends React.Component {
|
||||
|
||||
showSuspendUserDialog = () => {
|
||||
const {comment, showSuspendUserDialog} = this.props;
|
||||
return showSuspendUserDialog({
|
||||
userId: comment.user.id,
|
||||
username: comment.user.username,
|
||||
commentId: comment.id,
|
||||
commentStatus: comment.status,
|
||||
});
|
||||
};
|
||||
|
||||
showBanUserDialog = () => {
|
||||
const {comment, showBanUserDialog} = this.props;
|
||||
return showBanUserDialog({
|
||||
userId: comment.user.id,
|
||||
username: comment.user.username,
|
||||
commentId: comment.id,
|
||||
commentStatus: comment.status,
|
||||
});
|
||||
};
|
||||
|
||||
viewUserDetail = () => {
|
||||
const {viewUserDetail, comment} = this.props;
|
||||
return viewUserDetail(comment.user.id);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
actions = [],
|
||||
@@ -29,28 +55,21 @@ class Comment extends React.Component {
|
||||
bannedWords,
|
||||
selected,
|
||||
className,
|
||||
...props
|
||||
data,
|
||||
root,
|
||||
currentUserId,
|
||||
currentAsset,
|
||||
acceptComment,
|
||||
rejectComment,
|
||||
} = this.props;
|
||||
|
||||
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
|
||||
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
|
||||
const commentType = getCommentType(comment);
|
||||
|
||||
let selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
|
||||
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
|
||||
|
||||
const showSuspenUserDialog = () => props.showSuspendUserDialog({
|
||||
userId: comment.user.id,
|
||||
username: comment.user.username,
|
||||
commentId: comment.id,
|
||||
commentStatus: comment.status,
|
||||
});
|
||||
|
||||
const showBanUserDialog = () => props.showBanUserDialog({
|
||||
userId: comment.user.id,
|
||||
username: comment.user.username,
|
||||
commentId: comment.id,
|
||||
commentStatus: comment.status,
|
||||
});
|
||||
const queryData = {root, comment, asset: comment.asset};
|
||||
|
||||
return (
|
||||
<li
|
||||
@@ -62,7 +81,7 @@ class Comment extends React.Component {
|
||||
<div className={styles.author}>
|
||||
{
|
||||
(
|
||||
<span className={styles.username} onClick={() => viewUserDetail(comment.user.id)}>
|
||||
<span className={styles.username} onClick={this.viewUserDetail}>
|
||||
{comment.user.username}
|
||||
</span>
|
||||
)
|
||||
@@ -72,30 +91,29 @@ class Comment extends React.Component {
|
||||
</span>
|
||||
{
|
||||
(comment.editing && comment.editing.edited)
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
: null
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
: null
|
||||
}
|
||||
{props.currentUserId !== comment.user.id &&
|
||||
{currentUserId !== comment.user.id &&
|
||||
<ActionsMenu icon="not_interested">
|
||||
<ActionsMenuItem
|
||||
disabled={comment.user.status === 'BANNED'}
|
||||
onClick={showSuspenUserDialog}>
|
||||
onClick={this.showSuspendUserDialog}>
|
||||
Suspend User</ActionsMenuItem>
|
||||
<ActionsMenuItem
|
||||
disabled={comment.user.status === 'BANNED'}
|
||||
onClick={showBanUserDialog}>
|
||||
onClick={this.showBanUserDialog}>
|
||||
Ban User
|
||||
</ActionsMenuItem>
|
||||
</ActionsMenu>
|
||||
}
|
||||
<div className={styles.adminCommentInfoBar}>
|
||||
{comment.hasParent && <ReplyBadge/>}
|
||||
<CommentType type={commentType} className={styles.commentType}/>
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
comment={comment}
|
||||
asset={comment.asset}
|
||||
fill="adminCommentInfoBar"
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,7 +121,7 @@ class Comment extends React.Component {
|
||||
|
||||
<div className={styles.moderateArticle}>
|
||||
Story: {comment.asset.title}
|
||||
{!props.currentAsset &&
|
||||
{!currentAsset &&
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
|
||||
</div>
|
||||
<CommentAnimatedEdit body={comment.body}>
|
||||
@@ -124,10 +142,9 @@ class Comment extends React.Component {
|
||||
</a>
|
||||
</p>
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminCommentContent"
|
||||
comment={comment}
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
/>
|
||||
<div className={styles.sideActions}>
|
||||
<IfHasLink text={comment.body}>
|
||||
@@ -150,37 +167,35 @@ class Comment extends React.Component {
|
||||
acceptComment={() =>
|
||||
(comment.status === 'ACCEPTED'
|
||||
? null
|
||||
: props.acceptComment({commentId: comment.id}))}
|
||||
: acceptComment({commentId: comment.id}))}
|
||||
rejectComment={() =>
|
||||
(comment.status === 'REJECTED'
|
||||
? null
|
||||
: props.rejectComment({commentId: comment.id}))}
|
||||
: rejectComment({commentId: comment.id}))}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminSideActions"
|
||||
comment={comment}
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CommentAnimatedEdit>
|
||||
</div>
|
||||
<Slot
|
||||
data={props.data}
|
||||
root={props.root}
|
||||
fill="adminCommentDetailArea"
|
||||
comment={comment}
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
/>
|
||||
{flagActions && flagActions.length
|
||||
? <FlagBox
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
: null}
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
.count {
|
||||
display: inline-block;
|
||||
background: #989797;
|
||||
background: #616161;
|
||||
margin: 2px;
|
||||
vertical-align: middle;
|
||||
padding: 1px 7px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
margin-left: 2px;
|
||||
line-height: 20px;
|
||||
line-height: 18px;
|
||||
box-sizing: border-box;
|
||||
height: 21px;
|
||||
height: 18px;
|
||||
right: 0;
|
||||
margin-top: -2px;
|
||||
margin-top: 0px;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -101,33 +101,19 @@ export default class Moderation extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {root, data, moderation, settings, viewUserDetail, hideUserDetail, activeTab, getModPath, premodEnabled, ...props} = this.props;
|
||||
const assetId = this.props.params.id;
|
||||
const {root, data, moderation, settings, viewUserDetail, hideUserDetail, activeTab, getModPath, queueConfig, handleCommentChange, ...props} = this.props;
|
||||
const {asset} = root;
|
||||
const assetId = asset && asset.id;
|
||||
|
||||
const comments = root[activeTab];
|
||||
|
||||
let activeTabCount;
|
||||
switch(activeTab) {
|
||||
case 'all':
|
||||
activeTabCount = root.allCount;
|
||||
break;
|
||||
case 'new':
|
||||
activeTabCount = root.newCount;
|
||||
break;
|
||||
case 'approved':
|
||||
activeTabCount = root.approvedCount;
|
||||
break;
|
||||
case 'premod':
|
||||
activeTabCount = root.premodCount;
|
||||
break;
|
||||
case 'reported':
|
||||
activeTabCount = root.reportedCount;
|
||||
break;
|
||||
case 'rejected':
|
||||
activeTabCount = root.rejectedCount;
|
||||
break;
|
||||
}
|
||||
const activeTabCount = root[`${activeTab}Count`];
|
||||
const menuItems = Object.keys(queueConfig).map((queue) => ({
|
||||
key: queue,
|
||||
name: queueConfig[queue].name,
|
||||
icon: queueConfig[queue].icon,
|
||||
count: root[`${queue}Count`]
|
||||
}));
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -139,16 +125,10 @@ export default class Moderation extends Component {
|
||||
/>
|
||||
<ModerationMenu
|
||||
asset={asset}
|
||||
allCount={root.allCount}
|
||||
newCount={root.newCount}
|
||||
getModPath={getModPath}
|
||||
approvedCount={root.approvedCount}
|
||||
premodCount={root.premodCount}
|
||||
rejectedCount={root.rejectedCount}
|
||||
reportedCount={root.reportedCount}
|
||||
items={menuItems}
|
||||
selectSort={this.props.setSortOrder}
|
||||
sort={this.props.moderation.sortOrder}
|
||||
premodEnabled={premodEnabled}
|
||||
activeTab={activeTab}
|
||||
/>
|
||||
<ModerationQueue
|
||||
@@ -188,9 +168,9 @@ export default class Moderation extends Component {
|
||||
|
||||
<Slot
|
||||
data={data}
|
||||
root={root}
|
||||
assset={asset}
|
||||
queryData={{root, asset}}
|
||||
activeTab={activeTab}
|
||||
handleCommentChange={handleCommentChange}
|
||||
fill='adminModeration'
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -9,16 +9,10 @@ import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ModerationMenu = ({
|
||||
asset = {},
|
||||
allCount,
|
||||
approvedCount,
|
||||
premodCount,
|
||||
newCount,
|
||||
rejectedCount,
|
||||
reportedCount,
|
||||
asset = {},
|
||||
items,
|
||||
selectSort,
|
||||
sort,
|
||||
premodEnabled,
|
||||
getModPath,
|
||||
activeTab
|
||||
}) => {
|
||||
@@ -27,57 +21,23 @@ const ModerationMenu = ({
|
||||
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
|
||||
<div className={styles.tabBarPadding} />
|
||||
<div>
|
||||
|
||||
{
|
||||
premodEnabled ? (
|
||||
<Link
|
||||
to={getModPath('premod', asset.id)}
|
||||
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'premod'})}
|
||||
activeClassName={styles.active}>
|
||||
<Icon name='access_time' className={styles.tabIcon} /> {t('modqueue.premod')} <CommentCount count={premodCount} />
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
to={getModPath('new', asset.id)}
|
||||
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'new'})}
|
||||
activeClassName={styles.active}>
|
||||
<Icon name='question_answer' className={styles.tabIcon} /> {t('modqueue.new')} <CommentCount count={newCount} />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
<Link
|
||||
to={getModPath('reported', asset.id)}
|
||||
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'reported'})}
|
||||
activeClassName={styles.active}>
|
||||
<Icon name='flag' className={styles.tabIcon} /> {t('modqueue.reported')} <CommentCount count={reportedCount} />
|
||||
</Link>
|
||||
<Link
|
||||
to={getModPath('approved', asset.id)}
|
||||
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'approved'})}
|
||||
activeClassName={styles.active}>
|
||||
<Icon name='check' className={styles.tabIcon} /> {t('modqueue.approved')} <CommentCount count={approvedCount} />
|
||||
</Link>
|
||||
<Link
|
||||
to={getModPath('rejected', asset.id)}
|
||||
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'rejected'})}
|
||||
activeClassName={styles.active}>
|
||||
<Icon name='close' className={styles.tabIcon} /> {t('modqueue.rejected')} <CommentCount count={rejectedCount} />
|
||||
</Link>
|
||||
<Link
|
||||
to={getModPath('all', asset.id)}
|
||||
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'all'})}
|
||||
activeClassName={styles.active}>
|
||||
<Icon name='question_answer' className={styles.tabIcon} /> {t('modqueue.all')} <CommentCount count={allCount} />
|
||||
</Link>
|
||||
{items.map((queue) =>
|
||||
<Link
|
||||
key={queue.key}
|
||||
to={getModPath(queue.key, asset.id)}
|
||||
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === queue.key})}
|
||||
activeClassName={styles.active}>
|
||||
<Icon name={queue.icon} className={styles.tabIcon} /> {queue.name} <CommentCount count={queue.count} />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<SelectField
|
||||
className={styles.selectField}
|
||||
label="Sort"
|
||||
value={sort}
|
||||
onChange={(sort) => selectSort(sort)}>
|
||||
<Option value={'REVERSE_CHRONOLOGICAL'}>{t('modqueue.newest_first')}</Option>
|
||||
<Option value={'CHRONOLOGICAL'}>{t('modqueue.oldest_first')}</Option>
|
||||
<Option value={'DESC'}>{t('modqueue.newest_first')}</Option>
|
||||
<Option value={'ASC'}>{t('modqueue.oldest_first')}</Option>
|
||||
</SelectField>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,10 +45,7 @@ const ModerationMenu = ({
|
||||
};
|
||||
|
||||
ModerationMenu.propTypes = {
|
||||
allCount: PropTypes.number.isRequired,
|
||||
premodCount: PropTypes.number.isRequired,
|
||||
rejectedCount: PropTypes.number.isRequired,
|
||||
reportedCount: PropTypes.number.isRequired,
|
||||
items: PropTypes.array.isRequired,
|
||||
asset: PropTypes.shape({
|
||||
id: PropTypes.string
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import Comment from '../containers/Comment';
|
||||
import styles from './styles.css';
|
||||
import EmptyCard from '../../../components/EmptyCard';
|
||||
import {actionsMap} from '../../../utils/moderationQueueActionsMap';
|
||||
import LoadMore from './LoadMore';
|
||||
import LoadMore from '../../../components/LoadMore';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {CSSTransitionGroup} from 'react-transition-group';
|
||||
|
||||
@@ -97,7 +97,7 @@ class ModerationQueue extends React.Component {
|
||||
rejectComment={props.rejectComment}
|
||||
currentAsset={props.currentAsset}
|
||||
currentUserId={this.props.currentUserId}
|
||||
/>;
|
||||
/>;
|
||||
})
|
||||
}
|
||||
</CSSTransitionGroup>
|
||||
@@ -110,7 +110,7 @@ class ModerationQueue extends React.Component {
|
||||
<LoadMore
|
||||
loadMore={this.loadMore}
|
||||
showLoadMore={comments.length < commentCount}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,20 +61,20 @@ const StorySearch = (props) => {
|
||||
|
||||
{
|
||||
loading
|
||||
? <Spinner />
|
||||
: assets.map((story, i) => {
|
||||
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
|
||||
? <Spinner />
|
||||
: assets.map((story, i) => {
|
||||
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
|
||||
|
||||
return <Story
|
||||
key={i}
|
||||
id={story.id}
|
||||
title={story.title}
|
||||
createdAt={new Date(story.created_at).toISOString()}
|
||||
open={storyOpen}
|
||||
author={story.author}
|
||||
goToStory={props.goToStory}
|
||||
/>;
|
||||
})
|
||||
return <Story
|
||||
key={i}
|
||||
id={story.id}
|
||||
title={story.title}
|
||||
createdAt={new Date(story.created_at).toISOString()}
|
||||
open={storyOpen}
|
||||
author={story.author}
|
||||
goToStory={props.goToStory}
|
||||
/>;
|
||||
})
|
||||
}
|
||||
|
||||
{assets.length === 0 && <div className={styles.noResults}>No results</div>}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @TODO: deprecated as this file contains styles from multiple components. Please remove this file
|
||||
* when styles have been refactored.
|
||||
*/
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.listContainer {
|
||||
@@ -18,7 +23,7 @@
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
color: #C0C0C0;
|
||||
color: #BDBDBD;
|
||||
text-transform: capitalize;
|
||||
font-weight: 100;
|
||||
font-size: 14px;
|
||||
@@ -29,7 +34,7 @@
|
||||
margin-right: 20px;
|
||||
&:hover {
|
||||
color: white;
|
||||
border-bottom: solid 2px #F36451;
|
||||
/*border-bottom: solid 2px #F36451;*/
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
@@ -111,7 +116,7 @@ span {
|
||||
color: white;
|
||||
text-transform: capitalize;
|
||||
font-weight: 400;
|
||||
font-size: 15px;
|
||||
font-size: 20px;
|
||||
letter-spacing: 1px;
|
||||
transition: background-color 200ms;
|
||||
opacity: 1;
|
||||
@@ -173,7 +178,7 @@ span {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 18px;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
max-width: 650px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
@@ -397,26 +402,6 @@ span {
|
||||
}
|
||||
}
|
||||
|
||||
.loadMoreContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
};
|
||||
|
||||
.loadMore {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #FFF;
|
||||
max-width: 660px;
|
||||
margin-bottom: 30px;
|
||||
background-color: #2376D8;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.loadMore:hover {
|
||||
background-color: #4399FF;
|
||||
}
|
||||
|
||||
.tabIcon {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
@@ -464,20 +449,20 @@ span {
|
||||
|
||||
.commentLeave {
|
||||
opacity: 1.0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.commentLeaveActive {
|
||||
opacity: 0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.commentEnter {
|
||||
opacity: 0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.commentEnterActive {
|
||||
opacity: 1.0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.editedMarker {
|
||||
@@ -490,7 +475,7 @@ span {
|
||||
|
||||
.searchTrigger {
|
||||
position: relative;
|
||||
top: .3em;
|
||||
top: .2em;
|
||||
}
|
||||
|
||||
.adminCommentInfoBar {
|
||||
@@ -499,4 +484,4 @@ span {
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user