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 | ||
|
|
d47287fbd5 | ||
|
|
74f778aef7 | ||
|
|
9566f83eac | ||
|
|
84e4091ea6 | ||
|
|
0ea2660c97 | ||
|
|
7125250cd6 | ||
|
|
5036a2665a | ||
|
|
b2e871f4a6 | ||
|
|
77b7ddf337 | ||
|
|
3804a7afae | ||
|
|
d38dbc87b6 | ||
|
|
a7a80a452b | ||
|
|
6c19b90652 | ||
|
|
686eb614f7 | ||
|
|
a9abd55895 | ||
|
|
165d07cae9 | ||
|
|
d4536cb1a2 | ||
|
|
b126cc3cb3 |
+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,4 +1,5 @@
|
||||
# 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)
|
||||
|
||||
@@ -19,7 +20,7 @@ endpoint when the server is running with built assets.
|
||||
|
||||
- Blog: https://blog.coralproject.net
|
||||
|
||||
- Community Guides for Journalism: https://guides.coralproject.net/
|
||||
- Community Guides for Journalism: https://guides.coralproject.net/
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
+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';
|
||||
@@ -47,6 +47,14 @@ const routes = (
|
||||
</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) => {
|
||||
export const updateAssetState = (id, closedAt) => (dispatch, _, {rest}) => {
|
||||
dispatch({type: UPDATE_ASSET_STATE_REQUEST, id, closedAt});
|
||||
return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {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());
|
||||
@@ -106,10 +105,10 @@ export const submitUser = () => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const finishInstall = () => (dispatch, getState) => {
|
||||
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) => {
|
||||
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}>
|
||||
|
||||
@@ -17,6 +17,7 @@ export default ({suspectWords, bannedWords, body, ...rest}) => {
|
||||
return (
|
||||
<Highlighter
|
||||
{...rest}
|
||||
autoEscape={true}
|
||||
searchWords={searchWords}
|
||||
textToHighlight={body}
|
||||
/>
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
color: white;
|
||||
background: grey;
|
||||
box-sizing: border-box;
|
||||
padding: 0px 5px;
|
||||
border-radius: 2px;
|
||||
padding: 2px 5px;
|
||||
font-size: 12px;
|
||||
height: 24px;
|
||||
letter-spacing: 0.4px;
|
||||
margin-bottom: 1px;
|
||||
line-height: 22px;
|
||||
|
||||
> i {
|
||||
font-size: 14px;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -139,29 +139,29 @@ export default class UserDetail extends React.Component {
|
||||
<hr/>
|
||||
{
|
||||
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>
|
||||
@@ -190,7 +190,7 @@ export default class UserDetail extends React.Component {
|
||||
className={styles.loadMore}
|
||||
loadMore={loadMore}
|
||||
showLoadMore={hasNextPage}
|
||||
/>
|
||||
/>
|
||||
</Drawer>
|
||||
</ClickOutside>
|
||||
);
|
||||
|
||||
@@ -55,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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
|
||||
@@ -88,13 +88,13 @@ class UserDetailContainer extends React.Component {
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoadingMore = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.isLoadingMore = false;
|
||||
throw err;
|
||||
});
|
||||
.then(() => {
|
||||
this.isLoadingMore = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.isLoadingMore = false;
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
@@ -124,7 +124,7 @@ class UserDetailContainer extends React.Component {
|
||||
}
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Date, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
);
|
||||
|
||||
@@ -5,14 +5,17 @@ const initialState = {
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
currentUser: state.auth.user,
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -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>
|
||||
|
||||
@@ -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/${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>
|
||||
? 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/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>
|
||||
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/${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>
|
||||
? 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,15 +42,15 @@ 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 {
|
||||
|
||||
+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;
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -66,7 +67,7 @@ class Comment extends React.Component {
|
||||
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 queryData = {root, comment, asset: comment.asset};
|
||||
|
||||
@@ -90,8 +91,8 @@ 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
|
||||
}
|
||||
{currentUserId !== comment.user.id &&
|
||||
<ActionsMenu icon="not_interested">
|
||||
@@ -107,6 +108,7 @@ class Comment extends React.Component {
|
||||
</ActionsMenu>
|
||||
}
|
||||
<div className={styles.adminCommentInfoBar}>
|
||||
{comment.hasParent && <ReplyBadge/>}
|
||||
<CommentType type={commentType} className={styles.commentType}/>
|
||||
<Slot
|
||||
fill="adminCommentInfoBar"
|
||||
@@ -190,10 +192,10 @@ class Comment extends React.Component {
|
||||
/>
|
||||
{flagActions && flagActions.length
|
||||
? <FlagBox
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
: null}
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -36,8 +36,8 @@ const ModerationMenu = ({
|
||||
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>
|
||||
|
||||
@@ -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 {
|
||||
@@ -444,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 {
|
||||
|
||||
@@ -37,6 +37,7 @@ export default withFragments({
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
__typename
|
||||
}
|
||||
}
|
||||
actions {
|
||||
@@ -48,11 +49,14 @@ export default withFragments({
|
||||
id
|
||||
username
|
||||
}
|
||||
__typename
|
||||
}
|
||||
__typename
|
||||
}
|
||||
editing {
|
||||
edited
|
||||
}
|
||||
hasParent
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -26,25 +26,27 @@ import {
|
||||
storySearchChange,
|
||||
clearState
|
||||
} from 'actions/moderation';
|
||||
import withQueueConfig from '../hoc/withQueueConfig';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
|
||||
import {Spinner} from 'coral-ui';
|
||||
import Moderation from '../components/Moderation';
|
||||
import Comment from './Comment';
|
||||
import queueConfig from '../queueConfig';
|
||||
import baseQueueConfig from '../queueConfig';
|
||||
|
||||
function prepareNotificationText(text) {
|
||||
return truncate(text, {length: 50}).replace('\n', ' ');
|
||||
}
|
||||
|
||||
function getAssetId(props) {
|
||||
if (props.params.tabOrId && !(props.params.tabOrId in queueConfig)) {
|
||||
if (props.params.tabOrId && !(props.params.tabOrId in props.queueConfig)) {
|
||||
return props.params.tabOrId;
|
||||
}
|
||||
return props.params.id || null;
|
||||
}
|
||||
|
||||
function getTab(props) {
|
||||
if (props.params.tabOrId && props.params.tabOrId in queueConfig) {
|
||||
if (props.params.tabOrId && props.params.tabOrId in props.queueConfig) {
|
||||
return props.params.tabOrId;
|
||||
}
|
||||
return props.params.tab || null;
|
||||
@@ -53,8 +55,15 @@ function getTab(props) {
|
||||
class ModerationContainer extends Component {
|
||||
subscriptions = [];
|
||||
|
||||
handleCommentChange = (root, comment, notify) => {
|
||||
return handleCommentChange(root, comment, this.props.data.variables.sort, notify, queueConfig, this.activeTab);
|
||||
handleCommentChange = (root, comment, notifyText) => {
|
||||
return handleCommentChange(
|
||||
root,
|
||||
comment,
|
||||
this.props.data.variables.sortOrder,
|
||||
() => notifyText && this.props.notify('info', notifyText),
|
||||
this.props.queueConfig,
|
||||
this.activeTab
|
||||
);
|
||||
};
|
||||
|
||||
get activeTab() {
|
||||
@@ -78,10 +87,10 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const notify = this.props.auth.user.id === user.id
|
||||
const notifyText = this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notify);
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -90,10 +99,10 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const notify = this.props.auth.user.id === user.id
|
||||
const notifyText = this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notify);
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -101,8 +110,8 @@ class ModerationContainer extends Component {
|
||||
document: COMMENT_EDITED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => {
|
||||
const notify = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notify);
|
||||
const notifyText = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -111,8 +120,8 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => {
|
||||
const user = comment.actions[comment.actions.length - 1].user;
|
||||
const notify = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notify);
|
||||
const notifyText = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -159,10 +168,10 @@ class ModerationContainer extends Component {
|
||||
const variables = {
|
||||
limit: 10,
|
||||
cursor: this.props.root[tab].endCursor,
|
||||
sort: this.props.data.variables.sort,
|
||||
sortOrder: this.props.data.variables.sortOrder,
|
||||
asset_id: this.props.data.variables.asset_id,
|
||||
statuses: queueConfig[tab].statuses,
|
||||
action_type: queueConfig[tab].action_type,
|
||||
statuses: this.props.queueConfig[tab].statuses,
|
||||
action_type: this.props.queueConfig[tab].action_type,
|
||||
};
|
||||
return this.props.data.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
@@ -199,17 +208,22 @@ class ModerationContainer extends Component {
|
||||
// Still loading.
|
||||
return <Spinner />;
|
||||
}
|
||||
} else if(asset !== undefined || !('premodCount' in root)) {
|
||||
} else if (asset !== undefined || !('premodCount' in root)) {
|
||||
|
||||
// loading.
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const premodEnabled = assetId ? isPremod(asset.settings.moderation) : isPremod(settings.moderation);
|
||||
const currentQueueConfig = Object.assign({}, queueConfig);
|
||||
if (premodEnabled) {
|
||||
const premodEnabled = assetId ? isPremod(asset.settings.moderation) :
|
||||
isPremod(settings.moderation);
|
||||
|
||||
const currentQueueConfig = Object.assign({}, this.props.queueConfig);
|
||||
|
||||
if (premodEnabled && root.newCount === 0) {
|
||||
delete currentQueueConfig.new;
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!premodEnabled && root.premodCount === 0) {
|
||||
delete currentQueueConfig.premod;
|
||||
}
|
||||
|
||||
@@ -279,8 +293,8 @@ const COMMENT_REJECTED_SUBSCRIPTION = gql`
|
||||
`;
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort, action_type: $action_type}) {
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type}) {
|
||||
nodes {
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
}
|
||||
@@ -304,15 +318,15 @@ const commentConnectionFragment = gql`
|
||||
${Comment.fragments.comment}
|
||||
`;
|
||||
|
||||
const withModQueueQuery = withQuery(gql`
|
||||
query CoralAdmin_Moderation($asset_id: ID, $sort: SORT_ORDER, $allAssets: Boolean!) {
|
||||
const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
query CoralAdmin_Moderation($asset_id: ID, $sortOrder: SORT_ORDER, $allAssets: Boolean!) {
|
||||
${Object.keys(queueConfig).map((queue) => `
|
||||
${queue}: comments(query: {
|
||||
${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''}
|
||||
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
|
||||
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
|
||||
asset_id: $asset_id,
|
||||
sort: $sort
|
||||
sortOrder: $sortOrder
|
||||
}) {
|
||||
...CoralAdmin_Moderation_CommentConnection
|
||||
}
|
||||
@@ -345,14 +359,15 @@ const withModQueueQuery = withQuery(gql`
|
||||
return {
|
||||
variables: {
|
||||
asset_id: id,
|
||||
sort: props.moderation.sortOrder,
|
||||
allAssets: id === null
|
||||
}
|
||||
sortOrder: props.moderation.sortOrder,
|
||||
allAssets: id === null,
|
||||
},
|
||||
fetchPolicy: 'network-only'
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const withQueueCountPolling = withQuery(gql`
|
||||
const withQueueCountPolling = withQuery(({queueConfig}) => gql`
|
||||
query CoralAdmin_ModerationCountPoll($asset_id: ID) {
|
||||
${Object.keys(queueConfig).map((queue) => `
|
||||
${queue}Count: commentCount(query: {
|
||||
@@ -393,11 +408,13 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
viewUserDetail,
|
||||
setSortOrder,
|
||||
storySearchChange,
|
||||
clearState
|
||||
clearState,
|
||||
notify,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
withQueueConfig(baseQueueConfig),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSetCommentStatus,
|
||||
withQueueCountPolling,
|
||||
|
||||
@@ -98,14 +98,14 @@ export const withAssetSearchQuery = withQuery(gql`
|
||||
}
|
||||
}
|
||||
`, {
|
||||
options: ({moderation: {storySearchString = ''}}) => {
|
||||
return {
|
||||
variables: {
|
||||
value: storySearchString
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
options: ({moderation: {storySearchString = ''}}) => {
|
||||
return {
|
||||
variables: {
|
||||
value: storySearchString
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import update from 'immutability-helper';
|
||||
import * as notification from 'coral-admin/src/services/notification';
|
||||
|
||||
const limit = 10;
|
||||
|
||||
@@ -29,29 +28,29 @@ function removeCommentFromQueue(root, queue, id) {
|
||||
});
|
||||
}
|
||||
|
||||
function shouldCommentBeAdded(root, queue, comment, sort) {
|
||||
function shouldCommentBeAdded(root, queue, comment, sortOrder) {
|
||||
if (root[`${queue}Count`] < limit) {
|
||||
|
||||
// Adding all comments until first limit has reached.
|
||||
return true;
|
||||
}
|
||||
const cursor = new Date(root[queue].endCursor);
|
||||
return sort === 'CHRONOLOGICAL'
|
||||
return sortOrder === 'ASC'
|
||||
? new Date(comment.created_at) <= cursor
|
||||
: new Date(comment.created_at) >= cursor;
|
||||
}
|
||||
|
||||
function addCommentToQueue(root, queue, comment, sort) {
|
||||
function addCommentToQueue(root, queue, comment, sortOrder) {
|
||||
if (queueHasComment(root, queue, comment.id)) {
|
||||
return root;
|
||||
}
|
||||
|
||||
const sortAlgo = sort === 'CHRONOLOGICAL' ? ascending : descending;
|
||||
const sortAlgo = sortOrder === 'ASC' ? ascending : descending;
|
||||
const changes = {
|
||||
[`${queue}Count`]: {$set: root[`${queue}Count`] + 1},
|
||||
};
|
||||
|
||||
if (shouldCommentBeAdded(root, queue, comment, sort)) {
|
||||
if (shouldCommentBeAdded(root, queue, comment, sortOrder)) {
|
||||
const nodes = root[queue].nodes.concat(comment).sort(sortAlgo);
|
||||
changes[queue] = {
|
||||
nodes: {$set: nodes},
|
||||
@@ -70,15 +69,15 @@ function getCommentQueues(comment, queueConfig) {
|
||||
const queues = [];
|
||||
Object.keys(queueConfig).forEach((key) => {
|
||||
const {action_type, statuses, tags} = queueConfig[key];
|
||||
let addToQueues = false;
|
||||
if (statuses && statuses.indexOf(comment.status) >= 0) {
|
||||
addToQueues = true;
|
||||
let addToQueues = true;
|
||||
if (statuses && statuses.indexOf(comment.status) === -1) {
|
||||
addToQueues = false;
|
||||
}
|
||||
if (tags && comment.tags && comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0)) {
|
||||
addToQueues = true;
|
||||
if (tags && (!comment.tags || !comment.tags.some((tagLink) => tags.indexOf(tagLink.tag.name) >= 0))) {
|
||||
addToQueues = false;
|
||||
}
|
||||
if (action_type && comment.actions && comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type}action`)) {
|
||||
addToQueues = true;
|
||||
if (action_type && (!comment.actions || !comment.actions.some((a) => a.__typename.toLowerCase() === `${action_type.toLowerCase()}action`))) {
|
||||
addToQueues = false;
|
||||
}
|
||||
if (addToQueues) {
|
||||
queues.push(key);
|
||||
@@ -91,16 +90,13 @@ function getCommentQueues(comment, queueConfig) {
|
||||
* Assimilate comment changes into current store.
|
||||
* @param {Object} root current state of the store
|
||||
* @param {Object} comment comment that was changed
|
||||
* @param {string} sort current sort order of the queues
|
||||
* @param {Object} [notify] show know notifications if set
|
||||
* @param {string} notify.activeQueue current active queue
|
||||
* @param {string} notify.text notification text to show
|
||||
* @param {bool} notify.anyQueue if true show the notification when the comment is shown
|
||||
* @param {string} sortOrder current sort order of the queues
|
||||
* @param {string} notify callback to show notification
|
||||
* in the current active queue besides the 'all' queue.
|
||||
* @param {Object} queueConfig queue configuration
|
||||
* @return {Object} next state of the store
|
||||
*/
|
||||
export function handleCommentChange(root, comment, sort, notify, queueConfig, activeQueue) {
|
||||
export function handleCommentChange(root, comment, sortOrder, notify, queueConfig, activeQueue) {
|
||||
let next = root;
|
||||
|
||||
const nextQueues = getCommentQueues(comment, queueConfig);
|
||||
@@ -110,22 +106,22 @@ export function handleCommentChange(root, comment, sort, notify, queueConfig, ac
|
||||
if (notificationShown) {
|
||||
return;
|
||||
}
|
||||
notification.info(notify);
|
||||
notify();
|
||||
notificationShown = true;
|
||||
};
|
||||
|
||||
Object.keys(queueConfig).forEach((queue) => {
|
||||
if (nextQueues.indexOf(queue) >= 0) {
|
||||
if (!queueHasComment(next, queue, comment.id)) {
|
||||
next = addCommentToQueue(next, queue, comment, sort);
|
||||
if (notify && activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sort)) {
|
||||
showNotificationOnce(comment);
|
||||
next = addCommentToQueue(next, queue, comment, sortOrder);
|
||||
if (notify && activeQueue === queue && shouldCommentBeAdded(next, queue, comment, sortOrder)) {
|
||||
showNotificationOnce();
|
||||
}
|
||||
}
|
||||
} else if(queueHasComment(next, queue, comment.id)){
|
||||
next = removeCommentFromQueue(next, queue, comment.id);
|
||||
if (notify && activeQueue === queue) {
|
||||
showNotificationOnce(comment);
|
||||
showNotificationOnce();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +130,7 @@ export function handleCommentChange(root, comment, sort, notify, queueConfig, ac
|
||||
&& queueHasComment(next, queue, comment.id)
|
||||
&& activeQueue === queue
|
||||
) {
|
||||
showNotificationOnce(comment);
|
||||
showNotificationOnce();
|
||||
}
|
||||
});
|
||||
return next;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* WithQueueConfig takes a `queueConfig` parameter that is
|
||||
* passed down as a prop enriched with queue config data from plugins.
|
||||
*/
|
||||
export default (queueConfig) => hoistStatics((WrappedComponent) => {
|
||||
class WithQueueConfig extends React.Component {
|
||||
static contextTypes = {
|
||||
plugins: PropTypes.object,
|
||||
};
|
||||
|
||||
pluginsConfig = this.context.plugins.getModQueueConfigs();
|
||||
|
||||
render() {
|
||||
return <WrappedComponent
|
||||
{...this.props}
|
||||
queueConfig={{...queueConfig, ...this.pluginsConfig}}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
return WithQueueConfig;
|
||||
});
|
||||
@@ -1,17 +1,16 @@
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {getModQueueConfigs} from 'coral-framework/helpers/plugins';
|
||||
|
||||
export default {
|
||||
new: {
|
||||
statuses: ['NONE'],
|
||||
icon: 'question_answer',
|
||||
name: t('modqueue.new'),
|
||||
},
|
||||
premod: {
|
||||
statuses: ['PREMOD'],
|
||||
icon: 'access_time',
|
||||
name: t('modqueue.premod'),
|
||||
},
|
||||
new: {
|
||||
statuses: ['NONE', 'PREMOD'],
|
||||
icon: 'question_answer',
|
||||
name: t('modqueue.new'),
|
||||
},
|
||||
reported: {
|
||||
action_type: 'FLAG',
|
||||
statuses: ['NONE', 'PREMOD'],
|
||||
@@ -33,5 +32,4 @@ export default {
|
||||
icon: 'question_answer',
|
||||
name: t('modqueue.all'),
|
||||
},
|
||||
...getModQueueConfigs(),
|
||||
};
|
||||
|
||||
@@ -134,20 +134,20 @@ export default class Stories extends Component {
|
||||
<Radio value='closed'>{t('streams.closed')}</Radio>
|
||||
</RadioGroup>
|
||||
<div className={styles.optionHeader}>{t('streams.sort_by')}</div>
|
||||
<RadioGroup
|
||||
name='sort by'
|
||||
value={sort}
|
||||
childContainer='div'
|
||||
onChange={this.onSettingChange('sort')}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value='desc'>{t('streams.newest')}</Radio>
|
||||
<Radio value='asc'>{t('streams.oldest')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<RadioGroup
|
||||
name='sort by'
|
||||
value={sort}
|
||||
childContainer='div'
|
||||
onChange={this.onSettingChange('sort')}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value='desc'>{t('streams.newest')}</Radio>
|
||||
<Radio value='asc'>{t('streams.oldest')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
{
|
||||
assetsIds.length
|
||||
? <div className={styles.mainContent}>
|
||||
? <div className={styles.mainContent}>
|
||||
<DataTable className={styles.streamsTable} rows={assetsIds} onClick={this.goToModeration}>
|
||||
<TableHeader name="title" cellFormatter={this.renderTitle}>{t('streams.article')}</TableHeader>
|
||||
<TableHeader name="publication_date" cellFormatter={this.renderDate}>
|
||||
@@ -162,7 +162,7 @@ export default class Stories extends Component {
|
||||
page={this.state.page}
|
||||
onNewPageHandler={this.onPageClick} />
|
||||
</div>
|
||||
: <EmptyCard>{t('streams.empty_result')}</EmptyCard>
|
||||
: <EmptyCard>{t('streams.empty_result')}</EmptyCard>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {toast} from 'react-toastify';
|
||||
|
||||
export function success(msg) {
|
||||
return toast(msg, {type: 'success'});
|
||||
}
|
||||
|
||||
export function error(msg) {
|
||||
return toast(msg, {type: 'error'});
|
||||
}
|
||||
|
||||
export function info(msg) {
|
||||
return toast(msg, {type: 'info'});
|
||||
}
|
||||
|
||||
export function showMutationErrors(error) {
|
||||
console.error(error);
|
||||
if (error.errors) {
|
||||
error.errors.forEach((err) => {
|
||||
toast(
|
||||
err.translation_key ? t(`error.${err.translation_key}`) : err,
|
||||
{type: 'error'}
|
||||
);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* createNotificationService returns a notification services based on toast.
|
||||
* @param {Object} toast
|
||||
* @return {Object} notification service
|
||||
*/
|
||||
export function createNotificationService(toast) {
|
||||
return {
|
||||
success(msg) {
|
||||
toast(msg, {type: 'success'});
|
||||
},
|
||||
error(msg) {
|
||||
toast(msg, {type: 'error'});
|
||||
},
|
||||
info(msg) {
|
||||
toast(msg, {type: 'info'});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from '../reducers';
|
||||
import {getClient} from 'coral-framework/services/client';
|
||||
|
||||
const middlewares = [
|
||||
applyMiddleware(getClient().middleware()),
|
||||
applyMiddleware(thunk)
|
||||
];
|
||||
|
||||
if (window.devToolsExtension) {
|
||||
|
||||
// we can't have the last argument of compose() be undefined
|
||||
middlewares.push(window.devToolsExtension());
|
||||
}
|
||||
|
||||
const coralReducers = {
|
||||
...mainReducer,
|
||||
apollo: getClient().reducer()
|
||||
};
|
||||
|
||||
const store = createStore(
|
||||
combineReducers(coralReducers),
|
||||
{},
|
||||
compose(...middlewares)
|
||||
);
|
||||
|
||||
store.coralReducers = coralReducers;
|
||||
|
||||
window.coralStore = store;
|
||||
export default store;
|
||||
@@ -12,10 +12,6 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.wrapper ul ul {
|
||||
padding-left: 20px
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
vertical-align: top;
|
||||
margin: 12px 12px 12px 0;
|
||||
@@ -36,4 +32,4 @@
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import {Button, Checkbox, TextField} from 'coral-ui';
|
||||
import {Button, Checkbox} from 'coral-ui';
|
||||
import QuestionBoxBuilder from './QuestionBoxBuilder';
|
||||
|
||||
import styles from './ConfigureCommentStream.css';
|
||||
|
||||
@@ -55,17 +56,14 @@ export default ({handleChange, handleApply, changed, ...props}) => (
|
||||
title: t('configure.enable_questionbox'),
|
||||
description: t('configure.enable_questionbox_description')
|
||||
}} />
|
||||
<div className={`${props.questionBoxEnable ? null : styles.hidden}`} >
|
||||
<TextField
|
||||
id="qboxcontent"
|
||||
onChange={handleChange}
|
||||
rows={3}
|
||||
value={props.questionBoxContent}
|
||||
label={t('configure.include_question_here')}
|
||||
{
|
||||
props.questionBoxEnable && <QuestionBoxBuilder
|
||||
questionBoxIcon={props.questionBoxIcon}
|
||||
questionBoxContent={props.questionBoxContent}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
.iconBubble{
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 10px;
|
||||
color: #9E9E9E;
|
||||
font-size: 24px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.iconPerson{
|
||||
z-index: 2;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
position: absolute;
|
||||
font-size: 33px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.qbIconContainer {
|
||||
position: relative;
|
||||
border: 0;
|
||||
color: white;
|
||||
display: inline-block;
|
||||
padding: 5px 20px;
|
||||
vertical-align: middle;
|
||||
width: 10px;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import styles from './DefaultIcon.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
const DefaultIcon = ({className}) => (
|
||||
<div className={cn(styles.qbIconContainer, className)}>
|
||||
<Icon name="chat_bubble" className={cn(styles.iconBubble)} />
|
||||
<Icon name="person" className={cn(styles.iconPerson)} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default DefaultIcon;
|
||||
@@ -0,0 +1,45 @@
|
||||
.qbBuilder {
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
.qbItemIconList {
|
||||
padding: 0;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.qbItemIcon {
|
||||
background: #F0F0F0;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
line-height: 45px;
|
||||
color: #252525;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
border: solid 2px #F0F0F0;
|
||||
transition: border 0.3s cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
|
||||
.qbItemIcon:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.qbItemIconActive {
|
||||
border: solid 2px #00796B;
|
||||
}
|
||||
|
||||
.defaultIcon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.qb {
|
||||
margin: 10px 0;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import React from 'react';
|
||||
import QuestionBox from 'talk-plugin-questionbox/QuestionBox';
|
||||
import {Icon, Spinner} from 'coral-ui';
|
||||
import DefaultIcon from './DefaultIcon';
|
||||
import cn from 'classnames';
|
||||
import styles from './QuestionBoxBuilder.css';
|
||||
|
||||
class QuestionBoxBuilder extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.loadEditor();
|
||||
}
|
||||
|
||||
async loadEditor() {
|
||||
const MarkdownEditor = await import('coral-framework/components/MarkdownEditor');
|
||||
|
||||
return this.setState({
|
||||
loading : false,
|
||||
MarkdownEditor
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {handleChange, questionBoxIcon, questionBoxContent} = this.props;
|
||||
const {loading, MarkdownEditor} = this.state;
|
||||
|
||||
if (loading) {
|
||||
return <Spinner/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.qbBuilder}>
|
||||
<h4>Include an Icon</h4>
|
||||
|
||||
<ul className={styles.qbItemIconList}>
|
||||
<li className={cn(
|
||||
styles.qbItemIcon,
|
||||
{[styles.qbItemIconActive]: questionBoxIcon === 'default'}
|
||||
)}
|
||||
id="qboxicon"
|
||||
onClick={handleChange}
|
||||
data-icon="default" >
|
||||
<DefaultIcon className={styles.defaultIcon} />
|
||||
</li>
|
||||
<li className={cn(
|
||||
styles.qbItemIcon,
|
||||
{[styles.qbItemIconActive]: questionBoxIcon === 'forum'}
|
||||
)}
|
||||
id="qboxicon"
|
||||
onClick={handleChange}
|
||||
data-icon="forum" >
|
||||
<Icon name="forum" />
|
||||
</li>
|
||||
<li className={cn(
|
||||
styles.qbItemIcon,
|
||||
{[styles.qbItemIconActive]: questionBoxIcon === 'build'}
|
||||
)}
|
||||
id="qboxicon"
|
||||
onClick={handleChange}
|
||||
data-icon="build" >
|
||||
<Icon name="build" />
|
||||
</li>
|
||||
<li className={cn(
|
||||
styles.qbItemIcon,
|
||||
{[styles.qbItemIconActive]: questionBoxIcon === 'format_quote'}
|
||||
)}
|
||||
id="qboxicon"
|
||||
onClick={handleChange}
|
||||
data-icon="format_quote" >
|
||||
<Icon name="format_quote" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<QuestionBox
|
||||
className={styles.qb}
|
||||
enable={true}
|
||||
icon={questionBoxIcon}
|
||||
content={questionBoxContent}
|
||||
/>
|
||||
|
||||
<MarkdownEditor
|
||||
value={questionBoxContent}
|
||||
onChange={(value) => handleChange({}, {questionBoxContent: value})}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default QuestionBoxBuilder;
|
||||
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {compose} from 'react-apollo';
|
||||
|
||||
import {updateOpenStatus, updateConfiguration} from 'coral-framework/actions/asset';
|
||||
import {updateOpenStatus, updateConfiguration} from 'coral-embed-stream/src/actions/asset';
|
||||
|
||||
import CloseCommentsInfo from '../components/CloseCommentsInfo';
|
||||
import ConfigureCommentStream from '../components/ConfigureCommentStream';
|
||||
@@ -27,10 +27,9 @@ class ConfigureStreamContainer extends Component {
|
||||
handleApply (e) {
|
||||
e.preventDefault();
|
||||
const {elements} = e.target;
|
||||
const {questionBoxIcon, questionBoxContent} = this.state.dirtySettings;
|
||||
const premod = elements.premod.checked;
|
||||
const questionBoxEnable = elements.qboxenable.checked;
|
||||
const questionBoxContent = elements.qboxcontent.value;
|
||||
|
||||
const premodLinksEnable = elements.plinksenable.checked;
|
||||
const {changed} = this.state;
|
||||
|
||||
@@ -38,6 +37,7 @@ class ConfigureStreamContainer extends Component {
|
||||
moderation: premod ? 'PRE' : 'POST',
|
||||
questionBoxEnable,
|
||||
questionBoxContent,
|
||||
questionBoxIcon,
|
||||
premodLinksEnable
|
||||
};
|
||||
|
||||
@@ -51,14 +51,18 @@ class ConfigureStreamContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
const changes = {};
|
||||
handleChange (e, newChanges) {
|
||||
let changes = {};
|
||||
|
||||
if (changes) {
|
||||
changes = {...newChanges};
|
||||
}
|
||||
|
||||
if (e.target && e.target.id === 'qboxenable') {
|
||||
changes.questionBoxEnable = e.target.checked;
|
||||
}
|
||||
if (e.target && e.target.id === 'qboxcontent') {
|
||||
changes.questionBoxContent = e.target.value;
|
||||
if (e.currentTarget && e.currentTarget.id === 'qboxicon') {
|
||||
changes.questionBoxIcon = e.currentTarget.dataset.icon;
|
||||
}
|
||||
if (e.target && e.target.id === 'plinksenable') {
|
||||
changes.premodLinksEnable = e.target.value;
|
||||
@@ -105,12 +109,13 @@ class ConfigureStreamContainer extends Component {
|
||||
changed={this.state.changed}
|
||||
premodLinksEnable={dirtySettings.premodLinksEnable}
|
||||
premod={premod}
|
||||
questionBoxIcon={dirtySettings.questionBoxIcon}
|
||||
questionBoxEnable={dirtySettings.questionBoxEnable}
|
||||
questionBoxContent={dirtySettings.questionBoxContent}
|
||||
/>
|
||||
<hr />
|
||||
<h3>{closedAt === 'open' ? t('configure.close') : t('configure.open')} {t('configure.comment_stream')}</h3>
|
||||
{(closedAt === 'open' && closedTimeout) ? <p>{t('configure.comment_stream_will_close')} {this.getClosedIn()}.</p> : ''}
|
||||
{(closedAt === 'open' && closedTimeout) ? <p>{t('configure.comment_stream_will_close')} {this.getClosedIn()}.</p> : ''}
|
||||
<CloseCommentsInfo
|
||||
onClick={this.toggleStatus}
|
||||
status={closedAt}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import {Router, Route} from 'react-router';
|
||||
import {history} from 'coral-framework/helpers/router';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Embed from './containers/Embed';
|
||||
import {LoginContainer} from 'coral-sign-in/containers/LoginContainer';
|
||||
@@ -12,6 +12,14 @@ const routes = (
|
||||
</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;
|
||||
|
||||
+7
-8
@@ -1,6 +1,5 @@
|
||||
import * as actions from '../constants/asset';
|
||||
import coralApi from '../helpers/request';
|
||||
import {addNotification} from '../actions/notification';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -12,12 +11,12 @@ const updateAssetSettingsRequest = () => ({type: actions.UPDATE_ASSET_SETTINGS_R
|
||||
const updateAssetSettingsSuccess = (settings) => ({type: actions.UPDATE_ASSET_SETTINGS_SUCCESS, settings});
|
||||
const updateAssetSettingsFailure = (error) => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE, error});
|
||||
|
||||
export const updateConfiguration = (newConfig) => (dispatch, getState) => {
|
||||
export const updateConfiguration = (newConfig) => (dispatch, getState, {rest}) => {
|
||||
const assetId = getState().asset.id;
|
||||
dispatch(updateAssetSettingsRequest());
|
||||
coralApi(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig})
|
||||
rest(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig})
|
||||
.then(() => {
|
||||
dispatch(addNotification('success', t('framework.success_update_settings')));
|
||||
dispatch(notify('success', t('framework.success_update_settings')));
|
||||
dispatch(updateAssetSettingsSuccess(newConfig));
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -26,12 +25,12 @@ export const updateConfiguration = (newConfig) => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const updateOpenStream = (closedBody) => (dispatch, getState) => {
|
||||
export const updateOpenStream = (closedBody) => (dispatch, getState, {rest}) => {
|
||||
const assetId = getState().asset.id;
|
||||
dispatch(fetchAssetRequest());
|
||||
coralApi(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody})
|
||||
rest(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody})
|
||||
.then(() => {
|
||||
dispatch(addNotification('success', t('framework.success_update_settings')));
|
||||
dispatch(notify('success', t('framework.success_update_settings')));
|
||||
dispatch(fetchAssetSuccess(closedBody));
|
||||
})
|
||||
.catch((error) => {
|
||||
+41
-39
@@ -1,12 +1,8 @@
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import bowser from 'bowser';
|
||||
import * as actions from '../constants/auth';
|
||||
import * as Storage from '../helpers/storage';
|
||||
import coralApi, {base} from '../helpers/request';
|
||||
import pym from '../services/pym';
|
||||
import {addNotification} from '../actions/notification';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
|
||||
import {resetWebsocket} from 'coral-framework/services/client';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const showSignInDialog = () => ({
|
||||
@@ -59,9 +55,9 @@ export const updateUsername = ({username}) => ({
|
||||
username
|
||||
});
|
||||
|
||||
export const createUsername = (userId, formData) => (dispatch) => {
|
||||
export const createUsername = (userId, formData) => (dispatch, _, {rest}) => {
|
||||
dispatch(createUsernameRequest());
|
||||
coralApi('/account/username', {method: 'PUT', body: formData})
|
||||
rest('/account/username', {method: 'PUT', body: formData})
|
||||
.then(() => {
|
||||
dispatch(createUsernameSuccess());
|
||||
dispatch(hideCreateUsernameDialog());
|
||||
@@ -111,9 +107,11 @@ const signInFailure = (error) => ({
|
||||
// AUTH TOKEN
|
||||
//==============================================================================
|
||||
|
||||
export const handleAuthToken = (token) => (dispatch) => {
|
||||
Storage.setItem('exp', jwtDecode(token).exp);
|
||||
Storage.setItem('token', token);
|
||||
export const handleAuthToken = (token) => (dispatch, _, {storage}) => {
|
||||
if (storage) {
|
||||
storage.setItem('exp', jwtDecode(token).exp);
|
||||
storage.setItem('token', token);
|
||||
}
|
||||
|
||||
dispatch({type: 'HANDLE_AUTH_TOKEN'});
|
||||
};
|
||||
@@ -123,10 +121,10 @@ export const handleAuthToken = (token) => (dispatch) => {
|
||||
//==============================================================================
|
||||
|
||||
export const fetchSignIn = (formData) => {
|
||||
return (dispatch) => {
|
||||
return (dispatch, _, {rest}) => {
|
||||
dispatch(signInRequest());
|
||||
|
||||
return coralApi('/auth/local', {method: 'POST', body: formData})
|
||||
return rest('/auth/local', {method: 'POST', body: formData})
|
||||
.then(({token}) => {
|
||||
if (!bowser.safari && !bowser.ios) {
|
||||
dispatch(handleAuthToken(token));
|
||||
@@ -171,10 +169,10 @@ const signInFacebookFailure = (error) => ({
|
||||
error
|
||||
});
|
||||
|
||||
export const fetchSignInFacebook = () => (dispatch) => {
|
||||
export const fetchSignInFacebook = () => (dispatch, _, {rest}) => {
|
||||
dispatch(signInFacebookRequest());
|
||||
window.open(
|
||||
`${base}/auth/facebook`,
|
||||
`${rest.uri}/auth/facebook`,
|
||||
'Continue with Facebook',
|
||||
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
|
||||
);
|
||||
@@ -188,10 +186,10 @@ const signUpFacebookRequest = () => ({
|
||||
type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST
|
||||
});
|
||||
|
||||
export const fetchSignUpFacebook = () => (dispatch) => {
|
||||
export const fetchSignUpFacebook = () => (dispatch, _, {rest}) => {
|
||||
dispatch(signUpFacebookRequest());
|
||||
window.open(
|
||||
`${base}/auth/facebook`,
|
||||
`${rest.uri}/auth/facebook`,
|
||||
'Continue with Facebook',
|
||||
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
|
||||
);
|
||||
@@ -220,11 +218,11 @@ const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST});
|
||||
const signUpSuccess = (user) => ({type: actions.FETCH_SIGNUP_SUCCESS, user});
|
||||
const signUpFailure = (error) => ({type: actions.FETCH_SIGNUP_FAILURE, error});
|
||||
|
||||
export const fetchSignUp = (formData) => (dispatch, getState) => {
|
||||
export const fetchSignUp = (formData) => (dispatch, getState, {rest}) => {
|
||||
const redirectUri = getState().auth.redirectUri;
|
||||
dispatch(signUpRequest());
|
||||
|
||||
coralApi('/users', {
|
||||
rest('/users', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {'X-Pym-Url': redirectUri}
|
||||
@@ -256,10 +254,10 @@ const forgotPasswordFailure = (error) => ({
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchForgotPassword = (email) => (dispatch, getState) => {
|
||||
export const fetchForgotPassword = (email) => (dispatch, getState, {rest}) => {
|
||||
dispatch(forgotPasswordRequest(email));
|
||||
const redirectUri = getState().auth.redirectUri;
|
||||
coralApi('/account/password/reset', {
|
||||
rest('/account/password/reset', {
|
||||
method: 'POST',
|
||||
body: {email, loc: redirectUri}
|
||||
})
|
||||
@@ -275,16 +273,18 @@ export const fetchForgotPassword = (email) => (dispatch, getState) => {
|
||||
// LOGOUT
|
||||
//==============================================================================
|
||||
|
||||
export const logout = () => (dispatch) => {
|
||||
return coralApi('/auth', {method: 'DELETE'}).then(() => {
|
||||
Storage.removeItem('token');
|
||||
export const logout = () => async (dispatch, _, {rest, client, pym, storage}) => {
|
||||
await rest('/auth', {method: 'DELETE'});
|
||||
|
||||
// Reset the websocket.
|
||||
resetWebsocket();
|
||||
if (storage) {
|
||||
storage.removeItem('token');
|
||||
}
|
||||
|
||||
dispatch({type: actions.LOGOUT});
|
||||
pym.sendMessage('coral-auth-changed');
|
||||
});
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({type: actions.LOGOUT});
|
||||
pym.sendMessage('coral-auth-changed');
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
@@ -300,17 +300,19 @@ const checkLoginSuccess = (user, isAdmin) => ({
|
||||
isAdmin
|
||||
});
|
||||
|
||||
export const checkLogin = () => (dispatch) => {
|
||||
export const checkLogin = () => (dispatch, _, {rest, client, pym, storage}) => {
|
||||
dispatch(checkLoginRequest());
|
||||
coralApi('/auth')
|
||||
rest('/auth')
|
||||
.then((result) => {
|
||||
if (!result.user) {
|
||||
Storage.removeItem('token');
|
||||
if (storage) {
|
||||
storage.removeItem('token');
|
||||
}
|
||||
throw new Error('Not logged in');
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
resetWebsocket();
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch(checkLoginSuccess(result.user));
|
||||
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
|
||||
@@ -322,10 +324,10 @@ export const checkLogin = () => (dispatch) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
if (error.status && error.status === 401) {
|
||||
if (error.status && error.status === 401 && storage) {
|
||||
|
||||
// Unauthorized.
|
||||
Storage.removeItem('token');
|
||||
storage.removeItem('token');
|
||||
}
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
@@ -351,10 +353,10 @@ const verifyEmailFailure = () => ({
|
||||
type: actions.VERIFY_EMAIL_FAILURE
|
||||
});
|
||||
|
||||
export const requestConfirmEmail = (email) => (dispatch, getState) => {
|
||||
export const requestConfirmEmail = (email) => (dispatch, getState, {rest}) => {
|
||||
const redirectUri = getState().auth.redirectUri;
|
||||
dispatch(verifyEmailRequest());
|
||||
return coralApi('/users/resend-verify', {
|
||||
return rest('/users/resend-verify', {
|
||||
method: 'POST',
|
||||
body: {email},
|
||||
headers: {'X-Pym-Url': redirectUri}
|
||||
@@ -387,11 +389,11 @@ export const setRedirectUri = (uri) => ({
|
||||
const editUsernameFailure = (error) => ({type: actions.EDIT_USERNAME_FAILURE, error});
|
||||
const editUsernameSuccess = () => ({type: actions.EDIT_USERNAME_SUCCESS});
|
||||
|
||||
export const editName = (username) => (dispatch) => {
|
||||
return coralApi('/account/username', {method: 'PUT', body: {username}})
|
||||
export const editName = (username) => (dispatch, _, {rest}) => {
|
||||
return rest('/account/username', {method: 'PUT', body: {username}})
|
||||
.then(() => {
|
||||
dispatch(editUsernameSuccess());
|
||||
dispatch(addNotification('success', t('framework.success_name_update')));
|
||||
dispatch(notify('success', t('framework.success_name_update')));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user