Compare commits

...
698 Commits
Author SHA1 Message Date
Kim Gardner 869fb9d029 Merge pull request #1344 from coralproject/rem-semi
Remove semicolon
2018-02-05 11:30:19 -05:00
Chi Vinh Le cb077a87dc Remove semicolon 2018-02-05 17:22:13 +01:00
Wyatt Johnson 901c96a67a Merge pull request #1343 from mattmontgomery/patch-2
Changes node requirement notation
2018-02-03 13:10:22 -07:00
Matt Montgomery 6b5a690c4f Changes node requirement notation
This changes the notation for the `node` requirement. Solves #1342
2018-02-02 17:04:01 -07:00
Kim Gardner 1dfce22b89 Merge pull request #1341 from coralproject/fix-zen-mode
Fix zen mode
2018-02-02 10:00:46 -05:00
Kiwi e36098369b Merge branch 'master' into fix-zen-mode 2018-02-02 15:53:13 +01:00
Chi Vinh Le aec0251c4f Fix zen mode 2018-02-02 15:50:43 +01:00
Kim Gardner c9613337d4 Merge pull request #1339 from coralproject/fix-flag-issues
Fix flag issues
2018-02-02 09:27:28 -05:00
Chi Vinh Le ac67b0484b Better fix 2018-02-02 12:12:58 +01:00
Chi Vinh Le edac21a4f4 Fix Flag Menu not working 2018-02-02 11:52:42 +01:00
Kim Gardner 86f4ae4b6d Merge pull request #1336 from icco/patch-2
Change text wrapping of Stories table
2018-02-01 13:56:31 -05:00
Nat Welch 219fcdddea Change text wrapping of Stories table 2018-02-01 13:26:34 -05:00
Wyatt Johnson 6c9411d173 Merge pull request #1333 from coralproject/remove-auth-data
Cleanly remove auth data from storage
2018-02-01 10:12:48 -07:00
Chi Vinh Le 6afb300d86 Unlisten 2018-02-01 17:55:53 +01:00
Wyatt Johnson 1f6c29cee6 Merge branch 'master' into remove-auth-data 2018-02-01 09:21:53 -07:00
Wyatt Johnson 2234acc28e Merge pull request #1334 from leeeandroo/missing-path
Insert BASE_PATH on public assets
2018-02-01 09:21:38 -07:00
Wyatt Johnson 13e6645f26 Merge branch 'master' into remove-auth-data 2018-02-01 09:15:54 -07:00
Wyatt Johnson 09983e412d Merge branch 'master' into missing-path 2018-02-01 09:14:16 -07:00
Wyatt Johnson 3563f7e2ae Merge pull request #1332 from coralproject/moderator-configure
Moderator Configuration
2018-02-01 09:13:59 -07:00
Leandro c4b6cb54e6 Insert BASE_PATH on public assets 2018-02-01 13:27:17 +01:00
Chi Vinh Le d6ddd9c195 Clearly remove auth data from storage 2018-02-01 12:13:33 +01:00
Wyatt Johnson 596c214f1c allowed moderators to configure asset 2018-01-31 13:41:07 -07:00
Wyatt Johnson 9b7f1dc43e Merge pull request #1331 from coralproject/public-path
Dynamic Public Path
2018-01-31 12:33:30 -07:00
Wyatt Johnson c4cddefcff abstracted config to service 2018-01-31 11:25:43 -07:00
Wyatt Johnson 062ce58cf8 ensure dynamic imports use proper path/url 2018-01-31 10:57:01 -07:00
Wyatt Johnson 8b6a993587 Merge pull request #1329 from coralproject/docs
Docs update
2018-01-30 15:40:43 -07:00
Wyatt Johnson 26bd01cbda updated docs for 4.1 release 2018-01-30 15:29:36 -07:00
Kim Gardner f492040a57 Merge pull request #1328 from coralproject/user-perms
Username Permissions
2018-01-30 15:49:00 -05:00
Kim Gardner d7fd8ac714 Merge branch 'master' into user-perms 2018-01-30 13:58:42 -05:00
Kim Gardner 3e273a76f1 Merge pull request #1327 from coralproject/bump-version
Update version to 4.1.0 and update LICENSE
2018-01-30 13:58:29 -05:00
Kim Gardner 7504dc6dfe Merge branch 'master' into bump-version 2018-01-30 13:28:08 -05:00
Wyatt Johnson 0f9387dbb9 Merge branch 'master' into user-perms 2018-01-30 11:03:42 -07:00
Wyatt Johnson ec54376a53 blocked user actions in username change process 2018-01-30 11:00:48 -07:00
Kim Gardner 0793784f5a Merge pull request #1324 from coralproject/notify-on-edit
Refactor + Fix notify on edit bug
2018-01-30 12:58:47 -05:00
Kiwi 00a7b11039 Merge branch 'master' into notify-on-edit 2018-01-30 18:30:42 +01:00
Kiwi 5138e79424 Merge pull request #1325 from coralproject/fix-gender
Fix gender translation
2018-01-30 18:30:25 +01:00
Kim Gardner 00eff653cb Update version to 4.1.0 and update LICENSE 2018-01-30 12:21:10 -05:00
Chi Vinh Le a59464e45d Merge branch 'notify-on-edit' of ssh://github.com/coralproject/talk into notify-on-edit 2018-01-30 18:20:59 +01:00
Chi Vinh Le feeaa8ee8b Better todo descr 2018-01-30 18:19:31 +01:00
Kiwi 1c4b8a0a67 Merge branch 'master' into notify-on-edit 2018-01-30 18:10:47 +01:00
Kim Gardner 8ad5675916 Merge branch 'master' into fix-gender 2018-01-30 12:09:06 -05:00
Kim Gardner 7a7d45cf91 Merge pull request #1326 from coralproject/fix-ie11-sideactions
Add IE CSS fixes
2018-01-30 12:08:58 -05:00
Kiwi e77e62236c Merge branch 'master' into fix-ie11-sideactions 2018-01-30 17:57:04 +01:00
Kiwi 8e142a5ecb Merge branch 'master' into fix-gender 2018-01-30 17:56:31 +01:00
Chi Vinh Le 3d57122a65 Add IE CSS fixes 2018-01-30 17:56:15 +01:00
Chi Vinh Le 665d8c1ef0 Fix gender translation 2018-01-30 17:42:30 +01:00
Kiwi bd2c4d4bfc Merge branch 'master' into notify-on-edit 2018-01-30 17:37:15 +01:00
Chi Vinh Le 81e68c8ef4 Fix edited comment not showing notifications 2018-01-30 17:34:18 +01:00
Chi Vinh Le cb5a41f119 Refactor 2018-01-30 17:18:09 +01:00
Kim Gardner 082287387a Merge pull request #1321 from coralproject/username-flow-fix
Changed Username Moderation Flow
2018-01-30 11:14:33 -05:00
Kim Gardner 9143bd2b31 Merge branch 'master' into username-flow-fix 2018-01-30 10:31:58 -05:00
Kim Gardner 2def894237 Merge pull request #1322 from hellp/approve-reject-button-style
Make Approve/Reject button styles more robust for longer labels
2018-01-30 10:31:27 -05:00
Kim Gardner 7dd7f9ca54 Merge branch 'master' into approve-reject-button-style 2018-01-30 10:14:22 -05:00
Chi Vinh Le b33cde03c3 Removes some weird characters 2018-01-30 15:59:02 +01:00
Chi Vinh Le d80cd6f0a2 More Refactor 2018-01-30 15:49:28 +01:00
Chi Vinh Le ebb85d17ce Refactor 2018-01-30 15:13:14 +01:00
Fabian Neumann efe84f3f98 Make Approve/Reject button styles more robust for long texts.
For other languages than English the 'Approve' text did not fit
into the button. This commit makes the CSS a bit more flexible by
removing some very pixel-specfic styles.
2018-01-30 14:50:35 +01:00
Wyatt Johnson 335349963c adjusted flow for changed usernames, e2e 2018-01-29 17:46:03 -07:00
Kim Gardner 495e3b009f Merge branch 'master' into username-flow-fix 2018-01-29 19:21:08 -05:00
Kim Gardner 2d82f4c217 Merge pull request #1319 from coralproject/cleanup
User Deletion Cleanup
2018-01-29 19:20:53 -05:00
Kim Gardner 6a70077398 Merge branch 'master' into cleanup 2018-01-29 19:11:18 -05:00
Wyatt Johnson d25ca0ef6f more test criteria 2018-01-29 15:36:59 -07:00
Wyatt Johnson 985fc05a47 only users that have approved usernames may comment 2018-01-29 15:29:55 -07:00
Wyatt Johnson 73cbe4ab4b Merge pull request #1315 from coralproject/better-change-detection
Better handle comment edits
2018-01-29 14:54:00 -07:00
Wyatt Johnson 8d656b51a0 introduced count adjusters for deleting a user 2018-01-29 13:56:09 -07:00
Chi Vinh Le 0a889fd1b3 More Refactoring 2018-01-29 21:26:44 +01:00
Kim Gardner c18c43e5c8 Merge branch 'master' into better-change-detection 2018-01-29 15:01:45 -05:00
Kim Gardner c22f8d0dc0 Merge pull request #1317 from coralproject/edit-history
Cleanup to Edit/Premod logic
2018-01-29 14:57:27 -05:00
Chi Vinh Le 0799e9d8ee More refactor 2018-01-29 20:52:44 +01:00
Kim Gardner 9645ccb6cd Merge branch 'master' into better-change-detection 2018-01-29 14:40:48 -05:00
Kim Gardner be51704b1f Merge branch 'master' into edit-history 2018-01-29 14:40:44 -05:00
Kim Gardner 7e2d414f13 Merge pull request #1312 from coralproject/zen-mode
Rename "single comment view" to "zen mode"
2018-01-29 14:40:03 -05:00
Kiwi d2e87bcbda Merge branch 'master' into zen-mode 2018-01-29 20:32:08 +01:00
Kim Gardner 9d168c6af8 Merge pull request #1316 from coralproject/fix-hidden-moderator-commens
Fix moderator comments not showing up in premod
2018-01-29 14:17:35 -05:00
Kim Gardner 3a67768c51 Merge branch 'master' into fix-hidden-moderator-commens 2018-01-29 14:09:48 -05:00
Kim Gardner e3ccb8560b Merge pull request #1318 from coralproject/nsp
Removed NSP from CI
2018-01-29 14:09:36 -05:00
Chi Vinh Le c355e10453 Refactor 2018-01-29 19:21:42 +01:00
Wyatt Johnson e20e55b589 removed outdated logic 2018-01-29 10:35:10 -07:00
Wyatt Johnson c13ea699d9 turned off nsp on CI 2018-01-29 10:07:42 -07:00
Wyatt Johnson 873932b6a6 Merge branch 'master' into cleanup 2018-01-29 10:02:52 -07:00
Wyatt Johnson bc42539fc4 cleanup 2018-01-29 10:02:38 -07:00
Chi Vinh Le 5f608e9bc6 Fix moderator comments not showing up 2018-01-29 17:40:38 +01:00
Chi Vinh Le 2bfa99fa1a Remove unused proptype 2018-01-29 14:52:26 +01:00
Chi Vinh Le c12fe526b8 Better detect edits 2018-01-29 14:52:09 +01:00
Chi Vinh Le fc3bc0a542 Merge branch 'master' into better-change-detection 2018-01-29 14:21:02 +01:00
Chi Vinh Le 78019396e2 Use same created_at 2018-01-29 13:47:46 +01:00
Kim Gardner 7d31487275 Merge branch 'master' into zen-mode 2018-01-26 15:31:22 -05:00
Kim Gardner f5c3f7e9bc Merge pull request #1313 from coralproject/activity-dot-subscription
Port Activity dot polling to subscriptions
2018-01-26 15:31:01 -05:00
Kim Gardner 9e127d2300 Merge branch 'master' into activity-dot-subscription 2018-01-26 15:13:30 -05:00
Wyatt Johnson 172bc22af5 Merge branch 'master' into cleanup 2018-01-26 12:58:36 -07:00
Wyatt Johnson 4be9e74ed9 removed unused code 2018-01-26 12:58:16 -07:00
Wyatt Johnson f4d134d0e2 Merge pull request #1311 from coralproject/migration-updates
Migration updates
2018-01-26 12:57:08 -07:00
Wyatt Johnson 752825db68 Merge branch 'master' into migration-updates 2018-01-26 10:51:58 -07:00
Chi Vinh Le 9a36473c8f Merge branch 'activity-dot-subscription' of ssh://github.com/coralproject/talk into activity-dot-subscription 2018-01-26 17:30:16 +01:00
Chi Vinh Le 3fa2af975c Bug fixes 2018-01-26 17:28:21 +01:00
Wyatt Johnson 57a77a7edf enhanced flag migration 2018-01-25 19:21:00 -07:00
Wyatt Johnson 6caae2df97 applied migration performance improvements 2018-01-25 18:17:04 -07:00
Kiwi c00e534984 Merge branch 'master' into activity-dot-subscription 2018-01-26 01:49:04 +01:00
Chi Vinh Le b638b783fe Refactor 2018-01-26 01:42:46 +01:00
Kim Gardner de18ab1e15 Merge pull request #1310 from coralproject/plugin-cli
Plugin CLI Fix
2018-01-25 18:44:53 -05:00
Wyatt Johnson 75751285fe fixed default setup 2018-01-25 16:18:05 -07:00
Wyatt Johnson 04390c5acd migration rewrite and removed verifications 2018-01-25 16:12:26 -07:00
Chi Vinh Le ce25cfb159 Reconstruct previous comment state more accurately 2018-01-25 22:03:37 +01:00
Wyatt Johnson e8f73ddb87 changed copy 2018-01-25 13:20:16 -07:00
Wyatt Johnson e6f796e99c expanded on migration support 2018-01-25 13:07:29 -07:00
Kim Gardner 0507e750b9 Update translations 2018-01-25 15:00:34 -05:00
Kim Gardner fe4cddda60 Copy change 2018-01-25 14:51:15 -05:00
Kim Gardner 63347dc0eb Change s to z for zen mode shortcut and update copy and docs 2018-01-25 14:50:06 -05:00
Chi Vinh Le 426025f8c2 Subscribe to edited + bug fixes 2018-01-25 20:49:57 +01:00
Chi Vinh Le 6fd4d1576e Correct logic for disabling activity subscriptions 2018-01-25 20:08:57 +01:00
Chi Vinh Le 26ca3622c2 Integrate featured into activity indicator subscription 2018-01-25 20:01:25 +01:00
Wyatt Johnson ef7693afa5 fixed error in migration 2018-01-25 11:25:32 -07:00
Wyatt Johnson de9383e657 more extreme error exiting for rejected promise that are not handled 2018-01-25 10:17:26 -07:00
Wyatt Johnson 1e517469c3 Fail on plugin installation fail 2018-01-25 10:16:14 -07:00
Chi Vinh Le 66934cbb82 Implement indicator subscriptions for moderation 2018-01-25 16:45:19 +01:00
Chi Vinh Le 2246b16cf4 Add some comments 2018-01-24 23:34:53 +01:00
Chi Vinh Le 1fc07e8649 Implement activity tracking for flagged accounts 2018-01-24 23:31:31 +01:00
Wyatt Johnson 4e9b19c7f6 performance related improvements to migraitons 2018-01-24 15:00:30 -07:00
Wyatt Johnson a8f794ef76 Merge pull request #1306 from coralproject/error-notifications
Error handling improvements
2018-01-24 12:34:11 -07:00
Kim Gardner 6825b4c999 Merge branch 'master' into error-notifications 2018-01-24 14:22:52 -05:00
Kim Gardner 54fb334288 Merge pull request #1308 from coralproject/flagged-user-padding
Add padding to flagged user
2018-01-24 14:22:39 -05:00
Kim Gardner 798732cbcf Merge branch 'master' into flagged-user-padding 2018-01-24 13:54:34 -05:00
Kim Gardner e18d92944a Merge pull request #1309 from hellp/wrong-premod-text
Fix wrong text on "premod if contains links" setting
2018-01-24 13:54:04 -05:00
Kim Gardner f0498fb3ba Merge branch 'master' into flagged-user-padding 2018-01-24 13:45:56 -05:00
Kim Gardner 0f938b19a3 Merge branch 'master' into wrong-premod-text 2018-01-24 13:45:08 -05:00
Wyatt Johnson bcd18ccb32 Merge branch 'master' into error-notifications 2018-01-24 10:31:43 -07:00
Wyatt Johnson c6dd182f56 Merge pull request #1303 from coralproject/moderation-refactor
Moderation refactor
2018-01-24 10:31:31 -07:00
Kiwi cae974d0df Merge branch 'master' into moderation-refactor 2018-01-24 18:19:09 +01:00
Kiwi c41320e155 Merge branch 'master' into error-notifications 2018-01-24 18:18:52 +01:00
Kiwi 66ca985405 Merge branch 'master' into flagged-user-padding 2018-01-24 18:18:40 +01:00
Wyatt Johnson a366c6efa4 Merge pull request #1305 from coralproject/auth-callback
Auth Callback Regression
2018-01-24 10:18:03 -07:00
Wyatt Johnson 366f40a6e4 applied fix to template 2018-01-24 10:09:33 -07:00
Fabian Neumann 9da61b5991 Fix wrong text on "premod if contains links" setting. 2018-01-24 16:58:58 +01:00
Chi Vinh Le 9feeaf4dca Add padding to flagged user 2018-01-24 15:35:34 +01:00
Kiwi 7f9e09461c Merge branch 'master' into error-notifications 2018-01-23 20:41:17 +01:00
Chi Vinh Le d15694041a Don't require notify property 2018-01-23 20:27:46 +01:00
Chi Vinh Le 59cdc9fd5d Port plugins 2018-01-23 20:06:55 +01:00
Chi Vinh Le b196bb379a Comments 2018-01-23 19:52:04 +01:00
Chi Vinh Le c7242f5d98 Improve error handling of report feature 2018-01-23 19:47:11 +01:00
Chi Vinh Le ace6cbb158 Error reporting for the rest 2018-01-23 19:41:00 +01:00
Chi Vinh Le 0523faee2b withQuery and withMutation support for notifyOnError (default: true) 2018-01-23 19:05:08 +01:00
Wyatt Johnson e4834027b0 Auth Callback Regression 2018-01-23 11:03:53 -07:00
Chi Vinh Le 303e2e84c5 Notify on error (embed stream -> profile) 2018-01-23 17:37:52 +01:00
Chi Vinh Le 04fd056cab Notify on errors (embed stream - configure) 2018-01-23 17:36:07 +01:00
Chi Vinh Le 74b545a8d9 Refetch configure and notify on error 2018-01-23 16:58:11 +01:00
Chi Vinh Le 957fd34168 Show error message on data error 2018-01-23 16:53:03 +01:00
Chi Vinh Le dd836ad28e Notify on error (Community > Reported Username) 2018-01-23 16:45:32 +01:00
Chi Vinh Le 196ae5e29c Notify on error (Communit > People) 2018-01-23 16:41:44 +01:00
Chi Vinh Le f4e4c1d810 Notify on error (UserDetail) 2018-01-23 16:27:02 +01:00
Wyatt Johnson f3469f3dc4 Merge branch 'master' into moderation-refactor 2018-01-22 16:34:56 -07:00
Wyatt Johnson d101989ebd Merge pull request #1298 from coralproject/live-reported
Implement Live Updates for Reported Usernames
2018-01-22 16:30:46 -07:00
Wyatt Johnson b36c70acc1 replaced pre-git with pre-commit 2018-01-22 16:16:04 -07:00
Wyatt Johnson 25359ee88d Moderation Refactor 2018-01-22 16:11:13 -07:00
Chi Vinh Le 6a06cb4684 Show notification when encountering an error in modqueue 2018-01-22 22:42:17 +01:00
Kiwi 12652d59c4 Merge branch 'master' into live-reported 2018-01-22 20:01:12 +01:00
Wyatt Johnson 8e8458781d Merge pull request #1302 from coralproject/plugin-directory
Plugin Working Directory
2018-01-22 11:50:45 -07:00
Kiwi 852b21e264 Merge branch 'master' into plugin-directory 2018-01-22 19:42:05 +01:00
Kim Gardner 81d049e533 Merge branch 'master' into live-reported 2018-01-22 13:33:15 -05:00
Kim Gardner 3fe8a8fb8d Merge pull request #1301 from coralproject/fix-report-bugs
Fixes for report dialog
2018-01-22 13:33:01 -05:00
Kim Gardner 0f6cc20478 Update error message 2018-01-22 13:08:28 -05:00
Wyatt Johnson 26c109e8ae Removed cwd in place of __dirname 2018-01-22 11:06:49 -07:00
Chi Vinh Le 1e34af6aa3 Fixes for report dialog 2018-01-22 18:20:36 +01:00
Kiwi 60c62e4f1a Merge branch 'master' into live-reported 2018-01-22 17:46:49 +01:00
Kim Gardner 30dddf387c Merge pull request #1299 from coralproject/fix-modqueue-counts
Fix modqueue count bugs
2018-01-22 11:31:42 -05:00
Kiwi 0528106fdf Merge branch 'master' into live-reported 2018-01-22 17:16:08 +01:00
Kim Gardner bdd207524c Merge branch 'master' into fix-modqueue-counts 2018-01-22 11:09:40 -05:00
Kim Gardner 4be19fd028 Merge pull request #1300 from coralproject/fix-false-spinner
Don't show spinner on fetchMore
2018-01-22 11:08:59 -05:00
Kim Gardner 66202d1fd6 Merge branch 'master' into fix-false-spinner 2018-01-22 10:51:49 -05:00
Kim Gardner d98a76272c Merge pull request #1280 from sanoma/master
Dutch translation
2018-01-22 10:51:00 -05:00
Chi Vinh Le 15d0ecbe51 Don't show spinner on fetchMore 2018-01-22 14:34:30 +01:00
Tibo Beijen ca653fd5ff Added missing translations, added since start of nl translations. 2018-01-22 14:13:44 +01:00
Tibo Beijen 468a1683ce Small lintin fix. 2018-01-22 11:22:12 +01:00
Tibo Beijen d827ebef72 Merge branch 'master' into sanoma-master 2018-01-22 09:29:22 +01:00
Kiwi 65588f234a Merge branch 'master' into live-reported 2018-01-19 22:07:20 +01:00
Kiwi bb2ea77008 Merge branch 'master' into fix-modqueue-counts 2018-01-19 21:52:13 +01:00
Chi Vinh Le 392c5511eb Fix modqueue counts 2018-01-19 21:15:44 +01:00
Wyatt Johnson a9be049bf6 Merge pull request #1296 from coralproject/user-json
Auth Callback Rewrite
2018-01-19 11:27:19 -07:00
Chi Vinh Le bf752a50ca Don't decrease count twice 2018-01-19 18:19:34 +01:00
Wyatt Johnson a402817cc9 Merge branch 'master' into user-json 2018-01-19 10:12:36 -07:00
Chi Vinh Le 669eb360df Merge branch 'live-reported' of ssh://github.com/coralproject/talk into live-reported 2018-01-19 17:04:49 +01:00
Chi Vinh Le 25dd979a37 Naming 2018-01-19 16:06:25 +01:00
Kiwi e653a5f9c0 Merge branch 'master' into live-reported 2018-01-19 16:04:43 +01:00
Chi Vinh Le 5d26f23b7b Prevent simulaneouse mutations (of the same one) 2018-01-19 15:51:51 +01:00
Chi Vinh Le e2a361276b show notification on errors 2018-01-19 15:33:34 +01:00
Chi Vinh Le 319bca3ba8 Refactor indicator 2018-01-19 14:55:53 +01:00
daan944 02ca59638a Merge branch 'master' into master 2018-01-19 11:29:47 +01:00
Wyatt Johnson b2cf5efed6 Merge pull request #1297 from coralproject/webpack-performance
Webpack Build Performance Improvements
2018-01-18 18:03:52 -07:00
Wyatt Johnson 7258c3dd08 added performance updates to webpack build 2018-01-18 17:19:19 -07:00
Wyatt Johnson 4bbd561a1b fix to cli 2018-01-18 16:54:27 -07:00
Wyatt Johnson 49d20db26f replaced homegrown with package 2018-01-18 14:22:19 -07:00
Wyatt Johnson 14d035adb1 Merge branch 'master' into user-json 2018-01-18 14:16:49 -07:00
Wyatt Johnson 844ce5dbfe fixed json parse error 2018-01-18 14:15:33 -07:00
Wyatt Johnson 8f1d9f051e added encoding for the user data as json with proper encoding/escaping 2018-01-18 13:58:45 -07:00
Chi Vinh Le a111439541 Only increme once 2018-01-18 20:31:51 +01:00
Chi Vinh Le 72331699b5 Refactor 2018-01-18 20:28:12 +01:00
Chi Vinh Le 544ef081dc Add previous username to notification 2018-01-18 20:09:47 +01:00
Chi Vinh Le ddf3816f5a Better notifications 2018-01-18 20:00:31 +01:00
Chi Vinh Le 7d022ed380 Refactor Community 2018-01-18 19:10:32 +01:00
Chi Vinh Le 40539bf3b7 Set limit to 10 when searching for people 2018-01-18 18:35:29 +01:00
Chi Vinh Le c1a26d2e71 Split community queries 2018-01-18 18:32:49 +01:00
Kim Gardner 6f55189658 Merge branch 'master' into master 2018-01-18 12:08:07 -05:00
Kim Gardner 3c9b4bd20e Merge pull request #1294 from coralproject/close-asset-now
closeAsset Mutation
2018-01-18 12:07:47 -05:00
Chi Vinh Le 2a6543bcea don't query username count twice 2018-01-18 17:43:46 +01:00
Wyatt Johnson 716a61aa5c Merge branch 'master' into master 2018-01-18 09:38:42 -07:00
Wyatt Johnson 127f0c1e20 Merge branch 'master' into close-asset-now 2018-01-18 09:38:24 -07:00
Wyatt Johnson fa83ba9556 Merge pull request #1292 from coralproject/locale-local-storage
Locale local storage
2018-01-18 09:38:12 -07:00
Kim Gardner 317541a63b Merge branch 'master' into close-asset-now 2018-01-18 11:27:12 -05:00
Kim Gardner fd5eb7d23d Merge branch 'master' into locale-local-storage 2018-01-18 11:27:04 -05:00
Kim Gardner 06f308c182 Merge pull request #1290 from coralproject/asset-rewrite
Asset Rewriting
2018-01-18 11:26:02 -05:00
Kim Gardner 2345daf783 Merge branch 'master' into locale-local-storage 2018-01-18 11:21:20 -05:00
Kim Gardner df2bce4d53 Merge branch 'master' into asset-rewrite 2018-01-18 11:19:23 -05:00
Kim Gardner c99411b8fd Merge pull request #1288 from coralproject/reply-on-hidden-comment
Replies to hidden/deleted comments
2018-01-18 11:18:53 -05:00
Kim Gardner 022a57479d Merge branch 'master' into reply-on-hidden-comment 2018-01-18 11:02:08 -05:00
Kim Gardner 56001d7780 Merge pull request #1284 from coralproject/action-index
Action index
2018-01-18 11:01:43 -05:00
Kim Gardner a010302242 Merge branch 'master' into reply-on-hidden-comment 2018-01-18 11:01:36 -05:00
Kim Gardner f306d54924 Merge branch 'master' into action-index 2018-01-18 10:53:45 -05:00
Kim Gardner c1ff7e9c84 Merge branch 'master' into asset-rewrite 2018-01-18 10:53:09 -05:00
Kim Gardner 3255b8d23b Merge branch 'master' into close-asset-now 2018-01-18 10:52:37 -05:00
Kim Gardner 8ea3ab9c02 Merge pull request #1283 from coralproject/formatting
Adding format to admin comment - CommentFormatter
2018-01-18 10:51:50 -05:00
Kim Gardner ec9a57bede Merge branch 'master' into formatting 2018-01-18 10:42:56 -05:00
Chi Vinh Le 5f3da9a09a Ignore own updates 2018-01-18 16:08:46 +01:00
daan944 dbfd0fd25e Merge branch 'master' into master 2018-01-18 14:13:57 +01:00
Wyatt Johnson ac62cbf348 added new closeAsset mutation and support in UI 2018-01-17 16:04:32 -07:00
Wyatt Johnson 08b1cada7e Merge pull request #1293 from coralproject/deploy-script
Fixed deploy script
2018-01-17 15:01:57 -07:00
Wyatt Johnson 4960b34748 fixed deploy script 2018-01-17 14:56:36 -07:00
Wyatt Johnson c612902779 fixed styles 2018-01-17 13:41:02 -07:00
Wyatt Johnson 3938b7883f introduced some locale fixes 2018-01-17 13:24:54 -07:00
Wyatt Johnson e048491221 Merge pull request #1291 from coralproject/webpack-sourcemaps
Webpack Sourcemap Configuration
2018-01-17 11:40:48 -07:00
Wyatt Johnson 68598d3c45 cleanup for logic 2018-01-17 11:34:09 -07:00
Wyatt Johnson 7ca89435c6 added configuration options for webpack 2018-01-17 11:24:36 -07:00
Chi Vinh Le cc320cf80a Remove leftover console 2018-01-17 18:03:59 +01:00
Chi Vinh Le 3c52d58a6e Cleanup Dangling 2018-01-17 18:01:42 +01:00
Chi Vinh Le f282aa47c9 Implement dangling 2018-01-17 17:31:48 +01:00
Kim Gardner 426d7dbfec Merge branch 'master' into formatting 2018-01-17 10:06:47 -05:00
Chi Vinh Le ee78f08598 Implement live update for reported users 2018-01-17 15:20:38 +01:00
Chi Vinh Le 519480c0e4 Fix counts not decreasing on mutations 2018-01-17 15:12:17 +01:00
Wyatt Johnson 45d6800143 added dry mode 2018-01-16 17:10:59 -07:00
Wyatt Johnson e3cdad1f2a added asset re-write 2018-01-16 16:47:05 -07:00
Wyatt Johnson 66cfd20f7b Merge branch 'master' into master 2018-01-16 16:00:37 -07:00
Wyatt Johnson af40a85071 Merge branch 'master' into master 2018-01-16 15:58:58 -07:00
Wyatt Johnson b11a730373 Merge branch 'master' into reply-on-hidden-comment 2018-01-16 14:13:13 -07:00
Wyatt Johnson b709cb485e refactored publicCreate 2018-01-16 14:11:24 -07:00
Kiwi 13fc91b355 Merge pull request #1287 from coralproject/ci-time-sync
CI Time Sync
2018-01-16 20:53:13 +01:00
Wyatt Johnson 9eec280868 added time syncing 2018-01-16 12:31:45 -07:00
Wyatt Johnson 45d4c79c26 added reply bug 2018-01-16 12:30:38 -07:00
Kiwi 1cf2bda36c Merge pull request #1286 from coralproject/e2e-pause
Add a little pause in e2e test
2018-01-16 19:44:21 +01:00
Chi Vinh Le b1adfe8c46 Add a little pause in e2e test 2018-01-16 19:33:07 +01:00
Chi Vinh Le 6aece16fd0 Add userFlagged subscription 2018-01-16 11:31:44 +01:00
okbel 2684ac4933 missing brackets 2018-01-15 13:14:24 -03:00
okbel 1f0e9da5d0 Merge branch 'formatting' of github.com:coralproject/talk into formatting
* 'formatting' of github.com:coralproject/talk:
  prettier
  End of line
  More syncs
  Fix deselect bug of action menu in ff
2018-01-15 13:00:07 -03:00
okbel bfa2f6edb9 Updates 2018-01-15 12:59:39 -03:00
Kiwi ea7998257c Merge branch 'master' into action-index 2018-01-15 16:02:53 +01:00
Wyatt Johnson 25c178b267 Merge branch 'master' into formatting 2018-01-12 17:23:35 -07:00
Wyatt Johnson 7885b025ee Merge pull request #1262 from coralproject/firefox-actionmenu-deselect
Fix deselect bug of action menu in ff
2018-01-12 16:49:04 -07:00
Wyatt Johnson 3606726c4b Merge branch 'master' into firefox-actionmenu-deselect 2018-01-12 16:33:55 -07:00
Wyatt Johnson d69b1f98c8 Merge branch 'master' into action-index 2018-01-12 15:50:02 -07:00
okbel 77d83e04d5 Wrong name 2018-01-12 15:53:32 -03:00
okbel 86c4d70daf Adding changes to the comments on the User Drawer Detail 2018-01-12 15:51:49 -03:00
okbel e256b0da8a Adding format to admin comment - CommentFormatter 2018-01-12 15:47:09 -03:00
Wyatt Johnson 93bda87ad0 Merge pull request #1153 from coralproject/next
Release 4.0.0
2018-01-12 09:43:40 -07:00
Chi Vinh Le 1ccd36e9e9 prettier 2018-01-12 14:07:05 +01:00
Tibo Beijen bf77ddc49b Merge branch 'master' into master 2018-01-12 13:54:31 +01:00
daan944 ebe6341b01 Merge pull request #1 from sanoma/feature/MDN-10616_translation_additions
Feature/mdn 10616 translation additions
2018-01-12 13:45:20 +01:00
d.luijten 34a0e314e8 After discussion: changed status to shorter “Pre-moderatie” 2018-01-12 13:44:29 +01:00
Chi Vinh Le 9762f34324 End of line 2018-01-12 11:45:52 +01:00
d.luijten 951f16f0a1 Improved email message: ‘je gebruiker’ is always referred to as ‘je account’. Also, simplified ‘Diens inhoud’ to ‘de inhoud van je gebruikersnaam’. 2018-01-12 11:32:48 +01:00
Chi Vinh Le 9ce14acc9a More syncs 2018-01-12 11:23:29 +01:00
d.luijten 988b39f19d Fixed call-to-action: “Kom in gesprek!” sounds a bit weird to me. 2018-01-12 11:20:29 +01:00
d.luijten 9333b407bb Fixed consistency: Community guidelines are used both with and without capital C. Chose to go for the version without capital C, since it’s not a title. 2018-01-12 11:19:21 +01:00
d.luijten 8867fd54f3 Fixed consistency: “Voor-mod” is never used. “Pre-moderatie” would be acceptable, but “Wachtend op moderatie” sounds more like the correct status message. 2018-01-12 11:14:51 +01:00
d.luijten 56bd3041d6 Fixed consistency: a “bio” has been both named “biografie” and “bio”. Chose “biografie” for now. 2018-01-12 11:11:09 +01:00
d.luijten a53bb6650a Fixed SI units. 2018-01-12 11:10:30 +01:00
d.luijten 0a99ae50b1 Fixed consistency: “Afgewezen wachtrij” implies that this name is used more frequently, but it isn’t. 2018-01-12 11:10:06 +01:00
Tibo Beijen 6bad3fdc2f Dotted some sentences. 2018-01-12 09:09:21 +01:00
Wyatt Johnson c19a6c8b10 Merge branch 'next' into firefox-actionmenu-deselect 2018-01-11 20:25:18 -07:00
Wyatt Johnson 0abc2ca243 replaced eslint:recommended with prettier 2018-01-11 20:00:34 -07:00
Wyatt Johnson d56c19016a Merge pull request #1279 from coralproject/docs
Updated docs for 4.0
2018-01-11 17:53:12 -07:00
Kim Gardner bb9a93986e Small wording tweak 2018-01-11 19:41:45 -05:00
Kim Gardner c906b2df5e Merge branch 'next' into docs 2018-01-11 19:36:58 -05:00
Wyatt Johnson 204b442e2f added deprecation warning 2018-01-11 17:32:52 -07:00
Wyatt Johnson 876f4ef700 updated docs for 4.0 2018-01-11 16:51:08 -07:00
Wyatt Johnson 7f02d4f39c fixed circle.yml 2018-01-11 15:54:46 -07:00
Kim Gardner c1f5450b9a Merge branch 'master' into next 2018-01-11 17:43:43 -05:00
Wyatt Johnson 7f291cf056 Merge pull request #1276 from coralproject/email-send-fix
Missing Email Error
2018-01-11 15:28:18 -07:00
Wyatt Johnson 7706a0521c Merge branch 'next' into email-send-fix 2018-01-11 15:28:11 -07:00
Wyatt Johnson 81d2927c0f Merge pull request #1278 from coralproject/plugin-context
Plugin context
2018-01-11 15:27:50 -07:00
Wyatt Johnson f2698298f7 Merge branch 'next' into plugin-context 2018-01-11 15:27:04 -07:00
Wyatt Johnson a4e8ea93bc fixed import typo 2018-01-11 15:24:46 -07:00
Kim Gardner ca49b373bf Merge branch 'next' into email-send-fix 2018-01-11 17:23:42 -05:00
Kim Gardner 4c6ec75cd7 Merge pull request #1277 from coralproject/hide-user-status-auth
Hide user status auth
2018-01-11 17:23:23 -05:00
Wyatt Johnson 07826a9bf4 fixing spelling :( 2018-01-11 15:20:53 -07:00
Belén Curcio f79f521d3d Merge branch 'next' into plugin-context 2018-01-11 19:15:59 -03:00
Belén Curcio 7e465f7317 Merge branch 'next' into email-send-fix 2018-01-11 19:11:33 -03:00
Belén Curcio 943c3e93a4 Merge branch 'next' into hide-user-status-auth 2018-01-11 19:11:16 -03:00
Kim Gardner f070d47c2d Merge pull request #1267 from coralproject/perm-fix
Permission fix
2018-01-11 16:40:10 -05:00
Wyatt Johnson 4eda547734 removed config from context 2018-01-11 14:22:45 -07:00
Wyatt Johnson 5865c30155 added a plugin context 2018-01-11 14:20:51 -07:00
Kim Gardner 838083e76d Merge branch 'next' into perm-fix 2018-01-11 16:10:41 -05:00
Wyatt Johnson 4d65052eef allow moderators to view role 2018-01-11 14:00:26 -07:00
Wyatt Johnson eec7d0fb63 Merge branch 'next' into hide-user-status-auth 2018-01-11 13:57:21 -07:00
Wyatt Johnson d837562089 hides the user status histories and tokens from the auth endpoints, which were resulting in errors 2018-01-11 13:56:23 -07:00
Kim Gardner fddba1e059 Merge branch 'next' into email-send-fix 2018-01-11 15:49:44 -05:00
Kim Gardner 1e85894a0c Merge pull request #1274 from coralproject/account-history
Suspension duration, fixes and wording for Account History
2018-01-11 15:49:26 -05:00
Kim Gardner 9b4dcd8d32 Merge branch 'next' into account-history 2018-01-11 15:34:50 -05:00
Kim Gardner 54ec9ea9a6 Merge pull request #1275 from coralproject/flags-enh
Change Flags to Reports and make font a bit bigger
2018-01-11 15:34:18 -05:00
Kim Gardner f51eda4f5e Merge branch 'next' into flags-enh 2018-01-11 15:24:24 -05:00
Kim Gardner be40594ecf Merge pull request #1273 from coralproject/selected-bug
Restore shortcut functionality
2018-01-11 15:24:09 -05:00
Kim Gardner f340cdd4b7 Merge branch 'next' into selected-bug 2018-01-11 15:10:09 -05:00
Kim Gardner 24b6a4f16f Merge pull request #1272 from coralproject/fix-scrollbars
Fix scrollbars issues
2018-01-11 15:05:34 -05:00
Kim Gardner f2e63b7621 Merge branch 'next' into fix-scrollbars 2018-01-11 14:41:32 -05:00
Kim Gardner 6d3d717ab3 Merge pull request #1271 from coralproject/fix-no-status-history
Fix no status history error
2018-01-11 14:41:17 -05:00
Wyatt Johnson 1dcf297a45 added email fixes 2018-01-11 10:58:43 -07:00
Kiwi 8cdc881f3d Merge branch 'next' into flags-enh 2018-01-11 18:47:05 +01:00
Belén Curcio 9187231737 Merge branch 'next' into account-history 2018-01-11 14:45:23 -03:00
okbel 2bc60b1211 fixes 2018-01-11 14:44:53 -03:00
Chi Vinh Le 90f1ef050b Change Flags to Reports and make a bit bigger 2018-01-11 18:41:53 +01:00
Kim Gardner eb5b736484 Merge branch 'next' into fix-no-status-history 2018-01-11 12:27:37 -05:00
Kim Gardner 92b506c85a Merge pull request #1270 from coralproject/suspect-label
Fix suspect label
2018-01-11 12:17:38 -05:00
Chi Vinh Le 2269c91244 Restore shortcut functionality 2018-01-11 16:54:54 +01:00
Chi Vinh Le 210b0ab907 Only show scrollbars when necessary 2018-01-11 16:05:20 +01:00
Chi Vinh Le 1337873adf Remove unwanted scroll in configure 2018-01-11 16:05:02 +01:00
Kim Gardner d922761f85 Merge branch 'next' into firefox-actionmenu-deselect 2018-01-11 09:51:40 -05:00
Kim Gardner 737b77a6ac Merge pull request #1269 from larrylizhao/feature/pluginTranslation
Translations refinement for plugins
2018-01-11 09:36:35 -05:00
Chi Vinh Le 11b38f7ea0 Fix no status history error 2018-01-11 13:55:09 +01:00
Bart Stroeken e5c3474455 dutch translation was not added yet without adding it to i18n.js - and refined a few translations. 2018-01-11 11:56:30 +01:00
Kiwi f89a5013d2 Merge branch 'next' into suspect-label 2018-01-11 11:20:04 +01:00
Kiwi 139d319965 Merge branch 'next' into perm-fix 2018-01-11 11:19:13 +01:00
Chi Vinh Le 89c929f970 Fix suspect label 2018-01-11 11:16:30 +01:00
liz 6e813e9663 Translations refinement for plugins 2018-01-11 17:16:26 +08:00
Bart Stroeken 1f2caada3f Finished drafting translations. Let's spin it up for a test run 2018-01-11 09:04:45 +01:00
Wyatt Johnson 14dd9acd3b changed copy for error 2018-01-10 16:49:40 -07:00
Wyatt Johnson d35d0e354e Merge branch 'next' into firefox-actionmenu-deselect 2018-01-10 15:35:27 -07:00
Wyatt Johnson ec38b837aa Merge branch 'next' into perm-fix 2018-01-10 14:48:08 -07:00
okbel d055e0b28b Feature Button 2018-01-10 17:51:21 -03:00
okbel 1f6d38c4ad Updating copy 2018-01-10 17:43:55 -03:00
okbel eef2578e61 ignore user box over flow, fixed 2018-01-10 17:41:18 -03:00
okbel 7127aeaa87 Icon User Drawer 2018-01-10 17:35:50 -03:00
Wyatt Johnson 44648fa901 fixed perms issue 2018-01-10 13:04:56 -07:00
Wyatt Johnson acc2e00c54 Merge pull request #1264 from coralproject/reply
Hide reply button when comment stream is closed
2018-01-10 11:55:05 -07:00
Belén Curcio 7d6fd69a3b Merge branch 'next' into reply 2018-01-10 15:53:39 -03:00
Wyatt Johnson 75d1bb2f22 Merge branch 'next' into firefox-actionmenu-deselect 2018-01-10 10:40:52 -07:00
Wyatt Johnson 1e372b583a Merge pull request #1265 from coralproject/lodash-size-opt
Use lodash with es modules
2018-01-10 10:40:35 -07:00
Kiwi 1b373b5af4 Merge branch 'next' into lodash-size-opt 2018-01-10 18:39:57 +01:00
Kiwi d0d9a38177 Merge branch 'next' into firefox-actionmenu-deselect 2018-01-10 18:39:54 +01:00
Kiwi 9d8867be9d Merge branch 'next' into reply 2018-01-10 18:39:45 +01:00
Wyatt Johnson fb7ff249b8 Merge pull request #1266 from coralproject/circle-next
Circle + Docker next
2018-01-10 10:38:56 -07:00
Wyatt Johnson bbdd69b2a0 removed debug... 2018-01-10 10:34:11 -07:00
Wyatt Johnson 3687fc1702 adjusted build script to build the next branch and release it 2018-01-10 10:31:43 -07:00
Chi Vinh Le caf2b88ee8 Fix typo 2018-01-10 18:29:03 +01:00
okbel 2ef3e29e7d Merge branch 'reply' of github.com:coralproject/talk into reply
* 'reply' of github.com:coralproject/talk: (193 commits)
  Add some margin to loadmore
  typo
  updated tests
  updated tests
  Fully implement SetUsername
  removed snakecase
  fix to optional required param
  Use correct mutation and show errors
  refactored mailer service with lodash!
  Fix live status updates
  Move ChangeUsername to the appropiate tab
  Fix change username not displaying errors
  Remove defunct refetch detection workaround
  Rename UnBan and UnSuspend
  hardened configuration
  user cli patches
  don't apply configuration from dotfiles during testing
  applied rename to graph mutation edges
  reintroduced the errors.ErrSameUsernameProvided
  fixed wrong query logic
  ...
2018-01-10 14:08:58 -03:00
Kiwi 6a3be92a4c Merge branch 'next' into reply 2018-01-10 18:08:12 +01:00
okbel 845c99de10 Updated tests 2018-01-10 13:57:16 -03:00
okbel 9c7b6e007a Merge branch 'reply' of github.com:coralproject/talk into reply
* 'reply' of github.com:coralproject/talk:
  Small grammar fixes
  refactor to cleanup
  extracted asset url
  extracted more funcs
  extracted events from opts
  Adding TALK_PERSPECTIVE_DO_NOT_STORE to the docs
  Adding doNotStore
  Translation update to make it more appropriate.
  Translation for hide off topic comments
2018-01-10 13:55:07 -03:00
okbel 4c3d3fb4b0 Updated tests 2018-01-10 13:54:43 -03:00
Chi Vinh Le 7036aa5bad Use lodash with es modules 2018-01-10 17:54:06 +01:00
Belén Curcio 1898941097 Merge branch 'master' into reply 2018-01-10 12:31:10 -03:00
okbel 24f7405065 remove uncommitted 2018-01-10 12:29:47 -03:00
okbel e1ead8f500 Adding e2e for the stream - open and closed asset features 2018-01-10 11:43:34 -03:00
okbel 5db8209182 Removing replyButton in Stream is closed 2018-01-10 11:21:24 -03:00
okbel 9c75f3fa59 Adding missing proptypes 2018-01-10 11:03:33 -03:00
Belén Curcio 25ac66ec59 Merge branch 'next' into firefox-actionmenu-deselect 2018-01-10 10:46:48 -03:00
Chi Vinh Le bdcc9273f1 Add some margin to loadmore 2018-01-10 12:22:23 +01:00
Kiwi dca85ad127 Merge branch 'next' into firefox-actionmenu-deselect 2018-01-10 12:19:58 +01:00
Chi Vinh Le 36f7c31a03 Fix deselect bug of action menu in ff 2018-01-10 12:16:28 +01:00
Kim Gardner 63bd6309d3 Merge branch 'master' into next 2018-01-09 17:22:47 -05:00
Kim Gardner bf76f0453b Merge pull request #1261 from coralproject/losowsky-patch-1
Small grammar fixes
2018-01-09 17:04:01 -05:00
Kim Gardner 00677148b9 Merge branch 'master' into losowsky-patch-1 2018-01-09 16:53:15 -05:00
Wyatt Johnson 7bdfb55a1c Merge pull request #1260 from coralproject/embed-events
Embed events
2018-01-09 14:53:04 -07:00
Andrew Losowsky c43051c6dd Small grammar fixes 2018-01-09 16:20:32 -05:00
Wyatt Johnson 20453dfddd refactor to cleanup 2018-01-09 14:13:02 -07:00
Wyatt Johnson 20f9bba845 Merge branch 'master' into embed-events 2018-01-09 11:25:22 -07:00
Wyatt Johnson 25e29cb79a extracted asset url 2018-01-09 11:01:10 -07:00
Wyatt Johnson 7f25f46ac0 extracted more funcs 2018-01-09 10:51:43 -07:00
Wyatt Johnson d3c1286d1a extracted events from opts 2018-01-09 10:47:58 -07:00
Kim Gardner 13bf20daf0 Merge pull request #1259 from coralproject/fix-set-username
Make Facebook login work again in next
2018-01-09 12:39:20 -05:00
Kim Gardner e61c09f017 Merge branch 'next' into fix-set-username 2018-01-09 11:52:29 -05:00
Kim Gardner 395793445c Merge branch 'master' into next 2018-01-09 11:52:20 -05:00
Kim Gardner abc6cf6366 Merge pull request #1258 from coralproject/toxic-update
Adding doNotStore param to our Toxic Comments Plugin
2018-01-09 11:51:59 -05:00
Kim Gardner eb33eb72d1 Merge branch 'master' into toxic-update 2018-01-09 11:44:44 -05:00
Kim Gardner 1a39635c0e Merge pull request #1257 from larrylizhao/feature/RefineTranslation
Feature/refine translation
2018-01-09 11:43:23 -05:00
Kiwi 13347a6baf Merge branch 'next' into fix-set-username 2018-01-09 15:07:56 +01:00
okbel ff016e22fe Adding TALK_PERSPECTIVE_DO_NOT_STORE to the docs 2018-01-09 10:42:28 -03:00
okbel 579644c910 Adding doNotStore 2018-01-09 10:34:24 -03:00
okbel 54b504eb8e typo 2018-01-09 10:24:58 -03:00
okbel 4490ea5202 Merge branch 'next' of github.com:coralproject/talk into next
* 'next' of github.com:coralproject/talk: (26 commits)
  removed snakecase
  fix to optional required param
  refactored mailer service with lodash!
  Docs typo
  Fix live status updates
  Move ChangeUsername to the appropiate tab
  Fix change username not displaying errors
  Remove defunct refetch detection workaround
  Rename UnBan and UnSuspend
  hardened configuration
  user cli patches
  don't apply configuration from dotfiles during testing
  applied rename to graph mutation edges
  reintroduced the errors.ErrSameUsernameProvided
  fixed wrong query logic
  resolving pr concerns
  Add proptype
  Mark onClickOutside not required
  Remove unused state
  Use null instead of boolean
  ...
2018-01-09 10:13:07 -03:00
okbel fb26d67e42 updated tests 2018-01-09 10:12:26 -03:00
okbel 07052824d7 updated tests 2018-01-09 10:10:37 -03:00
Chi Vinh Le a2eb474a5c Fully implement SetUsername 2018-01-09 12:25:44 +01:00
Chi Vinh Le 2bcb324582 Merge branch 'next' into fix-set-username 2018-01-09 11:41:56 +01:00
liz 37d1f3361c Translation update to make it more appropriate. 2018-01-09 16:54:13 +08:00
liz 6e8b28cc8a Translation for hide off topic comments 2018-01-09 14:03:09 +08:00
Wyatt Johnson 9513502667 Merge pull request #1255 from coralproject/fix-live-status-changes
Fix live status updates
2018-01-08 11:52:27 -07:00
Wyatt Johnson 55c2c00b49 Merge branch 'next' into fix-live-status-changes 2018-01-08 11:36:06 -07:00
Wyatt Johnson 8545be307d removed snakecase 2018-01-08 11:17:11 -07:00
Wyatt Johnson 04991a9856 Merge branch 'master' into next 2018-01-08 11:14:17 -07:00
Wyatt Johnson 978e1c8d4c fix to optional required param 2018-01-08 11:04:29 -07:00
Wyatt Johnson 6700698e52 Merge branch 'next' into fix-live-status-changes 2018-01-08 10:56:54 -07:00
Wyatt Johnson c0f266dfa5 Merge pull request #1256 from coralproject/docs-typo
Docs typo
2018-01-08 10:56:41 -07:00
Chi Vinh Le a40b287226 Use correct mutation and show errors 2018-01-08 18:52:54 +01:00
Wyatt Johnson 615be40b45 refactored mailer service with lodash! 2018-01-08 10:43:10 -07:00
Kim Gardner 99d66fef52 Docs typo 2018-01-08 11:27:25 -05:00
Chi Vinh Le c3a5da3ae6 Fix live status updates 2018-01-08 16:00:20 +01:00
Chi Vinh Le ec852601a4 Move ChangeUsername to the appropiate tab 2018-01-08 15:50:41 +01:00
Chi Vinh Le 791025b153 Fix change username not displaying errors 2018-01-08 15:48:49 +01:00
Chi Vinh Le be72e2effd Remove defunct refetch detection workaround 2018-01-08 15:01:09 +01:00
Chi Vinh Le 717d5e6566 Rename UnBan and UnSuspend 2018-01-08 14:43:14 +01:00
Wyatt Johnson c07ec23a20 hardened configuration
- moved env rewrite + dotenv code to single location
- fixed bug with system flag on user
2018-01-05 16:06:36 -07:00
Wyatt Johnson 6260339508 user cli patches
- removed unused user actions
- added system side graph context
- added user cli search
2018-01-05 15:32:36 -07:00
Wyatt Johnson 10754a65c6 don't apply configuration from dotfiles during testing 2018-01-05 12:12:54 -07:00
Wyatt Johnson c3970a46a2 applied rename to graph mutation edges 2018-01-05 12:11:32 -07:00
Wyatt Johnson c9bc9a4b10 reintroduced the errors.ErrSameUsernameProvided 2018-01-05 11:31:40 -07:00
Wyatt Johnson 7b5993bc60 fixed wrong query logic 2018-01-05 11:17:04 -07:00
Wyatt Johnson b42cce5747 resolving pr concerns 2018-01-05 11:12:35 -07:00
Wyatt Johnson 9c595b7f84 Merge branch 'master' into next 2018-01-05 11:10:32 -07:00
Chi Vinh Le acdaaffd4e Add proptype 2018-01-05 19:03:40 +01:00
Chi Vinh Le c01b192a73 Mark onClickOutside not required 2018-01-05 19:02:35 +01:00
Chi Vinh Le 7538e6e8fd Remove unused state 2018-01-05 18:57:12 +01:00
Chi Vinh Le 49c350376b Use null instead of boolean 2018-01-05 18:56:44 +01:00
Chi Vinh Le 4a259d09e4 Fix styling for new x button 2018-01-05 18:56:13 +01:00
okbel 918aa6988b Adding a UI reducer to manage UI state 2018-01-05 14:50:35 -03:00
okbel 7e4831e309 styles.css is de-pre-ca-ted 2018-01-05 14:30:28 -03:00
okbel 14034c1364 Using button in the Drawer 2018-01-05 14:24:21 -03:00
okbel 5a379b2cd8 Adding line back 2018-01-05 14:24:06 -03:00
Kim Gardner ad6a663179 Merge pull request #1237 from coralproject/admin-pages
CSS Customization and i18n fixes
2018-01-05 12:20:46 -05:00
okbel b185d61993 Adding clarifications 2018-01-05 07:49:37 -03:00
okbel 9f780a64cc Load more: Rejected Usernames -> Wrong name 2018-01-05 07:46:21 -03:00
okbel 88b44c3bf6 Moment settings based on users locale 2018-01-05 07:34:17 -03:00
okbel e83f99e389 Removing unused code 2018-01-05 07:28:51 -03:00
Kim Gardner ce4ab4e86c Merge branch 'master' into next 2018-01-04 17:28:13 -05:00
Kim Gardner 5cd6322284 Merge branch 'master' into admin-pages 2018-01-04 17:26:33 -05:00
Kim Gardner 0ab6cc3a0f Merge pull request #1230 from coralproject/safari-bs-changes
Safari Browserstack changes
2018-01-04 17:26:05 -05:00
Kim Gardner c617a3446d Merge branch 'master' into safari-bs-changes 2018-01-04 17:15:47 -05:00
Kim Gardner d76fe5350e Merge pull request #1238 from MEL27/master
Add initial Simplified Chinese translation
2018-01-04 17:15:23 -05:00
Kim Gardner 927aa54feb Merge branch 'master' into master 2018-01-04 17:03:10 -05:00
Kim Gardner fc272ed1e1 Merge branch 'master' into master 2018-01-04 17:02:32 -05:00
Kim Gardner 882251864a Merge pull request #1246 from coralproject/simplified-chinese-merge-fixes
Simplified Chinese merge fixes
2018-01-04 17:01:42 -05:00
Kim Gardner 4eaa234dd8 Merge branch 'master' into simplified-chinese-merge-fixes 2018-01-04 16:48:46 -05:00
Kim Gardner bf1fbfe03f Fix merge conflicts on master 2018-01-04 16:43:51 -05:00
Wyatt Johnson 66f068359f Added index for item_id on Actions. 2018-01-04 14:14:27 -07:00
Kim Gardner d3f93644db Merge branch 'master' into master 2018-01-04 16:07:18 -05:00
Kim Gardner b9412dcb10 Merge branch 'master' into master 2018-01-04 15:55:18 -05:00
Kim Gardner 1125351cd1 Merge pull request #1243 from larrylizhao/feature/zh_tw
Adding traditional Chinese locales for Coral talk
2018-01-04 15:35:22 -05:00
Wyatt Johnson c683dd8d32 fixed let -> const 2018-01-04 10:56:15 -07:00
Wyatt Johnson 843908dab2 changed beheviour of dotenv parsing 2018-01-04 10:52:15 -07:00
Wyatt Johnson ad35a04cdf pr review updates 2018-01-04 10:50:14 -07:00
Kiwi 2c87dac897 Merge branch 'master' into admin-pages 2018-01-04 18:42:21 +01:00
Chi Vinh Le d5bf44ada8 Keep localhost for mongodb and redis 2018-01-04 18:41:31 +01:00
okbel 7bfbeac368 Removing unused code 2018-01-04 13:59:37 -03:00
okbel ca55097b5c Removing unused code 2018-01-04 13:58:07 -03:00
okbel ee0c510569 Adding warning 2018-01-04 13:55:00 -03:00
okbel 3d9eec825c Merge branch 'next' of github.com:coralproject/talk into next
* 'next' of github.com:coralproject/talk:
  Up patch version to match tag.
2018-01-04 13:37:36 -03:00
okbel 6d777944aa Naming Conventions 2018-01-04 13:37:21 -03:00
okbel 841398c6c1 Removing pointer hover, adding proptypes 2018-01-04 13:23:41 -03:00
Kim Gardner f8c1f1e63d Merge branch 'master' into safari-bs-changes 2018-01-04 11:21:52 -05:00
Kim Gardner c7176a4660 Merge branch 'master' into feature/zh_tw 2018-01-04 11:21:07 -05:00
Kim Gardner e2534e7ca8 Merge branch 'master' into master 2018-01-04 11:20:57 -05:00
Kim Gardner d09948e7f6 Merge branch 'master' into next 2018-01-04 11:20:18 -05:00
Wyatt Johnson 270f865ccd Merge pull request #1245 from jheasly/version_fix
Up patch version to match tag.
2018-01-04 09:18:28 -07:00
okbel e52793e918 Moving bulk actions to User tab 2018-01-04 12:50:53 -03:00
okbel a8e09d4c89 Account History stable key 2018-01-04 12:25:29 -03:00
okbel 7bdaf74e1a SetUserRole using DataProxy 2018-01-04 12:08:26 -03:00
okbel f04c3cc059 Naming Conventions 2018-01-04 11:53:08 -03:00
okbel 8c5040d88b Naming conventions 2018-01-04 11:49:10 -03:00
okbel 3ca3b59802 Using user.id 2018-01-04 11:44:02 -03:00
okbel fa6aa4ae2d Search functionality on the Community People Section 2018-01-04 11:42:51 -03:00
okbel f6e5df2088 Merge branch 'next' of github.com:coralproject/talk into next
* 'next' of github.com:coralproject/talk:
  added user query by value to graph
2018-01-04 11:20:58 -03:00
okbel e343958b07 Merge branch 'master' of github.com:coralproject/talk into next
* 'master' of github.com:coralproject/talk:
  fixes default lang select bug
2018-01-04 11:02:50 -03:00
Belén Curcio 298cb5322e Merge pull request #1244 from coralproject/user-search
User Search
2018-01-04 11:02:28 -03:00
J. Heasly d7097ff729 Up patch version to match tag. 2018-01-03 17:51:58 -08:00
Wyatt Johnson 94f2b4397e Merge branch 'master' into feature/zh_tw 2018-01-03 14:57:50 -07:00
Wyatt Johnson 75e89591a8 Merge branch 'master' into safari-bs-changes 2018-01-03 14:54:58 -07:00
Wyatt Johnson 72b2211daa reduced complexity 2018-01-03 14:43:35 -07:00
Wyatt Johnson 0850b1f982 added some comments 2018-01-03 14:34:50 -07:00
Wyatt Johnson 375fbd3790 resolving code climate issues 2018-01-03 14:32:19 -07:00
Wyatt Johnson 82c84de33f code review 2018-01-03 14:22:52 -07:00
Wyatt Johnson 57972b3fb9 Merge branch 'master' into admin-pages 2018-01-03 14:08:55 -07:00
Wyatt Johnson 9dcb8f8f0e Merge branch 'next' into user-search 2018-01-03 14:08:38 -07:00
Wyatt Johnson 148d329ff8 Merge branch 'master' into next 2018-01-03 14:08:29 -07:00
Wyatt Johnson 517124cc93 Merge pull request #1236 from coralproject/i18n-bug-fix
i18n Default Language Fix
2018-01-03 14:08:11 -07:00
Wyatt Johnson 797cea4f63 added user query by value to graph 2018-01-03 14:01:51 -07:00
okbel cb2f032ae1 fix index for moderation actions, and view options 2018-01-03 17:38:22 -03:00
Belén Curcio 0572a1c97d Merge branch 'master' into next 2018-01-03 12:11:44 -03:00
liz bb8a75937a Fix for typo in plugin 2018-01-03 18:10:25 +08:00
Kiwi 8060d9c22e Merge branch 'master' into safari-bs-changes 2018-01-03 10:23:01 +01:00
liz 783e485aa5 Add traditional Chinese locales for plugins 2018-01-03 17:21:30 +08:00
liz 6f96bd416c Traditional Chinese locales 2018-01-03 17:19:11 +08:00
Wyatt Johnson 20e3d9065f Merge branch 'master' into i18n-bug-fix 2017-12-30 02:56:13 -07:00
Wyatt Johnson 7b6c59b6f1 Merge branch 'master' into admin-pages 2017-12-30 02:56:02 -07:00
Wyatt Johnson 0af8a31154 Merge pull request #1240 from gakhov/gakhov-patch-advanced-config
Fix a variable name for redis client configuration
2017-12-30 02:55:46 -07:00
Andrii Gakhov 140bba23e0 Fix a variable name for redis client configuration
The documentation incorrectly stated that the advanced Redis Client configuration could be set using `TALK_REDIS_CLIENT_CONFIG`, while, in fact, the correct variable is `TALK_REDIS_CLIENT_CONFIGURATION`, as it parsed in https://github.com/coralproject/talk/blob/master/config.js#L114
2017-12-28 13:04:37 +01:00
MEL27 ae0407ed5e Refine zh_CN translation 2017-12-26 12:08:47 +08:00
MEL27 a5d7bca8c0 Refine zh_CN translation 2017-12-26 00:20:58 +08:00
MEL27 8a40093399 Refine zh_CN translation 2017-12-25 22:22:46 +08:00
MEL27 b3d76bba05 Miscellaneous zh_CN translation changes 2017-12-25 17:45:03 +08:00
Kim Gardner e5a00d0793 Merge branch 'master' into master 2017-12-24 16:01:39 -05:00
MEL27 5f0f97edbb Refine locales/zh_CN.yml 2017-12-24 16:55:10 +08:00
MEL27 19c46d3b32 Refine Simplified Chinese translation 2017-12-24 12:51:19 +08:00
MEL27 5b356e6249 Add initial Simplified Chinese translation 2017-12-24 12:22:44 +08:00
Wyatt Johnson 20878ba040 css and standardization on all admin pages 2017-12-21 23:27:37 -07:00
Wyatt Johnson 69e4c5478d fixes default lang select bug 2017-12-21 23:26:34 -07:00
Kim Gardner 0d70c87ea9 Fix merge conflict 2017-12-21 17:01:48 -05:00
Wyatt Johnson d3ce68e078 Merge branch 'master' into next 2017-12-21 14:51:56 -07:00
Wyatt Johnson 4fa46c4144 Merge pull request #1154 from coralproject/user-status-refactor
User status refactor
2017-12-21 14:35:23 -07:00
Wyatt Johnson c2a8a3e4a3 fixed tests 2017-12-21 14:26:44 -07:00
Kim Gardner 01707a5150 Merge branch 'master' into safari-bs-changes 2017-12-21 15:26:31 -05:00
Kim Gardner 5488d5a7fa Merge pull request #1234 from coralproject/modqueue-enh
Modqueue fixes and enhancements
2017-12-21 15:26:09 -05:00
Wyatt Johnson c8ab5cdbf6 fixed tests 2017-12-21 12:43:26 -07:00
Wyatt Johnson 4fb9910221 updates to cli
- removed -c,--config flag in favor of env only or .env files
- loads .env always if found
- scripts re-written to call cli-serve directly
2017-12-21 12:03:34 -07:00
Wyatt Johnson 7ae0df2cbc fixed dist loading error 2017-12-21 11:55:09 -07:00
Wyatt Johnson 8bf2fe7b06 hid not logged in error 2017-12-21 11:51:16 -07:00
Wyatt Johnson c0f8fce442 fixed graphql error 2017-12-21 11:48:16 -07:00
Wyatt Johnson 47e32d5d6d Merge branch 'next' into user-status-refactor 2017-12-21 11:34:39 -07:00
Chi Vinh Le 111af0acf8 More refactor 2017-12-21 18:43:39 +01:00
Chi Vinh Le e34f96832a Refactor 2017-12-21 18:36:15 +01:00
Chi Vinh Le f9e289561e Refactor 2017-12-21 18:29:18 +01:00
Wyatt Johnson e409d071cb Merge branch 'master' into next 2017-12-21 09:37:39 -07:00
Chi Vinh Le 2d6a05433a Mark single dangling comments, reflow issues and scroll enhancement 2017-12-21 15:55:18 +01:00
Chi Vinh Le 3c97ce3f66 Merge branch 'safari-bs-changes' of ssh://github.com/coralproject/talk into safari-bs-changes 2017-12-21 14:12:43 +01:00
Chi Vinh Le f8c1733e14 Remove sendKey hack 2017-12-21 14:11:23 +01:00
Chi Vinh Le 2e726c9d6a Set window position 2017-12-21 14:10:28 +01:00
Kim Gardner 7f75255282 Merge branch 'master' of git+ssh://github.com/coralproject/talk into user-status-refactor 2017-12-21 07:04:27 -05:00
Kiwi 918b863820 Merge branch 'master' into safari-bs-changes 2017-12-21 12:47:22 +01:00
Chi Vinh Le 3c5991453d Merge branch 'safari-bs-changes' of ssh://github.com/coralproject/talk into safari-bs-changes 2017-12-21 12:32:33 +01:00
Chi Vinh Le 3776c4d050 Use private ip 2017-12-21 12:31:36 +01:00
okbel 5925a4b956 updated tests classes 2017-12-20 21:21:16 -03:00
okbel dafd703261 Merge branch 'user-status-refactor' of github.com:coralproject/talk into user-status-refactor
* 'user-status-refactor' of github.com:coralproject/talk:
  added mount path to the endpoint to respect the mounting config
  linting
  removed dev requirement for engine
2017-12-20 21:14:21 -03:00
okbel 5c0f546d50 Adding tests 2017-12-20 21:06:48 -03:00
okbel e8e9a4b5ed bounding fns 2017-12-20 21:06:37 -03:00
okbel 1371776f74 Updating the store mutations 2017-12-20 21:05:52 -03:00
Wyatt Johnson deb2709829 added mount path to the endpoint to respect the mounting config 2017-12-20 16:29:26 -07:00
okbel 4cc35e5b8f rejected usernames should not be able to comment 2017-12-20 20:13:36 -03:00
Wyatt Johnson ad68346332 linting 2017-12-20 16:11:39 -07:00
Wyatt Johnson 97406e1b18 removed dev requirement for engine 2017-12-20 16:10:02 -07:00
okbel 483b306e69 Updated tests 2017-12-20 20:09:51 -03:00
okbel 0bce215d33 Adding tests 2017-12-20 20:00:47 -03:00
okbel 73666399b4 Updating the store mutations 2017-12-20 20:00:34 -03:00
okbel 60e890c244 Adding Classes 2017-12-20 20:00:24 -03:00
okbel 16b5bd5d09 Updating the store mutations 2017-12-20 20:00:05 -03:00
okbel 37eafd2fed Adding Classes 2017-12-20 19:59:14 -03:00
okbel 5d51f71f0c Test 2017-12-20 18:48:43 -03:00
okbel a989a0b20e Adding Classes to the PageObject 2017-12-20 18:48:30 -03:00
okbel 4f3a400358 Classes for AccountHistory 2017-12-20 18:48:10 -03:00
okbel f67f1e3b52 Adding role, instead of roles. And disable banning and suspending for the user thats logged in 2017-12-20 18:10:13 -03:00
okbel 570c0edfb3 merge conficts 2017-12-20 16:06:55 -03:00
Kiwi c1b5216647 Merge branch 'master' into safari-bs-changes 2017-12-20 18:08:52 +01:00
Chi Vinh Le 7d71452ab5 Add ip package 2017-12-20 17:45:03 +01:00
Chi Vinh Le 29a60c1241 Use local IP instead of localhost 2017-12-20 17:41:55 +01:00
Kim Gardner b8ffe25212 Merge branch 'master' into next 2017-12-20 09:48:18 -05:00
Kim Gardner 2bde40777d Merge branch 'master' into next 2017-12-19 11:44:41 -05:00
Wyatt Johnson 31655847a9 Merge branch 'next' into user-status-refactor 2017-12-16 14:58:59 -06:00
Wyatt Johnson 6655d06d80 Merge branch 'master' into next 2017-12-16 14:54:25 -06:00
Wyatt Johnson fadf3b5106 Fixed import error 2017-12-11 15:32:00 -06:00
Belen Curcio 588c1c201c Avoiding refetch from Community 2017-12-11 11:37:35 -03:00
Belen Curcio f005d1e5f8 Styling, refactor and instant update after banning and unbanning from community 2017-12-11 11:33:26 -03:00
Belen Curcio 1d12c3629a Adding utils, and replacing REST old endpoints 2017-12-11 10:21:56 -03:00
Belen Curcio a1a0671382 unBan Update 2017-12-11 09:36:42 -03:00
Belen Curcio e66e23bbf6 Updated with User-Status-Refactor API - User Detail Updated 2017-12-11 09:10:50 -03:00
Belen Curcio 26abd02032 Updated with User-Status-Refactor API 2017-12-11 08:49:22 -03:00
Belen Curcio 8e582bff45 Adding unSuspendUser mutation 2017-12-11 08:37:09 -03:00
Belen Curcio 8df4b79f97 Updated with User-Status-Refactor API 2017-12-11 08:33:40 -03:00
Wyatt Johnson 74c90d43a7 linting patch 2017-12-07 11:07:14 -07:00
Wyatt Johnson 454fa752e2 fixed template errors 2017-12-07 11:05:42 -07:00
Belen Curcio 83d4408ddf merge 2017-12-07 13:55:40 -03:00
Belen Curcio 1322db89bc Merge branch 'actions-user-drawer' of github.com:coralproject/talk into user-status-refactor 2017-12-07 12:36:53 -03:00
Wyatt Johnson 6f4f69ac3e Merge branch 'next' into user-status-refactor 2017-12-06 09:03:38 -07:00
Wyatt Johnson 14b5ccb53e fixed small bug 2017-12-05 15:14:57 -07:00
Wyatt Johnson 2e4229ce18 removed reference 2017-12-05 15:14:57 -07:00
Wyatt Johnson 6bc9e6a0bb added support for deep route caching, settings cache, template optim 2017-12-05 15:14:57 -07:00
Kim Gardner a0ee5c6414 Update CSS classname 2017-12-05 14:38:43 -07:00
Wyatt Johnson 48ef9b162d Merge branch 'master' into next 2017-12-05 14:02:09 -07:00
Wyatt Johnson 2d70caf03d Merge branch 'next' into user-status-refactor 2017-12-04 15:42:57 -07:00
Wyatt Johnson 419784f6d1 Merge branch 'master' into next 2017-12-04 15:28:41 -07:00
Wyatt Johnson 674ab19d06 Adjusted mongo indexes to support index scanning 2017-12-04 09:05:40 -07:00
Belen Curcio c53fe4b197 Updated tests and Drawer component 2017-12-04 00:37:41 -03:00
Belen Curcio 67c81c0168 Adding react-mdl 2017-12-03 23:16:54 -03:00
Belen Curcio e44b37538e wip 2017-12-03 16:39:48 -03:00
Belen Curcio 56beac9a1f Adding old user status 2017-12-03 16:31:08 -03:00
Belen Curcio 0596547223 Adding old user status 2017-12-03 16:29:12 -03:00
Belen Curcio ca6e839ca4 UserInfoTooltip, styling and func 2017-12-03 16:19:59 -03:00
Belen Curcio 6557c6036f Functionality and styling 2017-12-03 15:48:37 -03:00
Belen Curcio a3d83efd7d Styling and functionality 2017-12-03 15:01:06 -03:00
Belen Curcio 0483aae369 Moving ActionsMenu 2017-12-03 14:54:28 -03:00
Wyatt Johnson cbf3e4734d improved query performance with indexes, added engine for debugging 2017-11-30 16:40:58 -07:00
Wyatt Johnson 0c484954df Merge branch 'next' into user-status-refactor 2017-11-30 11:29:44 -07:00
Wyatt Johnson b672b0a4e0 added better compression support 2017-11-30 11:20:13 -07:00
Belen Curcio c84408004c Updated comment user status path 2017-11-30 11:36:32 -03:00
Belen Curcio 4b0d64dfc6 New path 2017-11-30 11:18:34 -03:00
Belen Curcio a78bff5ee6 Merge branch 'master' of github.com:coralproject/talk into user-status-refactor 2017-11-30 11:12:53 -03:00
Kim Gardner a8c0ff0c0a Merge branch 'master' into next 2017-11-30 11:41:36 +00:00
Wyatt Johnson fc824b1fbe Merge branch 'master' into next 2017-11-28 12:58:58 -07:00
Belen Curcio 321f3117a4 Linting 2017-11-28 15:08:04 -03:00
Belen Curcio 09839ab024 Updating after approving a user 2017-11-28 15:00:01 -03:00
Belen Curcio da2b9aca03 Removing ternary 2017-11-28 14:56:29 -03:00
Belen Curcio 20d0cbaf27 Wrong filename 2017-11-28 14:52:31 -03:00
Belen Curcio c177403628 Merge branch 'user-status-refactor' of github.com:coralproject/talk into user-status-refactor 2017-11-28 14:37:50 -03:00
Belen Curcio b7704fb2ca Translations and Styling 2017-11-28 14:37:23 -03:00
Wyatt Johnson 2437401daf Merge branch 'next' into user-status-refactor 2017-11-28 09:47:34 -07:00
Wyatt Johnson 7f189cd4d4 Merge branch 'master' into next 2017-11-28 09:47:21 -07:00
Belen Curcio 16e3924d3f Ban user dialog steps 2017-11-28 13:39:49 -03:00
Belen Curcio d510905ba2 Adding ban message and setRole 2017-11-28 00:18:09 -03:00
Wyatt Johnson d4889eb52c more role fixes 2017-11-27 16:44:40 -07:00
Wyatt Johnson f2fe095b43 fixed bug with featuring comments doesn't work 2017-11-27 16:27:43 -07:00
Wyatt Johnson 13865ff29e roles -> role, null -> COMMENTER for user roles 2017-11-27 11:53:40 -07:00
Belen Curcio 9386912b9b Approve Username fix 2017-11-27 14:43:04 -03:00
Belen Curcio 95d356de10 Adding UserDetailCommentList 2017-11-27 12:20:22 -03:00
Belen Curcio f2dccf7845 Adding addUserRole and removeUserRole 2017-11-24 18:36:59 -03:00
Belen Curcio e442961dbb UserDetail Refactor 2017-11-24 18:09:22 -03:00
Belen Curcio d58e54befa UserDetail Refactor 2017-11-24 17:35:53 -03:00
Belen Curcio 0fe9577abf Build User History 2017-11-24 16:24:11 -03:00
Belen Curcio 9703b86784 Working account history 2017-11-24 13:42:45 -03:00
Belen Curcio e2af3470dc Adding diff and fix to utils 2017-11-24 13:09:30 -03:00
Belen Curcio 741419f47b missing fragment 2017-11-24 12:58:05 -03:00
Wyatt Johnson 4fc305ebff fixes based on feedback 2017-11-23 13:49:36 -07:00
Wyatt Johnson db8dd5a487 removed double await 2017-11-23 12:27:35 -07:00
Wyatt Johnson d8d15f27ab added role management to graph 2017-11-23 11:20:55 -07:00
Wyatt Johnson 6119503057 added support for user assigned_by fields to get resolved 2017-11-23 10:53:20 -07:00
Belen Curcio 6e6ddbf378 Suspension response 2017-11-23 08:42:08 -03:00
Belen Curcio 5793ba45c3 Restoring message for suspend 2017-11-23 08:17:40 -03:00
Belen Curcio 903973ce4b Adding message 2017-11-23 08:13:03 -03:00
Belen Curcio c6c7961dc9 Merge branch 'mod-improv' of github.com:coralproject/talk into user-status-refactor 2017-11-23 08:08:44 -03:00
Belen Curcio fd0b475b84 banUser and unBanUser - removing setUserStatus 2017-11-23 08:07:13 -03:00
Belen Curcio b69195253b banUser and unBanUser wip - removing setStatus 2017-11-22 21:45:55 -03:00
Wyatt Johnson a537d9ac17 added message to ban/suspend workflows 2017-11-22 14:08:18 -07:00
Belen Curcio c742101250 Updating tests: dont wait for message 2017-11-22 12:46:43 -03:00
Belen Curcio dcbae2a117 Updating componets 2017-11-22 12:45:16 -03:00
Belen Curcio e530003c72 Hiding messages for suspension and bann 2017-11-22 12:38:07 -03:00
Belen Curcio 48e87c4659 Updating contations and deleting legacy code 2017-11-22 12:22:27 -03:00
Belen Curcio 9ae32b9366 removing message 2017-11-22 02:51:16 -03:00
Belen Curcio e782776ab5 refactor changeUsername - Product question, 2017-11-22 02:37:58 -03:00
Belen Curcio 75d914b894 updated tests, changeUsername container, etc 2017-11-22 02:13:26 -03:00
Belen Curcio 69c93cd048 wip 2017-11-22 01:43:35 -03:00
Belen Curcio e3a8656e32 wip ChangeUsername 2017-11-22 01:32:32 -03:00
Belen Curcio 137c5889eb user should not be suspended if username is rejecte 2017-11-22 01:07:10 -03:00
Belen Curcio 3b768c144b Bug suspending/banning/rejecting users, 2017-11-22 00:58:59 -03:00
Belen Curcio f19d6b6800 changes ChangeUsername 2017-11-22 00:53:01 -03:00
Wyatt Johnson 90daab8219 added USERS_NEW event to SSE bus 2017-11-21 14:36:34 -07:00
Wyatt Johnson 45e27fd6ad fixes to cli 2017-11-20 13:29:50 -07:00
Belen Curcio 2706e47f85 Now using changeUsername 2017-11-20 16:07:50 -03:00
Belen Curcio 43e9da9cbc setUsername refactor 2017-11-20 15:37:06 -03:00
Belen Curcio dbe88e58f4 setUsername refactor 2017-11-20 15:35:16 -03:00
Wyatt Johnson debda80db1 fixed query issues 2017-11-20 10:32:46 -07:00
Belen Curcio 1756c7c050 Reject username 2017-11-20 02:04:19 -03:00
Belen Curcio 01b8c7aee6 Safe access to objects 2017-11-20 01:22:44 -03:00
Belen Curcio 7b9d7163e1 updated e2e 2017-11-20 01:08:42 -03:00
Belen Curcio e6e5e5506e removing pending 2017-11-20 00:57:01 -03:00
Belen Curcio 8f12fcd09b Safe access 2017-11-20 00:38:52 -03:00
Belen Curcio 7ec239fd68 Banned User Stream 2017-11-19 22:52:26 -03:00
Belen Curcio 5299ea674e bug error view options 2017-11-19 22:50:14 -03:00
Belen Curcio 6a3c80468b Missing proptype 2017-11-19 22:48:45 -03:00
Belen Curcio aa4b386ee6 Banned User Stream 2017-11-19 22:45:05 -03:00
Belen Curcio 0459eceee7 refetch status 2017-11-19 22:40:06 -03:00
Belen Curcio 367ec00a02 Refactor for setUserStatus to setUserBanStatus 2017-11-19 21:18:50 -03:00
Belen Curcio e34ba248b9 Refactor for setUserStatus to setUserBanStatus 2017-11-19 21:04:04 -03:00
Belen Curcio 6d6030d60a Removing setCommenterStatus 2017-11-19 20:38:13 -03:00
Belen Curcio e941f00384 Suspend User Mutation 2017-11-19 20:30:11 -03:00
Wyatt Johnson e17b15ec2c Merge branch 'next' into user-status-refactor 2017-11-17 09:43:59 -07:00
Wyatt Johnson c98c9b3369 Merge branch 'master' into next 2017-11-17 09:29:31 -07:00
Wyatt Johnson 73cec00fb9 fixed migration issues 2017-11-17 09:17:43 -07:00
Belen Curcio 90a9ff251d wip 2017-11-15 14:39:59 -03:00
Belen Curcio a626dfafc8 Refactor Admin Side queries 2017-11-15 13:53:32 -03:00
Belen Curcio b4aeee9433 Client Refactor 2017-11-15 13:21:20 -03:00
Belen Curcio e8e103020c Merge branch 'user-status-refactor' of github.com:coralproject/talk into mod-improv 2017-11-14 13:54:21 -03:00
Belen Curcio ff6e41f2e1 styling 2017-11-14 13:13:02 -03:00
Belen Curcio aabd152314 Account History 2017-11-14 13:01:13 -03:00
Belen Curcio 6a17d04570 Merge branch 'master' of github.com:coralproject/talk into mod-improv 2017-11-14 13:00:20 -03:00
Belen Curcio 32547a214b Missing proptypes 2017-11-14 11:56:23 -03:00
Wyatt Johnson ef40e9dce5 spelling 2017-11-13 16:08:20 -07:00
Wyatt Johnson e090ba9123 Merge branch 'next' into user-status-refactor 2017-11-13 16:06:16 -07:00
Wyatt Johnson 81f825d4b3 Merge branch 'master' into next 2017-11-13 16:01:11 -07:00
Wyatt Johnson ecf02f454c staff members cannot be flagged 2017-11-13 15:57:54 -07:00
Wyatt Johnson 9f823c0b11 added action counts for users 2017-11-13 15:46:49 -07:00
Wyatt Johnson 401d8a423d added emails for status changes 2017-11-09 16:07:46 -07:00
Wyatt Johnson d80401a49b Merge branch 'next' into user-status-refactor 2017-11-09 14:59:47 -07:00
Wyatt Johnson cb768d3190 Merge branch 'master' into next 2017-11-09 14:59:32 -07:00
Kim Gardner 7be888bfa8 Bump release number for 4.0.0 2017-11-09 11:44:42 +00:00
Wyatt Johnson 77c927ac59 added migration, migration tests 🎉 2017-11-08 16:35:38 -07:00
Wyatt Johnson 0257e09a40 Merge branch 'next' into user-status-refactor 2017-11-08 13:52:48 -07:00
Wyatt Johnson 61df0e1eeb Merge branch 'master' into next 2017-11-08 13:49:21 -07:00
Wyatt Johnson 1b7f78b562 user queries 2017-11-08 13:40:46 -07:00
Wyatt Johnson 9e84c72148 added more mutations 2017-11-08 09:41:38 -07:00
Wyatt Johnson 7034003fd1 fixed changed package script 2017-11-02 17:55:31 -06:00
Wyatt Johnson b5bcbc4fc8 added username edit, tests 2017-11-02 17:54:56 -06:00
Wyatt Johnson 04e6f5b52d added prebuild 2017-11-02 17:23:09 -06:00
Wyatt Johnson 76a255fb7b initial pass at status support 2017-11-02 17:16:57 -06:00
Wyatt Johnson 1f3722edc1 Merge branch 'master' into user-status-refactor 2017-11-02 16:06:34 -06:00
Wyatt Johnson 3f7ddddba3 perms cleanup 2017-10-26 14:27:46 -06:00
Wyatt Johnson 91931b2e6f Merge branch 'master' into flags-locales 2017-10-10 11:22:59 -06:00
Wyatt Johnson cab94842aa small naming change 2017-10-10 11:22:19 -06:00
Wyatt Johnson 86c3c286ee Graph fixes and migrations
- Added migrations for mapping old keys
- Added migration for fixing old dont agree flags
- Removed enum requirement for output
2017-10-10 11:22:19 -06:00
Kim Gardner d6e7bb0a0a Merge branch 'master' into flags-locales 2017-10-10 13:08:19 +01:00
Wyatt Johnson 970a93360a Merge branch 'master' into flags-locales 2017-10-06 11:04:27 -06:00
Kim Gardner 7d9b588bd2 Merge branch 'master' into flags-locales 2017-10-05 11:34:06 +01:00
Wyatt Johnson 13c38c2215 added toxic comment translations 2017-10-04 13:48:10 -06:00
Wyatt Johnson 2b08ef5c42 updated translations 2017-10-04 11:08:34 -06:00
Wyatt Johnson b7ea9e7d97 Merge branch 'master' into flags-locales 2017-10-04 10:36:42 -06:00
Wyatt Johnson d85db01752 changed custom error to standardized error 2017-10-04 10:35:55 -06:00
Wyatt Johnson 69c03c9190 cleaned up graph mutators 2017-10-04 10:29:09 -06:00
Wyatt Johnson 00de72a6eb Initial work for locking flag type and providing translations 2017-10-03 17:20:40 -06:00
815 changed files with 27420 additions and 17717 deletions
+1 -1
View File
@@ -28,5 +28,5 @@ plugins/*
!plugins/talk-plugin-deep-reply-count
!plugins/talk-plugin-subscriber
!plugins/talk-plugin-flag-details
public
node_modules
+2
View File
@@ -13,6 +13,7 @@ client/coral-framework/graphql/introspection.json
.idea/
*.swp
*.DS_STORE
.prettierrc.json
coverage/
test/e2e/reports/
@@ -52,3 +53,4 @@ plugins/*
!plugins/talk-plugin-slack-notifications
**/node_modules/*
yarn-error.log
+7 -1
View File
@@ -1,5 +1,11 @@
{
"exec": "npm-run-all --parallel generate-introspection start:development",
"verbose": true,
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
"ext": "js,json,graphql"
"ext": "js,json,graphql,yml",
"watch": [
".",
"bin/cli",
"bin/cli-serve"
]
}
+1
View File
@@ -5,6 +5,7 @@ ONBUILD ARG TALK_THREADING_LEVEL=3
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
ONBUILD ARG TALK_DEFAULT_LANG=en
ONBUILD ARG TALK_PLUGINS_JSON
ONBUILD ARG TALK_WEBPACK_SOURCE_MAP
# Bundle app source
ONBUILD COPY . /usr/src/app
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 2017 Mozilla Foundation
Copyright 2018 Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+27 -6
View File
@@ -6,10 +6,11 @@ const merge = require('lodash/merge');
const helmet = require('helmet');
const plugins = require('./services/plugins');
const compression = require('compression');
const {HELMET_CONFIGURATION} = require('./config');
const {MOUNT_PATH} = require('./url');
const { HELMET_CONFIGURATION } = require('./config');
const { MOUNT_PATH } = require('./url');
const routes = require('./routes');
const debug = require('debug')('talk:app');
const { ENABLE_TRACING, APOLLO_ENGINE_KEY, PORT } = require('./config');
const app = express();
@@ -25,7 +26,7 @@ app.use((req, res, next) => {
//==============================================================================
// Inject server route plugins.
plugins.get('server', 'app').forEach(({plugin, app: callback}) => {
plugins.get('server', 'app').forEach(({ plugin, app: callback }) => {
debug(`added plugin '${plugin.name}'`);
// Pass the app to the plugin to mount it's routes.
@@ -41,15 +42,35 @@ if (process.env.NODE_ENV !== 'test') {
app.use(morgan('dev'));
}
if (ENABLE_TRACING && APOLLO_ENGINE_KEY) {
const { Engine } = require('apollo-engine');
const engine = new Engine({
engineConfig: {
apiKey: APOLLO_ENGINE_KEY,
},
graphqlPort: PORT,
endpoint: `${MOUNT_PATH}api/v1/graph/ql`,
});
engine.start();
app.use(engine.expressMiddleware());
}
// Trust the first proxy in front of us, this will enable us to trust the fact
// that SSL was terminated correctly.
app.set('trust proxy', 1);
// Enable a suite of security good practices through helmet. We disable
// frameguard to allow crossdomain injection of the embed.
app.use(helmet(merge(HELMET_CONFIGURATION, {
frameguard: false,
})));
app.use(
helmet(
merge(HELMET_CONFIGURATION, {
frameguard: false,
})
)
);
// Compress the responses if appropriate.
app.use(compression());
+13 -28
View File
@@ -4,8 +4,9 @@
* Module dependencies.
*/
const program = require('./commander');
const {head, map} = require('lodash');
require('./util');
const program = require('commander');
const { head, map } = require('lodash');
const Matcher = require('did-you-mean');
program
@@ -17,7 +18,6 @@ program
.command('token', 'work with the access tokens')
.command('users', 'work with the application auth')
.command('migration', 'provides utilities for migrating the database')
.command('verify', 'provides utilities for performing data verification')
.command(
'plugins',
'provides utilities for interacting with the plugin system'
@@ -25,33 +25,18 @@ program
.parse(process.argv);
// If the command wasn't found, output help.
const cmds = map(program.commands, '_name');
const cmd = head(program.args);
if (!cmds.includes(cmd)) {
const m = new Matcher(cmds);
const similarCMDs = m.list(cmd);
const commands = map(program.commands, '_name');
const command = head(program.args);
if (!commands.includes(command)) {
const m = new Matcher(commands);
const similarCommands = m.list(command);
console.error(`cli '${cmd}' is not a talk cli command. See 'cli --help'.`);
if (similarCMDs.length > 0) {
const sc = similarCMDs.map(({value}) => `\t${value}\n`).join('');
console.error(
`cli '${command}' is not a talk cli command. See 'cli --help'.`
);
if (similarCommands.length > 0) {
const sc = similarCommands.map(({ value }) => `\t${value}\n`).join('');
console.error(`\nThe most similar commands are\n${sc}`);
}
process.exit(1);
}
/**
* When this provess exists, check to see if we have a running command, if we do
* check to see if it is still running. If it is, then kill it with a SIGINT
* signal. This is for the use case where we want to kill the process that is
* labled with the PID written out by the parent process.
*/
process.once('exit', () => {
if (
// program.runningCommand &&
program.runningCommand.killed === false &&
program.runningCommand.exitCode === null
) {
program.runningCommand.kill('SIGINT');
}
});
+95 -26
View File
@@ -4,7 +4,8 @@
* Module dependencies.
*/
const program = require('./commander');
const util = require('./util');
const program = require('commander');
const parseDuration = require('ms');
const Table = require('cli-table');
const AssetModel = require('../models/asset');
@@ -12,34 +13,28 @@ const CommentModel = require('../models/comment');
const AssetsService = require('../services/assets');
const mongoose = require('../services/mongoose');
const scraper = require('../services/scraper');
const util = require('./util');
const inquirer = require('inquirer');
const { URL } = require('url');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
/**
* Lists all the assets registered in the database.
*/
async function listAssets() {
try {
let assets = await AssetModel.find({}).sort({'created_at': 1});
let assets = await AssetModel.find({}).sort({ created_at: 1 });
let table = new Table({
head: [
'ID',
'Title',
'URL'
]
head: ['ID', 'Title', 'URL'],
});
assets.forEach((asset) => {
assets.forEach(asset => {
table.push([
asset.id,
asset.title ? asset.title : '',
asset.url ? asset.url : ''
asset.url ? asset.url : '',
]);
});
@@ -61,13 +56,13 @@ async function refreshAssets(ageString) {
$or: [
{
scraped: {
$lte: age
}
$lte: age,
},
},
{
scraped: null
}
]
scraped: null,
},
],
});
// Queue all the assets for scraping.
@@ -95,7 +90,6 @@ async function updateURL(assetID, assetURL) {
async function merge(srcID, dstID) {
try {
// Grab the assets...
let [srcAsset, dstAsset] = await AssetsService.findByIDs([srcID, dstID]);
if (!srcAsset || !dstAsset) {
@@ -103,21 +97,22 @@ async function merge(srcID, dstID) {
}
// Count the affected resources...
let srcCommentCount = await CommentModel.find({asset_id: srcID}).count();
let srcCommentCount = await CommentModel.find({ asset_id: srcID }).count();
console.log(`Now going to update ${srcCommentCount} comments and delete the source Asset[${srcID}].`);
console.log(
`Now going to update ${srcCommentCount} comments and delete the source Asset[${srcID}].`
);
let {confirm} = await inquirer.prompt([
let { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Proceed with merge',
default: false
}
default: false,
},
]);
if (confirm) {
// Perform the merge!
await AssetsService.merge(srcID, dstID);
} else {
@@ -131,6 +126,70 @@ async function merge(srcID, dstID) {
}
}
async function rewrite(search, replace, options) {
try {
search = new RegExp(search);
const assets = await AssetModel.find({
url: { $regex: search },
});
if (assets.length === 0) {
console.log(`No assets found with the pattern: ${search}`);
return util.shutdown(0);
}
let opts = [];
assets.forEach(({ id, url: oldURL }) => {
// Replace the url.
const newURL = oldURL.replace(search, replace);
// Try to validate that the new url is valid.
try {
new URL(newURL);
} catch (err) {
throw new Error(
`Rewrite would have replaced the valid URL ${oldURL} with an invalid one ${newURL}`
);
}
opts.push({
find: { id },
updateOne: { $set: { url: newURL } },
id,
oldURL,
newURL,
});
});
if (opts.length > 0) {
if (options.dryRun) {
const table = new Table({ head: ['ID', 'Old URL', 'New URL'] });
opts.forEach(({ id, oldURL, newURL }) => {
table.push([id, oldURL, newURL]);
});
console.log(table.toString());
} else {
const bulk = AssetModel.collection.initializeUnorderedBulkOp();
opts.forEach(({ find, updateOne, oldURL, newURL }) => {
// If the url was updated with the operation, then queue up the update op.
if (newURL !== oldURL) {
bulk.find(find).updateOne(updateOne);
}
});
await bulk.execute();
console.log(`${opts.length} assets had their url's updated`);
}
}
util.shutdown(0);
} catch (err) {
console.error(err);
util.shutdown(1);
}
}
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
@@ -152,9 +211,19 @@ program
program
.command('merge <srcID> <dstID>')
.description('merges two assets together by moving comments from src to dst and deleting the src asset')
.description(
'merges two assets together by moving comments from src to dst and deleting the src asset'
)
.action(merge);
program
.command('rewrite <search> <replace>')
.option('-d, --dry-run', 'enables dry run of the replacement')
.description(
"rewrites asset url's using the provided regex replacement pattern"
)
.action(rewrite);
program.parse(process.argv);
// If there is no command listed, output help.
+14 -22
View File
@@ -4,27 +4,22 @@
* Module dependencies.
*/
const program = require('./commander');
const util = require('./util');
const program = require('commander');
const scraper = require('../services/scraper');
const mailer = require('../services/mailer');
const util = require('./util');
const mongoose = require('../services/mongoose');
const kue = require('../services/kue');
util.onshutdown([
() => mongoose.disconnect(),
]);
util.onshutdown([() => mongoose.disconnect()]);
/**
* Starts the job processor.
*/
function processJobs() {
// The scraper only needs to shutdown when the scraper has actually been
// started.
util.onshutdown([
() => kue.Task.shutdown()
]);
util.onshutdown([() => kue.Task.shutdown()]);
// Start the scraper processor.
scraper.process();
@@ -39,13 +34,15 @@ function processJobs() {
* @return {Promise}
*/
function removeJob(job) {
return new Promise((resolve, reject) => job.remove((err) => {
if (err) {
return reject(err);
}
return new Promise((resolve, reject) =>
job.remove(err => {
if (err) {
return reject(err);
}
return resolve(job);
}));
return resolve(job);
})
);
}
/**
@@ -82,17 +79,13 @@ async function getJobBatch(n, includeStuck) {
* Cleans up the jobs that are in the queue.
*/
async function cleanupJobs(options) {
// The scraper only needs to shutdown when the scraper has actually been
// started.
util.onshutdown([
() => kue.Task.shutdown()
]);
util.onshutdown([() => kue.Task.shutdown()]);
const n = 100;
try {
// Connect to redis by establishing a queue.
kue.Task.connect();
@@ -100,9 +93,8 @@ async function cleanupJobs(options) {
let jobs = await getJobBatch(n, options.stuck);
while (jobs.length > 0) {
// Remove all the jobs.
await Promise.all(jobs.map((job) => removeJob(job)));
await Promise.all(jobs.map(job => removeJob(job)));
jobCount += jobs.length;
+60 -32
View File
@@ -4,20 +4,18 @@
* Module dependencies.
*/
const program = require('./commander');
const util = require('./util');
const _ = require('lodash');
const program = require('commander');
const inquirer = require('inquirer');
const mongoose = require('../services/mongoose');
const MigrationService = require('../services/migration');
// Register shutdown hooks.
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
async function createMigration(name) {
try {
// Create the migration.
await MigrationService.create(name);
@@ -28,47 +26,60 @@ async function createMigration(name) {
}
}
async function runMigrations() {
async function runMigrations(options) {
const { yes, queryBatchSize, updateBatchSize } = options;
try {
if (!yes) {
const { backedUp } = await inquirer.prompt([
{
type: 'confirm',
name: 'backedUp',
message: 'Did you perform a database backup',
default: false,
},
]);
let {backedUp} = await inquirer.prompt([
{
type: 'confirm',
name: 'backedUp',
message: 'Did you perform a database backup',
default: false
if (!backedUp) {
throw new Error(
'Please backup your databases prior to migrations occuring'
);
}
]);
if (!backedUp) {
throw new Error('Please backup your databases prior to migrations occuring');
}
// Get the migrations to run.
let migrations = await MigrationService.listPending();
const migrations = await MigrationService.listPending();
console.log('Now going to run the following migrations:\n');
for (let {filename} of migrations) {
for (const { filename } of migrations) {
console.log(`\tmigrations/${filename}`);
}
let {confirm} = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Proceed with migrations',
default: false
if (!yes) {
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Proceed with migrations',
default: false,
},
]);
if (confirm) {
// Run the migrations.
await MigrationService.run(migrations, {
queryBatchSize,
updateBatchSize,
});
} else {
console.warn('Skipping migrations');
}
]);
if (confirm) {
// Run the migrations.
await MigrationService.run(migrations);
} else {
console.warn('Skipping migrations');
// Run the migrations.
await MigrationService.run(migrations, {
queryBatchSize,
updateBatchSize,
});
}
util.shutdown();
@@ -87,8 +98,25 @@ program
.description('creates a new migration')
.action(createMigration);
// Bypasses issue that defaults + coercion doesn't work well together.
// Ref: https://github.com/tj/commander.js/issues/400#issuecomment-310860869
const parse10 = _.ary(_.partialRight(parseInt, 10), 1);
program
.command('run')
.option(
'-q, --query-batch-size <n>',
'change the size of queried documents that are batched at a time',
parse10,
10000
)
.option(
'-u, --update-batch-size <n>',
'change the size of documents that are batched before the update is sent',
parse10,
20000
)
.option('-y, --yes', 'will answer yes to all questions')
.description('runs all pending migrations')
.action(runMigrations);
+158 -115
View File
@@ -7,24 +7,25 @@
// Interface heavily inspired by the yarn package manager:
// https://yarnpkg.com/
const program = require('./commander');
require('./util');
const program = require('commander');
const inquirer = require('inquirer');
// Make things colorful!
require('colors');
const emoji = require('node-emoji');
const dir = process.cwd();
const fs = require('fs-extra');
const path = require('path');
const dir = path.resolve(__dirname, '..');
const spawn = require('cross-spawn');
const semver = require('semver');
const resolve = require('resolve');
const {plugins, itteratePlugins, isInternal} = require('../plugins');
const { plugins, iteratePlugins, isInternal } = require('../plugins');
function existsInNodeModules(name) {
try {
resolve.sync(name, {basedir: dir});
resolve.sync(name, { basedir: dir });
return true;
} catch (e) {
@@ -38,13 +39,13 @@ function versionMatch(name, version) {
resolve.sync(name, {
basedir: dir,
packageFilter: (pkg) => {
packageFilter: pkg => {
if (pkg && pkg.version && semver.satisfies(pkg.version, version)) {
matched = true;
}
return pkg;
}
},
});
return matched;
@@ -55,7 +56,7 @@ function versionMatch(name, version) {
const EXTERNAL = /^\w[a-z\-0-9.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
function reconcilePackages({quiet = false, upgradeRemote = false}) {
function reconcilePackages({ quiet = false, upgradeRemote = false }) {
const fetchable = [];
const local = [];
const upgradable = [];
@@ -70,13 +71,14 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
}
for (let i in plugins) {
let section = itteratePlugins(plugins[i]);
let section = iteratePlugins(plugins[i]);
for (let j in section) {
let {name, version} = section[j];
let { name, version } = section[j];
let namespaced = name.charAt(0) === '@';
let dep = name.split('/')
let dep = name
.split('/')
.slice(0, namespaced ? 2 : 1)
.join('/');
@@ -90,7 +92,7 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
console.log(` l ${name}`);
}
local.push({name, version});
local.push({ name, version });
continue;
}
@@ -98,9 +100,8 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
if (!quiet) {
console.log(` m ${name}`);
}
fetchable.push({name, version});
fetchable.push({ name, version });
} else if (!versionMatch(dep, version)) {
// A plugin was found, yet the current version does not match the
// current version installed. We should warn if upgradeRemote is
// not enabled that it is currently not supported.
@@ -114,14 +115,14 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
console.log(` oe ${name} (package upgrade may be required)`);
upgradable.push({name, version});
upgradable.push({ name, version });
} else {
if (!quiet) {
console.log(` e ${name}`);
}
if (upgradeRemote) {
upgradable.push({name, version});
upgradable.push({ name, version });
}
}
}
@@ -131,33 +132,44 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
console.log();
}
return {local, fetchable, upgradable};
return { local, fetchable, upgradable };
}
async function reconcileRemotePlugins({skipLocal, dryRun, upgradeRemote}) {
console.log(`\n[${skipLocal ? '1/2' : '2/3'}] ${emoji.get('mag')} Reconciling plugins...`.yellow);
const {fetchable, upgradable} = reconcilePackages({upgradeRemote});
async function reconcileRemotePlugins({ skipLocal, dryRun, upgradeRemote }) {
console.log(
`\n[${skipLocal ? '1/2' : '2/3'}] ${emoji.get(
'mag'
)} Reconciling plugins...`.yellow
);
const { fetchable, upgradable } = reconcilePackages({ upgradeRemote });
console.log(`[${skipLocal ? '2/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`.yellow);
console.log(
`[${skipLocal ? '2/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`
.yellow
);
if (fetchable.length > 0) {
console.log(`$ yarn add --ignore-scripts ${fetchable.map(({name, version}) => `${name}@${version}`.cyan)}`);
console.log(
`$ yarn add --ignore-scripts ${fetchable.map(
({ name, version }) => `${name}@${version}`.cyan
)}`
);
if (!dryRun) {
let args = [
'add',
'--ignore-scripts',
...fetchable.map(({name, version}) => `${name}@${version}`)
...fetchable.map(({ name, version }) => `${name}@${version}`),
];
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit']
stdio: ['ignore', 'pipe', 'inherit'],
});
if (output.status) {
throw new Error('Could not install external plugins, errors occured during install');
throw new Error(
'Could not install external plugins, errors occured during install'
);
}
console.log(output.stdout.toString());
@@ -165,36 +177,45 @@ async function reconcileRemotePlugins({skipLocal, dryRun, upgradeRemote}) {
}
if (upgradable.length > 0) {
console.log(`$ yarn upgrade ${upgradable.map(({name, version}) => `${name}@${version}`.cyan)}`);
console.log(
`$ yarn upgrade ${upgradable.map(
({ name, version }) => `${name}@${version}`.cyan
)}`
);
if (!dryRun) {
let args = [
'upgrade',
...upgradable.map(({name, version}) => `${name}@${version}`)
...upgradable.map(({ name, version }) => `${name}@${version}`),
];
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit']
stdio: ['ignore', 'pipe', 'inherit'],
});
if (output.status) {
throw new Error('Could not install external plugins, errors occured during install');
throw new Error(
'Could not install external plugins, errors occured during install'
);
}
console.log(output.stdout.toString());
}
}
return {upgradable, fetchable};
return { upgradable, fetchable };
}
async function reconcileLocalPlugins({skipRemote, dryRun}) {
console.log(`\n[${skipRemote ? '1/1' : '1/3'}] ${emoji.get('pick')} Installing local plugin dependencies...\n`.yellow);
const {local} = reconcilePackages({quiet: true});
async function reconcileLocalPlugins({ skipRemote, dryRun }) {
console.log(
`\n[${skipRemote ? '1/1' : '1/3'}] ${emoji.get(
'pick'
)} Installing local plugin dependencies...\n`.yellow
);
const { local } = reconcilePackages({ quiet: true });
for (let i in local) {
let {name} = local[i];
let { name } = local[i];
if (!fs.existsSync(path.join(dir, 'plugins', name, 'package.json'))) {
continue;
@@ -209,11 +230,13 @@ async function reconcileLocalPlugins({skipRemote, dryRun}) {
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit'],
cwd: wd
cwd: wd,
});
if (output.status) {
throw new Error('Could not install local plugin dependencies, errors occured during install');
throw new Error(
'Could not install local plugin dependencies, errors occurred during install'
);
}
console.log(output.stdout.toString());
@@ -224,53 +247,67 @@ async function reconcileLocalPlugins({skipRemote, dryRun}) {
// This traverses the local plugins and installs any dependencies listed there,
// this only is really needed for plugins that are installed via docker because
// core plugins will have their dependencies already included in core.
async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote}) {
let startTime = new Date();
async function reconcilePluginDeps({
skipLocal,
skipRemote,
dryRun,
upgradeRemote,
}) {
try {
let startTime = new Date();
// We don't need to do anything if we skip everything....
if (skipLocal && skipRemote) {
return;
}
// Traverse local plugins and install dependencies if enabled.
if (!skipLocal) {
await reconcileLocalPlugins({skipRemote, dryRun});
}
// Locate any external plugins and install them.
if (!skipRemote) {
let results = [];
try {
results = await reconcileRemotePlugins({skipLocal, skipRemote, dryRun, upgradeRemote});
} catch (e) {
throw e;
// We don't need to do anything if we skip everything....
if (skipLocal && skipRemote) {
return;
}
let status;
if (dryRun) {
status = '[dry-run] success'.green;
} else {
status = 'success'.green;
// Traverse local plugins and install dependencies if enabled.
if (!skipLocal) {
await reconcileLocalPlugins({ skipRemote, dryRun });
}
let message;
if (results.upgradable.length === 0 && results.fetchable.length === 0) {
message = 'Already up-to-date.';
} else if (results.upgradable.length === 0) {
message = `Fetched ${results.fetchable.length} new plugins.`;
} else if (results.fetchable.length === 0) {
message = `Upgraded ${results.upgradable.length} new plugins.`;
} else {
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${results.upgradable.length} plugins.`;
// Locate any external plugins and install them.
if (!skipRemote) {
const results = await reconcileRemotePlugins({
skipLocal,
skipRemote,
dryRun,
upgradeRemote,
});
let status;
if (dryRun) {
status = '[dry-run] success'.green;
} else {
status = 'success'.green;
}
let message;
if (results.upgradable.length === 0 && results.fetchable.length === 0) {
message = 'Already up-to-date.';
} else if (results.upgradable.length === 0) {
message = `Fetched ${results.fetchable.length} new plugins.`;
} else if (results.fetchable.length === 0) {
message = `Upgraded ${results.upgradable.length} new plugins.`;
} else {
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
results.upgradable.length
} plugins.`;
}
console.log(`\n${status} ${message}`);
}
console.log(`\n${status} ${message}`);
let endTime = new Date();
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(
2
);
console.log(`✨ Done in ${totalTime}s.`);
} catch (err) {
console.error(err);
process.exit(1);
}
let endTime = new Date();
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(2);
console.log(`✨ Done in ${totalTime}s.`);
}
async function createSeedPlugin() {
@@ -278,8 +315,7 @@ async function createSeedPlugin() {
function pluginNameExists(pluginName) {
const pluginNames = fs.readdirSync(pluginsDir);
return !!pluginNames
.filter((pn) => pn === pluginName).length;
return !!pluginNames.filter(pn => pn === pluginName).length;
}
let answers = await inquirer.prompt([
@@ -287,8 +323,7 @@ async function createSeedPlugin() {
type: 'input',
name: 'pluginName',
message: 'Plugin Name:',
validate: (input) => {
validate: input => {
if (pluginNameExists(input)) {
return 'Please, choose another name. That name already exists';
}
@@ -298,23 +333,23 @@ async function createSeedPlugin() {
}
return 'Plugin Name is required.';
}
},
},
{
type: 'confirm',
name: 'server',
message: 'Is this plugin extending the server capabilities?'
message: 'Is this plugin extending the server capabilities?',
},
{
type: 'confirm',
name: 'client',
message: 'Is this plugin extending the client capabilities?'
message: 'Is this plugin extending the client capabilities?',
},
{
type: 'confirm',
name: 'addPluginsJson',
message: 'Should we add it to the plugins.json?'
}
message: 'Should we add it to the plugins.json?',
},
]);
//==============================================================================
@@ -325,41 +360,41 @@ async function createSeedPlugin() {
const newPluginPath = path.join(pluginsDir, answers.pluginName);
if (fs.existsSync(seedPlugin)) {
if (answers.server && answers.client) {
// This is a server-side and client-side plugin!, let's copy the template
fs.copySync(seedPlugin, newPluginPath);
} else {
} else {
fs.copySync(seedPlugin, newPluginPath, {
filter: p => {
// Allowing plugin folder and files with no subfolders
const rootRx = /plugin$|plugin\/[^/]*(\.).{2,3}/gim;
if (
rootRx.test(p) &&
(fs.lstatSync(p).isDirectory() || fs.lstatSync(p).isFile())
) {
return true;
}
fs.copySync(seedPlugin, newPluginPath, {filter: (p) => {
// If it's a client-side plugin, copying client folder
if (answers.client) {
return /client/.test(p);
}
// Allowing plugin folder and files with no subfolders
const rootRx = /plugin$|plugin\/[^/]*(\.).{2,3}/igm;
if (rootRx.test(p) && (fs.lstatSync(p).isDirectory() || fs.lstatSync(p).isFile())) {
return true;
}
// If it's a client-side plugin, copying client folder
if (answers.client) {
return /client/.test(p);
}
// If it's a server-side plugin, copying server folder
if (answers.server) {
return /server/.test(p);
}
}});
// If it's a server-side plugin, copying server folder
if (answers.server) {
return /server/.test(p);
}
},
});
}
// Let's add this to the plugins.json
if (answers.addPluginsJson) {
const pluginsJson = path.resolve(__dirname, '..', 'plugins.json');
fs.readJson(pluginsJson)
.then((j) => {
fs
.readJson(pluginsJson)
.then(j => {
// This is a client-side plugin, let's push this.
if (answers.client) {
j.client.push(answers.pluginName);
@@ -376,14 +411,17 @@ async function createSeedPlugin() {
fs.writeFileSync(pluginsJson, output);
}
})
.catch((err) => {
.catch(err => {
console.error(err);
});
}
console.log(`✨ Yay! Plugin created! Find your plugin: ${answers.pluginName} in the ./plugins folder`);
console.log(
`✨ Yay! Plugin created! Find your plugin: ${
answers.pluginName
} in the ./plugins folder`
);
}
}
//==============================================================================
@@ -402,9 +440,14 @@ program
program
.command('reconcile')
.description('reconciles local plugin dependencies and downloads external plugins')
.description(
'reconciles local plugin dependencies and downloads external plugins'
)
.option('-u, --upgrade-remote', 'upgrades remote dependencies')
.option('-d, --dry-run', 'does not actually change anything on the filesystem acts only as a simulation')
.option(
'-d, --dry-run',
'does not actually change anything on the filesystem acts only as a simulation'
)
.option('--skip-local', 'skips the local dependancy reconciliation')
.option('--skip-remote', 'skips the remote plugin reconciliation')
.action(reconcilePluginDeps);
+6 -4
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
const program = require('./commander');
const util = require('./util');
const program = require('commander');
const serve = require('../serve');
//==============================================================================
@@ -10,12 +10,14 @@ const serve = require('../serve');
program
.option('-j, --jobs', 'enable job processing on this thread')
.option('-w, --websockets', 'enable the websocket (subscriptions) handler on this thread')
.option(
'-w, --websockets',
'enable the websocket (subscriptions) handler on this thread'
)
.parse(process.argv);
// Start serving.
serve({jobs: program.jobs, websockets: program.websockets}).catch((err) => {
serve({ jobs: program.jobs, websockets: program.websockets }).catch(err => {
console.error(err);
util.shutdown(1);
});
+5 -5
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env node
const program = require('./commander');
const util = require('./util');
const program = require('commander');
const inquirer = require('inquirer');
const mongoose = require('../services/mongoose');
const SettingsService = require('../services/settings');
const util = require('./util');
// Register the shutdown criteria.
util.onshutdown([() => mongoose.disconnect()]);
@@ -16,12 +16,12 @@ async function changeOrgName() {
try {
let settings = await SettingsService.retrieve();
let {organizationName} = await inquirer.prompt([
let { organizationName } = await inquirer.prompt([
{
name: 'organizationName',
message: 'Organization Name',
default: settings.organizationName
}
default: settings.organizationName,
},
]);
if (settings.organizationName !== organizationName) {
+49 -50
View File
@@ -4,7 +4,8 @@
* Module dependencies.
*/
const program = require('./commander');
const util = require('./util');
const program = require('commander');
const inquirer = require('inquirer');
const mongoose = require('../services/mongoose');
const SettingModel = require('../models/setting');
@@ -12,13 +13,11 @@ const MODERATION_OPTIONS = require('../models/enum/moderation_options');
const SettingsService = require('../services/settings');
const SetupService = require('../services/setup');
const UsersService = require('../services/users');
const util = require('./util');
const MigrationService = require('../services/migration');
const errors = require('../errors');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
//==============================================================================
// Setting up the program command line arguments.
@@ -34,19 +33,15 @@ program
//==============================================================================
const performSetup = async () => {
// Get the current settings, we are expecing an error here.
try {
// Try to get the settings.
await SettingsService.retrieve();
// We should NOT have gotten a settings object, this means that the
// application is already setup. Error out here.
throw errors.ErrSettingsInit;
} catch (e) {
// If the error is `not init`, then we're good, otherwise, it's something
// else.
if (e !== errors.ErrSettingsNotInit) {
@@ -57,6 +52,12 @@ const performSetup = async () => {
if (program.defaults) {
await SettingsService.init();
// Get the migrations to run.
let migrations = await MigrationService.listPending();
// Perform all migrations.
await MigrationService.run(migrations);
console.log('Settings created.');
console.log('\nTalk is now installed!');
@@ -66,7 +67,9 @@ const performSetup = async () => {
// Create the base settings model.
let settings = new SettingModel();
console.log('\nWe\'ll ask you some questions in order to setup your installation of Talk.\n');
console.log(
"\nWe'll ask you some questions in order to setup your installation of Talk.\n"
);
let answers = await inquirer.prompt([
{
@@ -74,31 +77,31 @@ const performSetup = async () => {
name: 'organizationName',
message: 'Organization Name',
default: settings.organizationName,
validate: (input) => {
validate: input => {
if (input && input.length > 0) {
return true;
}
return 'Organization Name is required.';
}
},
},
{
type: 'list',
choices: MODERATION_OPTIONS,
name: 'moderation',
default: settings.moderation,
message: 'Select a moderation mode'
message: 'Select a moderation mode',
},
{
type: 'confirm',
name: 'requireEmailConfirmation',
default: settings.requireEmailConfirmation,
message: 'Should emails always be confirmed'
message: 'Should emails always be confirmed',
},
]);
// Update the settings that were changed.
Object.keys(answers).forEach((key) => {
Object.keys(answers).forEach(key => {
if (answers[key] !== undefined) {
settings[key] = answers[key];
}
@@ -109,105 +112,101 @@ const performSetup = async () => {
type: 'confirm',
name: 'inputWhitelistedDomains',
default: true,
message: 'Would you like to specify a whitelisted domain'
message: 'Would you like to specify a whitelisted domain',
},
{
type: 'input',
name: 'whitelistedDomain',
message: 'Whitelisted Domain',
when: ({inputWhitelistedDomains}) => inputWhitelistedDomains,
validate: (input) => {
when: ({ inputWhitelistedDomains }) => inputWhitelistedDomains,
validate: input => {
if (input && input.length > 0) {
return true;
}
return 'Whitelisted Domain cannot be empty.';
}
}
},
},
]);
if (answers.inputWhitelistedDomains) {
settings.domains.whitelist = [answers.whitelistedDomain];
}
console.log('\nWe\'ll ask you some questions about your first admin user.\n');
console.log("\nWe'll ask you some questions about your first admin user.\n");
let user = await inquirer.prompt([
{
type: 'input',
name: 'username',
message: 'Username',
filter: (username) => {
return UsersService
.isValidUsername(username, false)
.catch((err) => {
throw err.message;
});
}
filter: username => {
return UsersService.isValidUsername(username, false).catch(err => {
throw err.message;
});
},
},
{
name: 'email',
message: 'Email',
format: 'email',
validate: (value) => {
validate: value => {
if (value && value.length >= 3) {
return true;
}
return 'Email is required';
}
},
},
{
name: 'password',
message: 'Password',
type: 'password',
filter: (password) => {
return UsersService
.isValidPassword(password)
.catch((err) => {
throw err.message;
});
}
filter: password => {
return UsersService.isValidPassword(password).catch(err => {
throw err.message;
});
},
},
{
name: 'confirmPassword',
message: 'Confirm Password',
type: 'password',
filter: (confirmPassword, {password}) => {
filter: (confirmPassword, { password }) => {
if (password !== confirmPassword) {
return Promise.reject(new Error('Passwords do not match'));
}
return UsersService
.isValidPassword(confirmPassword)
.catch((err) => {
throw err.message;
});
}
return UsersService.isValidPassword(confirmPassword).catch(err => {
throw err.message;
});
},
},
]);
let {user: newUser} = await SetupService.setup({
let { user: newUser } = await SetupService.setup({
settings: settings.toObject(),
user: {
email: user.email,
username: user.username,
password: user.password
}
password: user.password,
},
});
console.log('Settings created.');
console.log(`User ${newUser.id} created.`);
console.log('\nTalk is now installed!');
console.log('\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.');
console.log(
'\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.'
);
};
// Start tthe setup process.
// Start the setup process.
performSetup()
.then(() => {
util.shutdown();
})
.catch((e) => {
.catch(e => {
console.error(e);
util.shutdown(1);
});
+7 -19
View File
@@ -4,35 +4,25 @@
* Module dependencies.
*/
const program = require('./commander');
const util = require('./util');
const program = require('commander');
const mongoose = require('../services/mongoose');
const TokensService = require('../services/tokens');
const util = require('./util');
const Table = require('cli-table');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
async function listTokens(userID) {
try {
let tokens = await TokensService.list(userID);
let table = new Table({
head: [
'ID',
'Name',
'Status'
]
head: ['ID', 'Name', 'Status'],
});
tokens.forEach((token) => {
table.push([
token.id,
token.name,
token.active ? 'Active' : 'Revoked'
]);
tokens.forEach(token => {
table.push([token.id, token.name, token.active ? 'Active' : 'Revoked']);
});
console.log(table.toString());
@@ -46,7 +36,6 @@ async function listTokens(userID) {
async function revokeToken(tokenID) {
try {
await TokensService.revoke(null, tokenID);
console.log(`Revoked Token[${tokenID}]`);
@@ -60,8 +49,7 @@ async function revokeToken(tokenID) {
async function createToken(userID, tokenName) {
try {
let {pat: {id}, jwt} = await TokensService.create(userID, tokenName);
let { pat: { id }, jwt } = await TokensService.create(userID, tokenName);
console.log(`Created Token[${id}] for User[${userID}] = ${jwt}`);
+236 -422
View File
@@ -4,160 +4,135 @@
* Module dependencies.
*/
const program = require('./commander');
const util = require('./util');
const program = require('commander');
const inquirer = require('inquirer');
const { graphql } = require('graphql');
const helpers = require('../services/migration/helpers');
const { stripIndent } = require('common-tags');
const Table = require('cli-table');
// Make things colorful!
require('colors');
// Register the autocomplete plugin.
inquirer.registerPrompt(
'autocomplete',
require('inquirer-autocomplete-prompt')
);
const schema = require('../graph/schema');
const Context = require('../graph/context');
const UsersService = require('../services/users');
const UserModel = require('../models/user');
const CommentModel = require('../models/comment');
const ActionModel = require('../models/action');
const USER_ROLES = require('../models/enum/user_roles');
const mongoose = require('../services/mongoose');
const util = require('./util');
const Table = require('cli-table');
const databaseVerifications = require('./verifications/database');
const validateRequired = (msg = 'Field is required', len = 1) => (input) => {
if (input && input.length >= len) {
return true;
}
return msg;
};
// Regeister the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
function getUserCreateAnswers(options) {
if (options.flag_mode) {
let user = {
email: options.email,
password: options.password,
confirmPassword: options.password,
username: options.name,
roles: []
};
if (options.role && USER_ROLES.indexOf(options.role) > -1) {
user.roles = [options.role];
}
return Promise.resolve(user);
}
return inquirer.prompt([
{
name: 'email',
message: 'Email',
format: 'email',
validate: validateRequired('Email is required')
},
{
name: 'password',
message: 'Password',
type: 'password',
filter: (password) => {
return UsersService
.isValidPassword(password)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'confirmPassword',
message: 'Confirm Password',
type: 'password',
filter: (confirmPassword) => {
return UsersService
.isValidPassword(confirmPassword)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'username',
message: 'Username',
filter: (username) => {
return UsersService
.isValidUsername(username)
.catch((err) => {
throw err.message;
});
}
},
{
name: 'roles',
message: 'User Role',
type: 'checkbox',
choices: USER_ROLES
}
]);
}
// Register the shutdown criteria.
util.onshutdown([() => mongoose.disconnect()]);
/**
* Prompts for input and registers a user based on those.
* transforms a specific action to a removal action on the target model.
*/
async function createUser(options) {
try {
const answers = await getUserCreateAnswers(options);
if (answers.password !== answers.confirmPassword) {
throw new Error('Passwords do not match');
}
const user = await UsersService.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim());
console.log(`Created user ${user.id}.`);
if (answers.roles.length > 0) {
return Promise.all(answers.roles.map((role) => {
return UsersService
.addRoleToUser(user.id, role)
.then(() => {
console.log(`Added the role ${role} to User ${user.id}.`);
});
}));
}
util.shutdown();
} catch (err) {
console.error(err);
util.shutdown(1);
}
}
const actionDecrTransformer = ({ item_id, action_type, group_id }) => ({
query: { id: item_id },
update: {
$inc: {
[`action_counts.${action_type.toLowerCase()}`]: -1,
[`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`]: -1,
},
},
});
/**
* Deletes a user.
* Deletes a user and cleans up their associated verifications.
*/
async function deleteUser(userID) {
try {
// Find the user we're removing.
const user = await UserModel.findOne({id: userID});
const user = await UserModel.findOne({ id: userID });
if (!user) {
throw new Error(`user with id ${userID} not found`);
}
printUserAsTable(user);
console.warn(stripIndent`
This will delete the above user.
This might take a long time if there is a lot of data, please confirm that
you want to continue.
`);
const { confirm } = await inquirer.prompt({
type: 'confirm',
name: 'confirm',
message: 'Continue',
default: false,
});
if (!confirm) {
return util.shutdown();
}
const { transformSingleWithCursor } = helpers({
queryBatchSize: 10000,
updateBatchSize: 10000,
});
console.warn("Removing user's actions");
// Remove all actions against comments.
await transformSingleWithCursor(
ActionModel.collection.find({ user_id: user.id, item_type: 'COMMENTS' }),
actionDecrTransformer,
CommentModel
);
// Remove all actions against users.
await transformSingleWithCursor(
ActionModel.collection.find({ user_id: user.id, item_type: 'USERS' }),
actionDecrTransformer,
UserModel
);
// Remove all the user's actions.
await ActionModel
.where({user_id: user.id})
.setOptions({multi: true})
await ActionModel.where({ user_id: user.id })
.setOptions({ multi: true })
.remove();
console.warn("Removing user's comments");
// Removes all the user's reply counts on each of the comments that they
// have commented on.
await transformSingleWithCursor(
CommentModel.collection.aggregate([
{ $match: { author_id: user.id } },
{
$group: {
_id: '$parent_id',
count: { $sum: 1 },
},
},
]),
({ _id: parent_id, count }) => ({
query: { id: parent_id },
update: {
$inc: {
reply_count: -1 * count,
},
},
}),
CommentModel
);
// Remove all the user's comments.
await CommentModel
.where({author_id: user.id})
.setOptions({multi: true})
await CommentModel.where({ author_id: user.id })
.setOptions({ multi: true })
.remove();
// Update the counts that might have changed.
for (const verification of databaseVerifications) {
await verification({fix: true, limit: Infinity, batch: 1000});
}
console.warn('Removing the user');
// Remove the user.
await user.remove();
@@ -169,146 +144,94 @@ async function deleteUser(userID) {
}
}
/**
* Changes the password for a user.
*/
function passwd(userID) {
inquirer.prompt([
function printUserAsTable(user) {
let table = new Table({});
table.push(
{ ID: user.id.gray },
{ Username: user.username },
{ Emails: user.emails },
{ Tags: user.tags ? user.tags.map(({ tag: { name } }) => name) : '' },
{ Role: user.role },
{ Verified: user.hasVerifiedEmail },
{ Username: user.status.username.status },
{ Banned: user.banned },
{
name: 'password',
message: 'Password',
type: 'password',
validate: validateRequired('Password is required')
},
{
name: 'confirmPassword',
message: 'Confirm Password',
type: 'password',
validate: validateRequired('Confirm Password is required')
Suspension: user.suspended
? `Until ${user.status.suspension.until}`
: false,
}
])
.then((answers) => {
if (answers.password !== answers.confirmPassword) {
throw new Error('Password mismatch');
}
);
return UsersService.changePassword(userID, answers.password);
})
.then(() => {
console.log('Password changed.');
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
console.log(table.toString());
}
/**
* Updates the user from the options array.
* Searches for users based on their username and email address.
*/
function updateUser(userID, options) {
const updates = [];
if (options.email && typeof options.email === 'string' && options.email.length > 0) {
let q = UserModel.update({
'id': userID,
'profiles.provider': 'local'
}, {
$set: {
'profiles.$.id': options.email
async function searchUsers() {
const ctx = Context.forSystem();
const searchQuery = `
query SearchUsers($value: String) {
users(query: {value: $value}) {
nodes {
id
username
role
profiles {
id
provider
}
}
}
});
}
`;
updates.push(q);
}
if (options.name && typeof options.name === 'string' && options.name.length > 0) {
let q = UserModel.update({
'id': userID
}, {
$set: {
username: options.name
}
});
updates.push(q);
}
Promise
.all(updates.map((q) => q.exec()))
.then(() => {
console.log(`User ${userID} updated.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Lists all the users registered in the database.
*/
function listUsers() {
UsersService
.all()
.then((users) => {
let table = new Table({
head: [
'ID',
'Username',
'Profiles',
'Roles',
'Status',
'State'
]
});
users.forEach((user) => {
let state = user.disabled ? 'Disabled' : 'Enabled';
const profile = user.profiles.find(({provider}) => provider === 'local');
if (profile && profile.metadata && profile.metadata.confirmed_at) {
state += ', Verified';
} else {
state += ', Unverified';
try {
const answers = await inquirer.prompt({
type: 'autocomplete',
name: 'userID',
message: 'Search for a user',
source: async (answers, value) => {
if (value === null) {
value = '';
}
table.push([
user.id,
user.username,
user.profiles.map((p) => p.provider).join(', '),
user.roles.join(', '),
user.status,
state
]);
});
const { data, errors } = await graphql(schema, searchQuery, {}, ctx, {
value,
});
if (errors && errors.length > 0) {
throw errors[0];
}
console.log(table.toString());
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
if (data.users === null) {
return [];
}
/**
* Merges two users using the specified ID's.
* @param {String} dstUserID id of the user to which is the target of the merge
* @param {String} srcUserID id of the user to which is the source of the merge
*/
function mergeUsers(dstUserID, srcUserID) {
UsersService
.mergeUsers(dstUserID, srcUserID)
.then(() => {
console.log(`User ${srcUserID} was merged into user ${dstUserID}.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
return data.users.nodes.map(user => {
const emails = user.profiles
.filter(({ provider }) => provider === 'local')
.map(({ id }) => id)
.join(', ');
return {
name: `${user.username} (${emails}) ${user.id.gray} - ${
user.role.gray
}`,
value: user.id,
};
});
},
});
const { userID } = answers;
const user = await UserModel.findOne({ id: userID });
printUserAsTable(user);
util.shutdown(0);
} catch (err) {
console.error(err);
util.shutdown(1);
}
}
/**
@@ -316,127 +239,70 @@ function mergeUsers(dstUserID, srcUserID) {
* @param {String} userUD id of the user to add the role to
* @param {String} role the role to add
*/
function addRole(userID, role) {
async function setUserRole(userID) {
try {
const { role } = await inquirer.prompt([
{
name: 'role',
message: 'User Role',
type: 'list',
choices: USER_ROLES,
},
]);
if (USER_ROLES.indexOf(role) === -1) {
console.error(`Role '${role}' is not supported. Supported roles are ${USER_ROLES.join(', ')}.`);
await UsersService.setRole(userID, role);
console.log(`Set User ${userID} to the ${role} role.`);
util.shutdown();
} catch (err) {
console.error(err);
util.shutdown(1);
return;
}
UsersService
.addRoleToUser(userID, role)
.then(() => {
console.log(`Added the ${role} role to User ${userID}.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Removes a role from a user
* @param {String} userUD id of the user to remove the role from
* @param {String} role the role to remove
*/
function removeRole(userID, role) {
if (USER_ROLES.indexOf(role) === -1) {
console.error(`Role '${role}' is not supported. Supported roles are ${USER_ROLES.join(', ')}.`);
util.shutdown(1);
return;
}
UsersService
.removeRoleFromUser(userID, role)
.then(() => {
console.log(`Removed the ${role} role from User ${userID}.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Ban a user
* @param {String} userID id of the user to ban
*/
function ban(userID) {
UsersService
.setStatus(userID, 'BANNED')
.then(() => {
console.log(`Banned the User ${userID}.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Unban a user
* @param {String} userUD id of the user to remove the role from
*/
function unban(userID) {
UsersService
.setStatus(userID, 'ACTIVE')
.then(() => {
console.log(`Unban the User ${userID}.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Disable a given user.
* @param {String} userID the ID of a user to disable
*/
function disableUser(userID) {
UsersService
.disableUser(userID)
.then(() => {
console.log(`User ${userID} was disabled.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Enabled a given user.
* @param {String} userID the ID of a user to enable
*/
function enableUser(userID) {
UsersService
.enableUser(userID)
.then(() => {
console.log(`User ${userID} was enabled.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown(1);
});
}
/**
* Verifies an email address for a user.
*
* @param userID the user's id
* @param email the user's email address to be verified
* @param email the user's email address to be verified, otherwise verifies the
* first email if there is one, if there are multiple, you get a
* prompt.
*/
async function verify(userID, email) {
async function verifyUserEmail(userID, email) {
try {
// Get the user.
const user = await UserModel.findOne({ id: userID });
if (!user) {
throw new Error(`user with ID ${userID} cannot be found`);
}
// Get all the user's email addresses.
const emails = user.emails;
if (emails.length === 0) {
throw new Error('user did not have any email addresses');
}
if (!email && emails.length === 1) {
// The email wasn't passed, and there is only one option.
email = emails[0];
} else if (!emails.includes(email)) {
// The email passed doesn't belong to this user.
throw new Error(`user does not have the email ${email}`);
} else if (emails.length > 1) {
// The email wasn't passed, and there is more than one choice.
const answers = await inquirer.prompt([
{
name: 'email',
message: 'Select Email to Verify',
type: 'list',
choices: emails,
},
]);
email = answers.email;
}
// Verify the email.
await UsersService.confirmEmail(userID, email);
console.log(`User ${userID} had their email ${email} verified.`);
util.shutdown();
@@ -450,77 +316,25 @@ async function verify(userID, email) {
// Setting up the program command line arguments.
//==============================================================================
program
.command('create')
.option('--email [email]', 'Email to use')
.option('--password [password]', 'Password to use')
.option('--name [name]', 'Name to use')
.option('--role [role]', 'Role to add')
.option('-f, --flag_mode', 'Source from flags instead of prompting')
.description('create a new user')
.action(createUser);
program
.command('delete <userID>')
.description('delete a user')
.action(deleteUser);
program
.command('passwd <userID>')
.description('change a password for a user')
.action(passwd);
program
.command('update <userID>')
.option('--email [email]', 'Email to use')
.option('--name [name]', 'Name to use')
.description('update a user')
.action(updateUser);
program
.command('list')
.description('list all the users in the database')
.action(listUsers);
.description('searches for a user based on their stored username and email')
.action(searchUsers);
program
.command('merge <dstUserID> <srcUserID>')
.description('merge srcUser into the dstUser')
.action(mergeUsers);
program
.command('addrole <userID> <role>')
.description('adds a role to a given user')
.action(addRole);
program
.command('removerole <userID> <role>')
.description('removes a role from a given user')
.action(removeRole);
program
.command('ban <userID>')
.description('ban a given user')
.action(ban);
program
.command('uban <userID>')
.description('unban a given user')
.action(unban);
program
.command('disable <userID>')
.description('disable a given user from logging in')
.action(disableUser);
program
.command('enable <userID>')
.description('enable a given user from logging in')
.action(enableUser);
.command('set-role <userID> <role>')
.description('sets the role on a user')
.action(setUserRole);
program
.command('verify <userID> <email>')
.description('verifies the given user\'s email address')
.action(verify);
.description("verifies the given user's email address")
.action(verifyUserEmail);
program.parse(process.argv);
-49
View File
@@ -1,49 +0,0 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
const program = require('./commander');
const mongoose = require('../services/mongoose');
const util = require('./util');
const databaseVerifications = require('./verifications/database');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
async function database({fix = false, limit = Infinity, batch = 1000}) {
try {
for (const verification of databaseVerifications) {
await verification({fix, limit, batch});
}
} catch (err) {
console.error(`Failed to process all the ${databaseVerifications.length} verifications`, err);
util.shutdown(1);
return;
}
util.shutdown();
}
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
program
.command('db')
.description('verifies the database integrity')
.option('-f, --fix', 'fix the problems found with database inconsistencies')
.option('-l, --limit [size]', 'limit the amount of documents to process in a single pass, this will ensure only a maximum number of batch operations are issued [default: inf]', parseInt)
.option('-b, --batch [size]', 'batch size to process verifications and repairs of documents [default: 1000]', parseInt)
.action(database);
program.parse(process.argv);
// If there is no command listed, output help.
if (!process.argv.slice(2).length) {
program.outputHelp();
util.shutdown();
}
-51
View File
@@ -1,51 +0,0 @@
const pkg = require('../package.json');
const dotenv = require('dotenv');
const fs = require('fs');
const program = require('commander');
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
const parseArgs = require('minimist')(process.argv.slice(2), {
alias: {
'c': 'config'
},
string: [
'config',
'pid'
],
default: {
'config': null,
'pid': null
}
});
/**
* If the config flag is present, then we have to load the configuration from
* the file specified. We will then load those values into the environment.
*/
if (parseArgs.config) {
let envConfig = dotenv.parse(fs.readFileSync(parseArgs.config, {encoding: 'utf8'}));
Object.keys(envConfig).forEach((k) => {
process.env[k] = envConfig[k];
});
}
/**
* If the pid flag is present, then we have to create a pid file at the location
* specified.
*/
if (parseArgs.pid) {
const util = require('./util');
console.log('Wrote PID');
util.pid(parseArgs.pid);
}
module.exports = program
.version(pkg.version)
.option('-c, --config [path]', 'Specify the configuration file to load')
.option('--pid [path]', 'Specify a path to output the current PID to');
@@ -1,12 +1,12 @@
import React from 'react';
import {CoralLogo} from 'plugin-api/beta/client/components/ui';
import { CoralLogo } from 'plugin-api/beta/client/components/ui';
import styles from './MyPluginComponent.css';
class MyPluginComponent extends React.Component {
render() {
return (
<div className={styles.myPluginContainer}>
<CoralLogo className={styles.logo}/>
<CoralLogo className={styles.logo} />
<div className={styles.description}>
<h3>Plugin created by Talk CLI</h3>
+2 -3
View File
@@ -1,4 +1,3 @@
/**
This is a client index example file and it could look like this:
@@ -21,6 +20,6 @@ import MyPluginComponent from './components/MyPluginComponent';
export default {
slots: {
stream: [MyPluginComponent]
}
stream: [MyPluginComponent],
},
};
+22 -48
View File
@@ -1,7 +1,9 @@
const debug = require('debug')('talk:util');
const fs = require('fs');
// Setup the environment.
require('../services/env');
const util = module.exports = {};
const debug = require('debug')('talk:util');
const util = (module.exports = {});
/**
* Stores an array of functions that should be executed in the event that the
@@ -15,20 +17,22 @@ util.toshutdown = [];
* @param {Number} [defaultCode=0] default return code upon sucesfull shutdown.
*/
util.shutdown = (defaultCode = 0, signal = null) => {
if (signal) {
debug(`Reached ${signal} signal`);
}
debug(`${util.toshutdown.length} jobs now being called`);
Promise
.all(util.toshutdown.map((func) => func ? func(signal) : null).filter((func) => func))
Promise.all(
util.toshutdown
.map(func => (func ? func(signal) : null))
.filter(func => func)
)
.then(() => {
debug('Shutdown complete, now exiting');
process.exit(defaultCode);
})
.catch((err) => {
.catch(err => {
console.error(err);
process.exit(1);
@@ -41,54 +45,24 @@ util.shutdown = (defaultCode = 0, signal = null) => {
* @param {Array} jobs Array of promise capable shutdown functions that are
* executed.
*/
util.onshutdown = (jobs) => {
util.onshutdown = jobs => {
debug(`${jobs.length} jobs registered to be called during shutdown`);
// Add the new jobs to shutdown to the object reference.
util.toshutdown = util.toshutdown.concat(jobs);
};
/**
* Register a PID file to be maintained for the lifespan of the process.
* @param {String} path path to the PID file to create
*/
util.pid = (path) => {
if (!/\//.test(path)) {
if (!/\.pid/.test(path)) {
path += '.pid';
}
path = `/tmp/${path}`;
}
const pid = `${process.pid.toString()}\n`;
fs.writeFile(path, pid, (err) => {
if (err) {
console.error(`Can't write PID file: ${err}`);
throw err;
}
// Add the cleanup for the fs onto the shutdown.
util.onshutdown([
() => new Promise((resolve, reject) => {
// Remove the pid file.
fs.unlink(path, (err) => {
if (err) {
return reject(err);
}
return resolve();
});
})
]);
});
};
// Attach to the SIGTERM + SIGINT handles to ensure a clean shutdown in the
// event that we have an external event. SIGUSR2 is called when the app is asked
// to be 'killed', same procedure here.
process.on('SIGTERM', () => util.shutdown(0, 'SIGTERM'));
process.on('SIGINT', () => util.shutdown(0, 'SIGINT'));
process.on('SIGTERM', () => util.shutdown(0, 'SIGTERM'));
process.on('SIGINT', () => util.shutdown(0, 'SIGINT'));
process.once('SIGUSR2', () => util.shutdown(0, 'SIGUSR2'));
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
console.error(err);
process.exit(1);
});
-193
View File
@@ -1,193 +0,0 @@
const CommentModel = require('../../../models/comment');
const ActionsService = require('../../../services/actions');
const {arrayJoinBy, singleJoinBy} = require('../../../graph/loaders/util');
const sc = require('snake-case');
const debug = require('debug')('talk:cli:verify');
const getBatch = async (limit, offset) => CommentModel
.find({})
.select({'id': 1, 'action_counts': 1, 'reply_count': 1})
.limit(limit)
.skip(offset)
.sort('created_at');
module.exports = async ({fix, limit, batch}) => {
let operations = [];
// Count how many comments there are to process.
const totalCount = await CommentModel.count();
let offset = 0;
let comments = [];
let commentIDs = [];
console.log(`Processing ${totalCount} comments in batches of ${limit}...`);
// Keep processing documents until there are is none left.
while (offset < totalCount) {
// Get a batch of comments.
comments = await getBatch(batch, offset);
commentIDs = comments.map(({id}) => id);
// Get their reply counts.
let allReplyCounts = await CommentModel
.aggregate([
{
$match: {
parent_id: {
$in: commentIDs,
},
status: {
$in: ['NONE', 'ACCEPTED']
}
}
},
{
$group: {
_id: '$parent_id',
count: {
$sum: 1
}
}
}
])
.then(singleJoinBy(commentIDs, '_id'))
.then((results) => results.map((result) => result ? result.count : 0));
// Get their action summaries.
let allActionSummaries = await ActionsService
.getActionSummaries(commentIDs)
.then(arrayJoinBy(commentIDs, 'item_id'));
// Loop over the comments, with their action summaries.
for (let i = 0; i < comments.length; i++) {
let comment = comments[i];
let actionSummaries = allActionSummaries[i];
let replyCount = allReplyCounts[i];
// And check to see if the action summaries we just computed match what is
// currently set for the comments.
let commentOperations = [];
// If the reply count needs to be updated, then update it!
if (comment.reply_count !== replyCount) {
commentOperations.push({
reply_count: replyCount,
});
}
// First we process all the group id's.
for (let actionSummary of actionSummaries) {
if (actionSummary.group_id === null) {
continue;
}
// And we generate the group id.
const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase());
const GROUP_ID = sc(actionSummary.group_id.toLowerCase());
if (GROUP_ID.length <= 0) {
continue;
}
// And we add a new batch operation if the action summary is associated
// with a group.
const ACTION_COUNT_FIELD = `${ACTION_TYPE}_${GROUP_ID}`;
// Check that the action summaries match the cached counts.
if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== actionSummary.count) {
// Batch updates for those changes.
commentOperations.push({
[`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count,
});
}
}
// Group all the action summaries together from all the different group
// ids.
let groupedActionSummaries = actionSummaries.reduce((acc, actionSummary) => {
const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase());
if (!(ACTION_TYPE in acc)) {
acc[ACTION_TYPE] = 0;
}
acc[ACTION_TYPE] += actionSummary.count;
return acc;
}, {});
for (const ACTION_COUNT_FIELD of Object.keys(groupedActionSummaries)) {
const count = groupedActionSummaries[ACTION_COUNT_FIELD];
// Check that the action summaries match the cached counts.
if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== count) {
// Batch updates for those changes.
commentOperations.push({
[`action_counts.${ACTION_COUNT_FIELD}`]: count,
});
}
}
// If this comment has action summaries that should be updated, then
// perform an update!
if (commentOperations.length > 0) {
operations.push({
updateOne: {
filter: {
id: comment.id
},
update: {
$set: Object.assign({}, ...commentOperations),
},
},
});
}
}
debug(`Processed batch of ${comments.length} comments.`);
if (operations.length >= limit) {
debug(`Queued operations are ${operations.length}, reached limit of ${limit}, not processing any more.`);
if (operations.length > limit) {
debug(`${operations.length - limit} operations have been truncated to enforce the limit`);
}
break;
}
offset += batch;
}
const OPERATIONS_LENGTH = operations.length;
if (limit < Infinity && offset + comments.length < totalCount) {
console.log(`Processed ${offset + comments.length}/${totalCount} comments because we reached the update limit of ${limit}.`);
} else {
console.log(`Processed all ${totalCount} comments.`);
}
console.log(`${OPERATIONS_LENGTH} documents need fixing.`);
// If fix was enabled, execute the batch writes.
if (OPERATIONS_LENGTH > 0) {
if (fix) {
debug(`Fixing ${OPERATIONS_LENGTH} documents...`);
while (operations.length) {
let batchOperations = operations.splice(0, batch);
let result = await CommentModel.collection.bulkWrite(batchOperations);
debug(`Fixed batch of ${result.modifiedCount} documents.`);
}
console.log(`Applied all ${OPERATIONS_LENGTH} fixes.`);
} else {
console.warn('Skipping fixing, --fix was not enabled, pass --fix to fix these errors');
}
}
};
-12
View File
@@ -1,12 +0,0 @@
// This will import all the verifications that should be run by the:
//
// cli verify database
//
// command. They exist in the form:
//
// async ({fix = false, batch = 1000}) => {}
//
// where their options are derrived.
module.exports = [
require('./comments'),
];
+16 -6
View File
@@ -7,6 +7,8 @@ machine:
environment:
PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
NODE_ENV: "test"
MOCHA_FILE: "${CIRCLE_TEST_REPORTS}/junit/test-results.xml"
MOCHA_REPORTER: "mocha-junit-reporter"
pre:
# TODO: use the following to add in support for MongoDB 3.4.
# # Upgrade the database version to 3.4.
@@ -17,6 +19,13 @@ machine:
# - sudo apt-get install -y mongodb-org
# - sudo service mongod restart
# Force sync the time.
- date
- sudo service ntp stop
- sudo ntpdate -s time.nist.gov
- sudo service ntp start
- date
# Install chromium for e2e and remove old google-chrome
- sudo rm -rf /opt/google/chrome
- sudo rm -f /usr/bin/google-chrome*
@@ -28,7 +37,7 @@ dependencies:
# Install node dependencies.
- yarn --version
- yarn global add node-gyp nsp --force
- yarn global add node-gyp --force
- yarn
post:
@@ -47,10 +56,9 @@ database:
test:
override:
# Run the tests using the junit reporter.
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test
# Check dependancies using nsp.
- nsp check
- yarn e2e-ci
- yarn test
# Run the end to end tests
- yarn e2e:ci
deployment:
release:
@@ -59,7 +67,9 @@ deployment:
- bash ./scripts/docker.sh deploy
latest:
branch: master
branch:
- master
- next
owner: coralproject
commands:
- bash ./scripts/docker.sh deploy
+18 -18
View File
@@ -1,44 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
import { Router, Route, IndexRedirect, IndexRoute } from 'react-router';
import Configure from 'routes/Configure';
import Install from 'routes/Install';
import Stories from 'routes/Stories';
import Community from 'routes/Community/containers/Community';
import {ModerationLayout, Moderation} from 'routes/Moderation';
import Community from 'routes/Community';
import { ModerationLayout, Moderation } from 'routes/Moderation';
import Layout from 'containers/Layout';
const routes = (
<div>
<Route exact path="/admin/install" component={Install}/>
<Route path='/admin' component={Layout}>
<IndexRedirect to='/admin/moderate' />
<Route path='configure' component={Configure} />
<Route path='stories' component={Stories} />
<Route exact path="/admin/install" component={Install} />
<Route path="/admin" component={Layout}>
<IndexRedirect to="/admin/moderate" />
<Route path="configure" component={Configure} />
<Route path="stories" component={Stories} />
{/* Community Routes */}
<Route path='community'>
<Route path='flagged' components={Community}>
<Route path=':id' components={Community} />
<Route path="community">
<Route path="flagged" components={Community}>
<Route path=":id" components={Community} />
</Route>
<Route path='people' components={Community}>
<Route path=':id' components={Community} />
<Route path="people" components={Community}>
<Route path=":id" components={Community} />
</Route>
<IndexRedirect to='flagged' />
<IndexRedirect to="flagged" />
</Route>
{/* Moderation Routes */}
<Route path='moderate' component={ModerationLayout}>
<Route path="moderate" component={ModerationLayout}>
<IndexRoute components={Moderation} />
<Route path=':tabOrId' components={Moderation} />
<Route path=":tabOrId" components={Moderation} />
<Route path=':tab' components={Moderation}>
<Route path=':id' components={Moderation} />
<Route path=":tab" components={Moderation}>
<Route path=":id" components={Moderation} />
</Route>
</Route>
</Route>
+46 -34
View File
@@ -7,29 +7,33 @@ import jwtDecode from 'jwt-decode';
// SIGN IN
//==============================================================================
export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client, storage}) => {
dispatch({type: actions.LOGIN_REQUEST});
export const handleLogin = (email, password, recaptchaResponse) => (
dispatch,
_,
{ rest, client, storage }
) => {
dispatch({ type: actions.LOGIN_REQUEST });
const params = {
method: 'POST',
body: {
email,
password
}
password,
},
};
if (recaptchaResponse) {
params.headers = {
'X-Recaptcha-Response': recaptchaResponse
'X-Recaptcha-Response': recaptchaResponse,
};
}
return rest('/auth/local', params)
.then(({user, token}) => {
.then(({ user, token }) => {
if (!user) {
if (!bowser.safari && !bowser.ios && storage) {
storage.removeItem('token');
storage.removeItem('exp');
}
return dispatch(checkLoginFailure('not logged in'));
}
@@ -38,19 +42,19 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _,
client.resetWebsocket();
dispatch(checkLoginSuccess(user));
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
if (error.translation_key === 'NOT_AUTHORIZED') {
// invalid credentials
dispatch({
type: actions.LOGIN_FAILURE,
message: t('error.email_password')
message: t('error.email_password'),
});
}
else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
} else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
dispatch({
type: actions.LOGIN_MAXIMUM_EXCEEDED,
message: t(`error.${error.translation_key}`),
@@ -69,27 +73,32 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _,
//==============================================================================
const forgotPasswordRequest = () => ({
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
type: actions.FETCH_FORGOT_PASSWORD_REQUEST,
});
const forgotPasswordSuccess = () => ({
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS,
});
const forgotPasswordFailure = (error) => ({
const forgotPasswordFailure = error => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
error,
});
export const requestPasswordReset = (email) => (dispatch, _, {rest}) => {
export const requestPasswordReset = email => (dispatch, _, { rest }) => {
dispatch(forgotPasswordRequest(email));
const redirectUri = location.href;
return rest('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
return rest('/account/password/reset', {
method: 'POST',
body: { email, loc: redirectUri },
})
.then(() => dispatch(forgotPasswordSuccess()))
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(forgotPasswordFailure(errorMessage));
});
};
@@ -99,27 +108,28 @@ export const requestPasswordReset = (email) => (dispatch, _, {rest}) => {
//==============================================================================
const checkLoginRequest = () => ({
type: actions.CHECK_LOGIN_REQUEST
type: actions.CHECK_LOGIN_REQUEST,
});
const checkLoginSuccess = (user, isAdmin) => ({
type: actions.CHECK_LOGIN_SUCCESS,
user,
isAdmin
isAdmin,
});
const checkLoginFailure = (error) => ({
const checkLoginFailure = error => ({
type: actions.CHECK_LOGIN_FAILURE,
error
error,
});
export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
export const checkLogin = () => (dispatch, _, { rest, client, storage }) => {
dispatch(checkLoginRequest());
return rest('/auth')
.then(({user}) => {
.then(({ user }) => {
if (!user) {
if (!bowser.safari && !bowser.ios && storage) {
storage.removeItem('token');
storage.removeItem('exp');
}
return dispatch(checkLoginFailure('not logged in'));
}
@@ -127,9 +137,11 @@ export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
client.resetWebsocket();
dispatch(checkLoginSuccess(user));
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(checkLoginFailure(errorMessage));
});
};
@@ -138,16 +150,17 @@ export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
// LOGOUT
//==============================================================================
export const logout = () => (dispatch, _, {rest, client, storage}) => {
return rest('/auth', {method: 'DELETE'}).then(() => {
export const logout = () => (dispatch, _, { rest, client, storage }) => {
return rest('/auth', { method: 'DELETE' }).then(() => {
if (storage) {
storage.removeItem('token');
storage.removeItem('exp');
}
// Reset the websocket.
client.resetWebsocket();
dispatch({type: actions.LOGOUT});
dispatch({ type: actions.LOGOUT });
});
};
@@ -155,11 +168,10 @@ export const logout = () => (dispatch, _, {rest, client, storage}) => {
// AUTH TOKEN
//==============================================================================
export const handleAuthToken = (token) => (dispatch, _, {storage}) => {
export const handleAuthToken = token => (dispatch, _, { storage }) => {
if (storage) {
storage.setItem('exp', jwtDecode(token).exp);
storage.setItem('token', token);
}
dispatch({type: 'HANDLE_AUTH_TOKEN'});
dispatch({ type: 'HANDLE_AUTH_TOKEN' });
};
@@ -1,7 +1,19 @@
import {SHOW_BAN_USER_DIALOG, HIDE_BAN_USER_DIALOG} from '../constants/banUserDialog';
import {
SHOW_BAN_USER_DIALOG,
HIDE_BAN_USER_DIALOG,
} from '../constants/banUserDialog';
export const showBanUserDialog = ({userId, username, commentId, commentStatus}) =>
({type: SHOW_BAN_USER_DIALOG, userId, username, commentId, commentStatus});
export const hideBanUserDialog = () => ({type: HIDE_BAN_USER_DIALOG});
export const showBanUserDialog = ({
userId,
username,
commentId,
commentStatus,
}) => ({
type: SHOW_BAN_USER_DIALOG,
userId,
username,
commentId,
commentStatus,
});
export const hideBanUserDialog = () => ({ type: HIDE_BAN_USER_DIALOG });
+30 -32
View File
@@ -7,73 +7,71 @@ import {
SORT_UPDATE,
SET_PAGE,
SET_SEARCH_VALUE,
SET_ROLE,
SET_COMMENTER_STATUS,
SHOW_BANUSER_DIALOG,
HIDE_BANUSER_DIALOG,
SHOW_REJECT_USERNAME_DIALOG,
HIDE_REJECT_USERNAME_DIALOG
HIDE_REJECT_USERNAME_DIALOG,
SET_INDICATOR_TRACK,
} from '../constants/community';
import t from 'coral-framework/services/i18n';
export const fetchUsers = (query = {}) => (dispatch, _, {rest}) => {
export const fetchUsers = (query = {}) => (dispatch, _, { rest }) => {
dispatch(requestFetchUsers());
rest(`/users?${queryString.stringify(query)}`)
.then(({result, page, count, limit, totalPages}) =>{
.then(({ result, page, count, limit, totalPages }) => {
dispatch({
type: FETCH_USERS_SUCCESS,
users: result,
page,
count,
limit,
totalPages
totalPages,
});
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: FETCH_USERS_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: FETCH_USERS_FAILURE, error: errorMessage });
});
};
const requestFetchUsers = () => ({
type: FETCH_USERS_REQUEST
type: FETCH_USERS_REQUEST,
});
export const updateSorting = (sort) => ({
export const updateSorting = sort => ({
type: SORT_UPDATE,
sort
sort,
});
export const setPage = (page) => ({
export const setPage = page => ({
type: SET_PAGE,
page,
});
export const setSearchValue = (value) => ({
export const setSearchValue = value => ({
type: SET_SEARCH_VALUE,
value,
});
export const setRole = (id, role) => (dispatch, _, {rest}) => {
return rest(`/users/${id}/role`, {method: 'POST', body: {role}})
.then(() => {
return dispatch({type: SET_ROLE, id, role});
});
};
export const setCommenterStatus = (id, status) => (dispatch, _, {rest}) => {
return rest(`/users/${id}/status`, {method: 'POST', body: {status}})
.then(() => {
return dispatch({type: SET_COMMENTER_STATUS, id, status});
});
};
// Ban User Dialog
export const showBanUserDialog = (user) => ({type: SHOW_BANUSER_DIALOG, user});
export const hideBanUserDialog = () => ({type: HIDE_BANUSER_DIALOG});
export const showBanUserDialog = user => ({ type: SHOW_BANUSER_DIALOG, user });
export const hideBanUserDialog = () => ({ type: HIDE_BANUSER_DIALOG });
// Reject Username Dialog
export const showRejectUsernameDialog = (user) => ({type: SHOW_REJECT_USERNAME_DIALOG, user});
export const hideRejectUsernameDialog = () => ({type: HIDE_REJECT_USERNAME_DIALOG});
export const showRejectUsernameDialog = user => ({
type: SHOW_REJECT_USERNAME_DIALOG,
user,
});
export const hideRejectUsernameDialog = () => ({
type: HIDE_REJECT_USERNAME_DIALOG,
});
// Enable or disable the activity indicator subscriptions.
export const setIndicatorTrack = track => ({
type: SET_INDICATOR_TRACK,
track,
});
+2 -2
View File
@@ -1,7 +1,7 @@
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
export const fetchConfig = () => (dispatch) => {
export const fetchConfig = () => dispatch => {
let json = document.getElementById('data');
let data = JSON.parse(json.textContent);
dispatch({type: CONFIG_UPDATED, data});
dispatch({ type: CONFIG_UPDATED, data });
};
+5 -5
View File
@@ -1,13 +1,13 @@
import * as actions from 'constants/configure';
export const updatePending = ({updater, errorUpdater}) => {
return {type: actions.UPDATE_PENDING, updater, errorUpdater};
export const updatePending = ({ updater, errorUpdater }) => {
return { type: actions.UPDATE_PENDING, updater, errorUpdater };
};
export const clearPending = () => {
return {type: actions.CLEAR_PENDING};
return { type: actions.CLEAR_PENDING };
};
export const setActiveSection = (section) => {
return {type: actions.SET_ACTIVE_SECTION, section};
export const setActiveSection = section => {
return { type: actions.SET_ACTIVE_SECTION, section };
};
+77 -60
View File
@@ -3,17 +3,17 @@ import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
import t from 'coral-framework/services/i18n';
export const nextStep = () => ({type: actions.NEXT_STEP});
export const previousStep = () => ({type: actions.PREVIOUS_STEP});
export const goToStep = (step) => ({type: actions.GO_TO_STEP, step});
export const nextStep = () => ({ type: actions.NEXT_STEP });
export const previousStep = () => ({ type: actions.PREVIOUS_STEP });
export const goToStep = step => ({ type: actions.GO_TO_STEP, step });
const installRequest = () => ({type: actions.INSTALL_REQUEST});
const installSuccess = () => ({type: actions.INSTALL_SUCCESS});
const installFailure = (error) => ({type: actions.INSTALL_FAILURE, error});
const installRequest = () => ({ type: actions.INSTALL_REQUEST });
const installSuccess = () => ({ type: actions.INSTALL_SUCCESS });
const installFailure = error => ({ type: actions.INSTALL_FAILURE, error });
const addError = (name, error) => ({type: actions.ADD_ERROR, name, error});
const hasError = (error) => ({type: actions.HAS_ERROR, error});
const clearErrors = () => ({type: actions.CLEAR_ERRORS});
const addError = (name, error) => ({ type: actions.ADD_ERROR, name, error });
const hasError = error => ({ type: actions.HAS_ERROR, error });
const clearErrors = () => ({ type: actions.CLEAR_ERRORS });
const validation = (formData, dispatch, next) => {
if (!(formData != null)) {
@@ -21,24 +21,21 @@ const validation = (formData, dispatch, next) => {
return;
}
const validKeys = Object.keys(formData)
.filter((name) => name !== 'domains');
const validKeys = Object.keys(formData).filter(name => name !== 'domains');
// Required Validation
const empty = validKeys
.filter((name) => {
const cond = !formData[name].length;
const empty = validKeys.filter(name => {
const cond = !formData[name].length;
if (cond) {
if (cond) {
// Adding Error
dispatch(addError(name, 'This field is required.'));
} else {
dispatch(addError(name, ''));
}
// Adding Error
dispatch(addError(name, 'This field is required.'));
} else {
dispatch(addError(name, ''));
}
return cond;
});
return cond;
});
if (empty.length) {
dispatch(hasError());
@@ -46,19 +43,17 @@ const validation = (formData, dispatch, next) => {
}
// RegExp Validation
const validation = validKeys
.filter((name) => {
const cond = !validate[name](formData[name]);
if (cond) {
const validation = validKeys.filter(name => {
const cond = !validate[name](formData[name]);
if (cond) {
// Adding Error
dispatch(addError(name, errorMsj[name]));
} else {
dispatch(addError(name, ''));
}
dispatch(addError(name, errorMsj[name]));
} else {
dispatch(addError(name, ''));
}
return cond;
});
return cond;
});
if (validation.length) {
dispatch(hasError());
@@ -67,20 +62,21 @@ const validation = (formData, dispatch, next) => {
// Confirm Validation
const prefixLength = 'confirm'.length;
const confirm = validKeys
.filter((name) => {
if (!name.startsWith('confirm')) {
return false;
}
const confirm = validKeys.filter(name => {
if (!name.startsWith('confirm')) {
return false;
}
// Check that 'confirmX' equals 'X'.
const other = name.substr(prefixLength, 1).toLowerCase() + name.substr(prefixLength + 1);
const cond = formData[other] !== formData[name];
if (cond) {
dispatch(addError(name, errorMsj[name]));
}
return cond;
});
// Check that 'confirmX' equals 'X'.
const other =
name.substr(prefixLength, 1).toLowerCase() +
name.substr(prefixLength + 1);
const cond = formData[other] !== formData[name];
if (cond) {
dispatch(addError(name, errorMsj[name]));
}
return cond;
});
if (confirm.length) {
dispatch(hasError());
@@ -105,41 +101,62 @@ export const submitUser = () => (dispatch, getState) => {
});
};
export const finishInstall = () => (dispatch, getState, {rest}) => {
export const finishInstall = () => (dispatch, getState, { rest }) => {
const data = getState().install.data;
dispatch(installRequest());
return rest('/setup', {method: 'POST', body: data})
return rest('/setup', { method: 'POST', body: data })
.then(() => {
dispatch(installSuccess());
dispatch(nextStep());
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(installFailure(errorMessage));
});
};
export const updateSettingsFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_SETTINGS, name, value});
export const updateUserFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_USER, name, value});
export const updatePermittedDomains = (value) => ({type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS, value});
export const updateSettingsFormData = (name, value) => ({
type: actions.UPDATE_FORMDATA_SETTINGS,
name,
value,
});
export const updateUserFormData = (name, value) => ({
type: actions.UPDATE_FORMDATA_USER,
name,
value,
});
export const updatePermittedDomains = value => ({
type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS,
value,
});
const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error});
const checkInstallRequest = () => ({ type: actions.CHECK_INSTALL_REQUEST });
const checkInstallSuccess = installed => ({
type: actions.CHECK_INSTALL_SUCCESS,
installed,
});
const checkInstallFailure = error => ({
type: actions.CHECK_INSTALL_FAILURE,
error,
});
export const checkInstall = (next) => async (dispatch, _, {rest}) => {
export const checkInstall = next => async (dispatch, _, { rest }) => {
dispatch(checkInstallRequest());
try {
const {installed} = await rest('/setup');
const { installed } = await rest('/setup');
dispatch(checkInstallSuccess(installed));
if (installed) {
next();
}
} catch (error) {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(checkInstallFailure(errorMessage));
}
};
+19 -14
View File
@@ -1,41 +1,46 @@
import * as actions from 'constants/moderation';
export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open});
export const singleView = () => ({type: actions.SINGLE_VIEW});
export const toggleModal = open => ({ type: actions.TOGGLE_MODAL, open });
export const singleView = () => ({ type: actions.SINGLE_VIEW });
// hide shortcuts note
export const hideShortcutsNote = () => (dispatch, _, {storage}) => {
export const hideShortcutsNote = () => (dispatch, _, { storage }) => {
try {
if (storage) {
storage.setItem('coral:shortcutsNote', 'hide');
}
} catch (e) {
// above will fail in Safari private mode
}
dispatch({type: actions.HIDE_SHORTCUTS_NOTE});
dispatch({ type: actions.HIDE_SHORTCUTS_NOTE });
};
export const setSortOrder = (order) => ({
export const setSortOrder = order => ({
type: actions.SET_SORT_ORDER,
order
order,
});
export const toggleStorySearch = (active) => ({
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH
export const toggleStorySearch = active => ({
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH,
});
export const storySearchChange = (value) => ({
export const storySearchChange = value => ({
type: actions.STORY_SEARCH_CHANGE_VALUE,
value
value,
});
export const clearState = () => ({
type: actions.MODERATION_CLEAR_STATE
type: actions.CLEAR_STATE,
});
export const selectCommentId = (id) => ({
type: actions.MODERATION_SELECT_COMMENT,
export const selectCommentId = id => ({
type: actions.SELECT_COMMENT,
id,
});
// Enable or disable the activity indicator subscriptions.
export const setIndicatorTrack = track => ({
type: actions.SET_INDICATOR_TRACK,
track,
});
+27 -22
View File
@@ -10,7 +10,7 @@ import {
UPDATE_ASSET_STATE_REQUEST,
UPDATE_ASSET_STATE_SUCCESS,
UPDATE_ASSET_STATE_FAILURE,
UPDATE_ASSETS
UPDATE_ASSETS,
} from '../constants/stories';
import t from 'coral-framework/services/i18n';
@@ -21,53 +21,58 @@ import t from 'coral-framework/services/i18n';
// Fetch a page of assets
// Get comments to fill each of the three lists on the mod queue
export const fetchAssets = (query = {}) => (dispatch, _, {rest}) => {
dispatch({type: FETCH_ASSETS_REQUEST});
export const fetchAssets = (query = {}) => (dispatch, _, { rest }) => {
dispatch({ type: FETCH_ASSETS_REQUEST });
return rest(`/assets?${queryString.stringify(query)}`)
.then(({result, page, count, limit, totalPages}) =>
dispatch({type: FETCH_ASSETS_SUCCESS,
.then(({ result, page, count, limit, totalPages }) =>
dispatch({
type: FETCH_ASSETS_SUCCESS,
assets: result,
page,
count,
limit,
totalPages,
}))
.catch((error) => {
})
)
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: FETCH_ASSETS_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: FETCH_ASSETS_FAILURE, error: errorMessage });
});
};
// Update an asset state
// Get comments to fill each of the three lists on the mod queue
export const updateAssetState = (id, closedAt) => (dispatch, _, {rest}) => {
dispatch({type: UPDATE_ASSET_STATE_REQUEST, id, closedAt});
return rest(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
.then(() => dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
.catch((error) => {
export const updateAssetState = (id, closedAt) => (dispatch, _, { rest }) => {
dispatch({ type: UPDATE_ASSET_STATE_REQUEST, id, closedAt });
return rest(`/assets/${id}/status`, { method: 'PUT', body: { closedAt } })
.then(() => dispatch({ type: UPDATE_ASSET_STATE_SUCCESS }))
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: UPDATE_ASSET_STATE_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: UPDATE_ASSET_STATE_FAILURE, error: errorMessage });
});
};
export const updateAssets = (assets) => (dispatch) => {
dispatch({type: UPDATE_ASSETS, assets});
export const updateAssets = assets => dispatch => {
dispatch({ type: UPDATE_ASSETS, assets });
};
export const setPage = (page) => ({
export const setPage = page => ({
type: SET_PAGE,
page,
});
export const setSearchValue = (value) => ({
export const setSearchValue = value => ({
type: SET_SEARCH_VALUE,
value,
});
export const setCriteria = (criteria) => ({
export const setCriteria = criteria => ({
type: SET_CRITERIA,
criteria,
});
@@ -1,7 +1,19 @@
import {SHOW_SUSPEND_USER_DIALOG, HIDE_SUSPEND_USER_DIALOG} from '../constants/suspendUserDialog.js';
import {
SHOW_SUSPEND_USER_DIALOG,
HIDE_SUSPEND_USER_DIALOG,
} from '../constants/suspendUserDialog.js';
export const showSuspendUserDialog = ({userId, username, commentId, commentStatus}) =>
({type: SHOW_SUSPEND_USER_DIALOG, userId, username, commentId, commentStatus});
export const hideSuspendUserDialog = () => ({type: HIDE_SUSPEND_USER_DIALOG});
export const showSuspendUserDialog = ({
userId,
username,
commentId,
commentStatus,
}) => ({
type: SHOW_SUSPEND_USER_DIALOG,
userId,
username,
commentId,
commentStatus,
});
export const hideSuspendUserDialog = () => ({ type: HIDE_SUSPEND_USER_DIALOG });
+18 -9
View File
@@ -1,28 +1,37 @@
import * as actions from 'constants/userDetail';
export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, userId});
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
export const viewUserDetail = userId => ({
type: actions.VIEW_USER_DETAIL,
userId,
});
export const hideUserDetail = () => ({ type: actions.HIDE_USER_DETAIL });
export const changeUserDetailStatuses = (tab) => {
export const changeTab = tab => {
let statuses = null;
if (tab === 'rejected') {
statuses = ['REJECTED'];
}
return {type: actions.CHANGE_USER_DETAIL_STATUSES, tab, statuses};
return { type: actions.CHANGE_TAB_USER_DETAIL, tab, statuses };
};
export const clearUserDetailSelections = () => ({type: actions.CLEAR_USER_DETAIL_SELECTIONS});
export const clearUserDetailSelections = () => ({
type: actions.CLEAR_USER_DETAIL_SELECTIONS,
});
export const toggleSelectCommentInUserDetail = (id, active) => {
return {
type: active ? actions.SELECT_USER_DETAIL_COMMENT : actions.UNSELECT_USER_DETAIL_COMMENT,
id
type: active
? actions.SELECT_USER_DETAIL_COMMENT
: actions.UNSELECT_USER_DETAIL_COMMENT,
id,
};
};
export const toggleSelectAllCommentInUserDetail = (ids, active) => {
return {
type: active ? actions.SELECT_ALL_USER_DETAIL_COMMENT : actions.CLEAR_USER_DETAIL_SELECTIONS,
ids
type: active
? actions.SELECT_ALL_USER_DETAIL_COMMENT
: actions.CLEAR_USER_DETAIL_SELECTIONS,
ids,
};
};
+39 -21
View File
@@ -6,38 +6,56 @@ import t from 'coral-framework/services/i18n';
*/
// change status of a user
export const userStatusUpdate = (status, userId, commentId) => {
return (dispatch, _, {rest}) => {
dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
return rest(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
.then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
.catch((error) => {
return (dispatch, _, { rest }) => {
dispatch({ type: userTypes.UPDATE_STATUS_REQUEST });
return rest(`/users/${userId}/status`, {
method: 'POST',
body: { status: status, comment_id: commentId },
})
.then(res => dispatch({ type: userTypes.UPDATE_STATUS_SUCCESS, res }))
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({
type: userTypes.UPDATE_STATUS_FAILURE,
error: errorMessage,
});
});
};
};
// change status of a user
export const sendNotificationEmail = (userId, subject, body) => {
return (dispatch, _, {rest}) => {
return rest(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
.catch((error) => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: userTypes.USER_EMAIL_FAILURE, error: errorMessage});
});
return (dispatch, _, { rest }) => {
return rest(`/users/${userId}/email`, {
method: 'POST',
body: { subject, body },
}).catch(error => {
console.error(error);
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: userTypes.USER_EMAIL_FAILURE, error: errorMessage });
});
};
};
// let a user edit their username
export const enableUsernameEdit = (userId) => {
return (dispatch, _, {rest}) => {
return rest(`/users/${userId}/username-enable`, {method: 'POST'})
.catch((error) => {
export const enableUsernameEdit = userId => {
return (dispatch, _, { rest }) => {
return rest(`/users/${userId}/username-enable`, { method: 'POST' }).catch(
error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error: errorMessage});
});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({
type: userTypes.USERNAME_ENABLE_FAILURE,
error: errorMessage,
});
}
);
};
};
@@ -0,0 +1,30 @@
.table {
flex-direction: column;
}
.table, .headerRow, .row {
display: flex;
}
.headerRowItem, .item {
flex: 1;
padding: 20px;
&:nth-child(2) {
flex: 2;
}
}
.headerRowItem {
color: #595959;
font-weight: bold;
}
.headerRow, .row {
border-bottom: 1px solid #e0e0e0;
}
.action {
color: black;
font-weight: bold;
}
@@ -0,0 +1,103 @@
import React from 'react';
import PropTypes from 'prop-types';
import { murmur3 } from 'murmurhash-js';
import styles from './AccountHistory.css';
import cn from 'classnames';
import flatten from 'lodash/flatten';
import orderBy from 'lodash/orderBy';
import moment from 'moment';
const buildUserHistory = (userState = {}) => {
return orderBy(
flatten(
Object.keys(userState.status)
.filter(k => k !== '__typename')
.map(k => userState.status[k].history)
),
'created_at',
'desc'
);
};
const buildActionResponse = (typename, until, status) => {
switch (typename) {
case 'UsernameStatusHistory':
return `Username ${status}`;
case 'BannedStatusHistory':
return status ? 'User banned' : 'Ban removed';
case 'SuspensionStatusHistory':
return until ? 'Account Suspended' : 'Suspension removed';
default:
return '-';
}
};
const getModerationValue = (userId, assignedBy = {}) => {
if (assignedBy && userId !== assignedBy.id) {
return assignedBy.username;
}
return 'SYSTEM';
};
class AccountHistory extends React.Component {
render() {
const { user } = this.props;
const userHistory = buildUserHistory(user.state);
return (
<div>
<div className={cn(styles.table, 'talk-admin-account-history')}>
<div
className={cn(
styles.headerRow,
'talk-admin-account-history-header-row'
)}
>
<div className={styles.headerRowItem}>Date</div>
<div className={styles.headerRowItem}>Action</div>
<div className={styles.headerRowItem}>Moderation</div>
</div>
{userHistory.map(
({ __typename, created_at, assigned_by, until, status }) => (
<div
className={cn(styles.row, 'talk-admin-account-history-row')}
key={`${__typename}_${murmur3(created_at)}`}
>
<div
className={cn(
styles.item,
'talk-admin-account-history-row-date'
)}
>
{moment(new Date(created_at)).format('MMM DD, YYYY')}
</div>
<div
className={cn(
styles.item,
styles.action,
'talk-admin-account-history-row-status'
)}
>
{buildActionResponse(__typename, until, status)}
</div>
<div
className={cn(
styles.item,
'talk-admin-account-history-row-assigned-by'
)}
>
{getModerationValue(user.id, assigned_by)}
</div>
</div>
)
)}
</div>
</div>
);
}
}
AccountHistory.propTypes = {
user: PropTypes.object.isRequired,
};
export default AccountHistory;
@@ -8,9 +8,6 @@
color: black;
> :global(.mdl-menu__container) {
margin-left: 10px;
> :global(.mdl-menu__outline) {
box-shadow: none;
}
}
}
@@ -18,12 +15,13 @@
box-shadow: none;
color: white;
background-color: #616161;
border-color: #616161;
}
.arrowIcon {
margin-left: 6px;
margin-right: 0;
vertical-align: middle;
vertical-align: middle;
margin-right: 0;
font-size: 14px;
}
@@ -33,8 +31,10 @@
}
.menuItem {
background-color: #2a2a2a;
color: white;
color: #2a2a2a;
background-color: white;
font-size: 0.95em;
&:first-child {
margin-bottom: 1px;
border-radius: 2px 2px 0px 0px;
@@ -43,7 +43,8 @@
border-radius: 0px 0px 2px 2px;
}
&:hover, &:active, &:focus {
background-color: #767676;
background-color: #e2e2e2;
border-color: #616161;
}
&[disabled], &[disabled]:hover, &[disabled]:focus, &[disabled]:active {
background-color: #262626;
@@ -1,9 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button, Icon} from 'coral-ui';
import {Menu} from 'react-mdl';
import { Button, Icon } from 'coral-ui';
import ClickOutside from 'coral-framework/components/ClickOutside';
import { Menu } from 'react-mdl';
import cn from 'classnames';
import {findDOMNode} from 'react-dom';
import { findDOMNode } from 'react-dom';
import styles from './ActionsMenu.css';
import t from 'coral-framework/services/i18n';
@@ -13,46 +14,60 @@ let count = 0;
class ActionsMenu extends React.Component {
id = `actions-dropdown-${count++}`;
menu = null;
state = {open: false};
state = { open: false };
timeout = null;
componentWillUnmount() {
clearTimeout(this.timeout);
}
handleRef = (ref) => {
handleRef = ref => {
this.menu = ref ? findDOMNode(ref).parentNode : null;
}
};
syncOpenState = () => {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.setState({open: this.menu.className.indexOf('is-visible') >= 0});
this.setState({ open: this.menu.className.indexOf('is-visible') >= 0 });
}, 150);
};
render() {
const {className = ''} = this.props;
const { className = '', buttonClassNames = '', label = '' } = this.props;
return (
<div className={cn(styles.root, className)} onBlur={this.syncOpenState} >
<Button
cStyle='actions'
className={cn(styles.button, {[styles.buttonOpen]: this.state.open})}
disabled={false}
id={this.id}
<ClickOutside onClickOutside={this.syncOpenState}>
<div
className={cn(styles.root, className)}
onBlur={this.syncOpenState}
onClick={this.syncOpenState}
icon={this.props.icon}
raised>
{t('modqueue.actions')}
<Icon
name={this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
className={styles.arrowIcon}
/>
</Button>
<Menu target={this.id} className={styles.menu} ref={this.handleRef}>
{this.props.children}
</Menu>
</div>
onKeyUp={this.syncOpenState}
>
<Button
cStyle="actions"
className={cn(
styles.button,
{ [styles.buttonOpen]: this.state.open },
buttonClassNames
)}
disabled={false}
id={this.id}
onClick={this.syncOpenState}
icon={this.props.icon}
raised
>
{label ? label : t('modqueue.actions')}
<Icon
name={
this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'
}
className={styles.arrowIcon}
/>
</Button>
<Menu target={this.id} className={styles.menu} ref={this.handleRef}>
{this.props.children}
</Menu>
</div>
</ClickOutside>
);
}
}
@@ -61,6 +76,8 @@ ActionsMenu.propTypes = {
icon: PropTypes.string,
children: PropTypes.node,
className: PropTypes.string,
label: PropTypes.string,
buttonClassNames: PropTypes.string,
};
export default ActionsMenu;
@@ -1,13 +1,18 @@
import React from 'react';
import cn from 'classnames';
import {MenuItem} from 'react-mdl';
import { MenuItem } from 'react-mdl';
import PropTypes from 'prop-types';
import styles from './ActionsMenu.css';
import camelCase from 'lodash/camelCase';
const ActionsMenuItem = (props) =>
<MenuItem className={cn(styles.menuItem, props.className, 'action-menu-item')} {...props} id={camelCase(props.children)}/>;
const ActionsMenuItem = props => (
<MenuItem
className={cn(styles.menuItem, props.className, 'action-menu-item')}
{...props}
id={camelCase(props.children)}
/>
);
ActionsMenuItem.propTypes = {
className: PropTypes.string,
children: PropTypes.string,
+81 -55
View File
@@ -1,103 +1,129 @@
import React from 'react';
import PropTypes from 'prop-types';
import Layout from 'coral-admin/src/components/ui/Layout';
import Layout from 'coral-admin/src/components/Layout';
import styles from './NotFound.css';
import {Button, TextField, Alert, Success} from 'coral-ui';
import { Button, TextField, Alert, Success } from 'coral-ui';
import Recaptcha from 'react-recaptcha';
import cn from 'classnames';
class AdminLogin extends React.Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {email: '', password: '', requestPassword: false};
this.state = { email: '', password: '', requestPassword: false };
}
handleSignIn = (e) => {
handleSignIn = e => {
e.preventDefault();
this.props.handleLogin(this.state.email, this.state.password);
}
};
onRecaptchaLoad = () => {
// do something?
}
};
onRecaptchaVerify = (recaptchaResponse) => {
this.props.handleLogin(this.state.email, this.state.password, recaptchaResponse);
}
onRecaptchaVerify = recaptchaResponse => {
this.props.handleLogin(
this.state.email,
this.state.password,
recaptchaResponse
);
};
handleRequestPassword = (e) => {
handleRequestPassword = e => {
e.preventDefault();
this.props.requestPasswordReset(this.state.email);
}
};
render () {
const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props;
render() {
const { errorMessage, loginMaxExceeded, recaptchaPublic } = this.props;
const signInForm = (
<form className="talk-admin-login-sign-in" onSubmit={this.handleSignIn}>
{errorMessage && <Alert>{errorMessage}</Alert>}
<TextField
id="email"
label='Email Address'
label="Email Address"
value={this.state.email}
onChange={(e) => this.setState({email: e.target.value})} />
onChange={e => this.setState({ email: e.target.value })}
/>
<TextField
id="password"
label='Password'
label="Password"
value={this.state.password}
onChange={(e) => this.setState({password: e.target.value})}
type='password' />
<div style={{height: 10}}></div>
onChange={e => this.setState({ password: e.target.value })}
type="password"
/>
<div style={{ height: 10 }} />
<Button
className="talk-admin-login-sign-in-button"
type='submit'
cStyle='black'
type="submit"
cStyle="black"
full
onClick={this.handleSignIn}>Sign In</Button>
onClick={this.handleSignIn}
>
Sign In
</Button>
<p className={styles.forgotPasswordCTA}>
Forgot your password? <a href="#" className={styles.forgotPasswordLink} onClick={(e) => {
e.preventDefault();
this.setState({requestPassword: true});
}}>Request a new one.</a>
Forgot your password?{' '}
<a
href="#"
className={styles.forgotPasswordLink}
onClick={e => {
e.preventDefault();
this.setState({ requestPassword: true });
}}
>
Request a new one.
</a>
</p>
{
loginMaxExceeded &&
{loginMaxExceeded && (
<Recaptcha
sitekey={recaptchaPublic}
render='explicit'
theme='dark'
render="explicit"
theme="dark"
onloadCallback={this.onRecaptchaLoad}
verifyCallback={this.onRecaptchaVerify} />
}
verifyCallback={this.onRecaptchaVerify}
/>
)}
</form>
);
const requestPasswordForm = (
this.props.passwordRequestSuccess
? <p className={styles.passwordRequestSuccess} onClick={() => {
const requestPasswordForm = this.props.passwordRequestSuccess ? (
<p
className={styles.passwordRequestSuccess}
onClick={() => {
location.href = location.href;
}}>
{this.props.passwordRequestSuccess} <a className={styles.signInLink} href="#">Sign in</a>
<Success />
</p>
: <form onSubmit={this.handleRequestPassword}>
<TextField
label='Email Address'
value={this.state.email}
onChange={(e) => this.setState({email: e.target.value})} />
<Button
type='submit'
cStyle='black'
full
onClick={this.handleRequestPassword}>Reset Password</Button>
</form>
}}
>
{this.props.passwordRequestSuccess}{' '}
<a className={styles.signInLink} href="#">
Sign in
</a>
<Success />
</p>
) : (
<form onSubmit={this.handleRequestPassword}>
<TextField
label="Email Address"
value={this.state.email}
onChange={e => this.setState({ email: e.target.value })}
/>
<Button
type="submit"
cStyle="black"
full
onClick={this.handleRequestPassword}
>
Reset Password
</Button>
</form>
);
return (
<Layout fixedDrawer restricted={true}>
<div className={cn(styles.loginLayout, 'talk-admin-login')}>
<h1 className={styles.loginHeader}>Team sign in</h1>
<p className={styles.loginCTA}>Sign in to interact with your community.</p>
{ this.state.requestPassword ? requestPasswordForm : signInForm }
<p className={styles.loginCTA}>
Sign in to interact with your community.
</p>
{this.state.requestPassword ? requestPasswordForm : signInForm}
</div>
</Layout>
);
+1 -1
View File
@@ -5,7 +5,7 @@ import 'material-design-lite';
import AppRouter from '../AppRouter';
export default class App extends React.Component {
render () {
render() {
return (
<div>
<ToastContainer />
@@ -5,23 +5,22 @@
background: white;
padding: 10px 12px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
transform: scale(.8);
margin: 0;
width: 100%;
margin: 0 0 .5em;
&:hover {
&:not(:disabled):hover {
box-shadow: none;
color: white;
background-color: #519954;
cursor: pointer;
}
&:disabled {
cursor: not-allowed;
}
}
.active {
@@ -3,16 +3,21 @@ import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './ApproveButton.css';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
const ApproveButton = ({active, minimal, onClick, className}) => {
const ApproveButton = ({ active, minimal, onClick, className, disabled }) => {
const text = active ? t('modqueue.approved') : t('modqueue.approve');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
className={cn(
styles.root,
{ [styles.minimal]: minimal, [styles.active]: active },
className
)}
onClick={onClick}
disabled={disabled || active}
>
<Icon name={'done'} className={styles.icon} />
{!minimal && text}
@@ -24,8 +29,8 @@ ApproveButton.propTypes = {
className: PropTypes.string,
active: PropTypes.bool,
minimal: PropTypes.bool,
disabled: PropTypes.bool,
onClick: PropTypes.func,
};
export default ApproveButton;
@@ -1,153 +1,153 @@
.dialog {
border: none;
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
width: 500px;
width: 400px;
top: 50%;
transform: translateY(-50%);
height: 184px;
padding: 20px;
border-radius: 4px;
}
h2 {
color: black;
font-size: 1.76em;
font-weight: 500;
margin: 0;
}
.header {
color: black;
font-size: 1.5em;
font-weight: 500;
margin: 0 0 8px 0;
}
h3 {
color: black;
font-size: 1.4em;
font-weight: 500;
margin: 0;
}
.subheader {
color: black;
font-size: 1.3em;
font-weight: 500;
margin: 0 0 8px 0;
}
.textField {
margin-top: 15px;
margin-top: 15px;
}
.textField label {
font-size: 1.08em;
font-weight: bold;
margin-bottom: 5px;
font-size: 1.08em;
font-weight: bold;
margin-bottom: 5px;
}
.textField input {
width: 100%;
display: block;
border: none;
outline: none;
border: 1px solid rgba(0,0,0,.12);
padding: 10px 6px;
box-sizing: border-box;
border-radius: 2px;
margin: 5px auto;
width: 100%;
display: block;
border: none;
outline: none;
border: 1px solid rgba(0,0,0,.12);
padding: 10px 6px;
box-sizing: border-box;
border-radius: 2px;
margin: 5px auto;
}
.footer {
margin: 20px auto 10px;
text-align: center;
margin: 20px auto 10px;
text-align: center;
}
.footer span {
display: block;
margin-bottom: 5px;
display: block;
margin-bottom: 5px;
}
.footer a {
color: #2c69b6;
cursor: pointer;
margin: 0 5px;
color: #2c69b6;
cursor: pointer;
margin: 0 5px;
}
.socialConnections {
margin-bottom: 20px;
margin-bottom: 20px;
}
.signInButton {
margin-top: 10px;
margin-top: 10px;
}
.close {
font-size: 20px;
line-height: 14px;
top: 10px;
right: 10px;
position: absolute;
display: block;
font-weight: bold;
color: #363636;
cursor: pointer;
font-size: 20px;
line-height: 14px;
top: 10px;
right: 10px;
position: absolute;
display: block;
font-weight: bold;
color: #363636;
cursor: pointer;
}
.close:hover {
color: #6b6b6b;
color: #6b6b6b;
}
input.error{
border: solid 2px #f44336;
border: solid 2px #f44336;
}
.errorMsg, .hint {
color: grey;
font-weight: 600;
padding: 3px 0 16px;
color: grey;
font-weight: 600;
padding: 3px 0 16px;
}
.alert {
padding: 10px;
margin-bottom: 20px;
border-radius: 2px;
padding: 10px;
margin-bottom: 20px;
border-radius: 2px;
}
.alert--success {
border: solid 1px #1ec00e;
background: #cbf1b8;
color: #006900;
border: solid 1px #1ec00e;
background: #cbf1b8;
color: #006900;
}
.alert--error {
background: #FFEBEE;
color: #B71C1C;
background: #FFEBEE;
color: #B71C1C;
}
.userBox a {
color: #2c69b6;
cursor: pointer;
margin: 0px;
color: #2c69b6;
cursor: pointer;
margin: 0px;
}
.attention {
display: inline-block;
width: 15px;
height: 15px;
background: #B71C1C;
color: #FFEBEE;
font-weight: bolder;
padding: 4px;
vertical-align: middle;
border-radius: 20px;
box-sizing: border-box;
font-size: 9px;
line-height: 7px;
text-align: center;
margin-right: 5px;
display: inline-block;
width: 15px;
height: 15px;
background: #B71C1C;
color: #FFEBEE;
font-weight: bolder;
padding: 4px;
vertical-align: middle;
border-radius: 20px;
box-sizing: border-box;
font-size: 9px;
line-height: 7px;
text-align: center;
margin-right: 5px;
}
.action {
margin-top: 15px;
margin-top: 15px;
}
.passwordRequestSuccess {
border: 1px solid green;
background-color: lightgreen;
padding: 10px;
border: 1px solid green;
background-color: lightgreen;
padding: 10px;
}
.passwordRequestFailure {
border: 1px solid orange;
background-color: 1px solid coral;
padding: 10px;
border: 1px solid orange;
background-color: 1px solid coral;
padding: 10px;
}
.cancel {
@@ -160,6 +160,20 @@ input.error{
}
.buttons {
margin: 20px;
text-align: center;
margin-top: 8px;
margin-bottom: 6px;
text-align: right;
}
.legend {
padding: 0;
font-weight: bold;
}
.messageInput {
border-radius: 3px;
width: 100%;
padding: 10px;
font-size: 14px;
box-sizing: border-box;
}
@@ -1,45 +1,130 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import { Dialog } from 'coral-ui';
import styles from './BanUserDialog.css';
import Button from 'coral-ui/components/Button';
import t from 'coral-framework/services/i18n';
const BanUserDialog = ({open, onCancel, onPerform, username, info}) => (
<Dialog
className={cn(styles.dialog, 'talk-ban-user-dialog')}
id="banUserDialog"
open={open}
onCancel={onCancel}
title={t('bandialog.ban_user')}>
<span className={styles.close} onClick={onCancel}>×</span>
<div className={styles.header}>
<h2>{t('bandialog.ban_user')}</h2>
</div>
<div className={styles.separator}>
<h3>{t('bandialog.are_you_sure', username)}</h3>
<i>{info}</i>
</div>
<div className={styles.buttons}>
<Button
className={cn(styles.cancel, 'talk-ban-user-dialog-button-cancel')}
cStyle="cancel"
onClick={onCancel}
raised >
{t('bandialog.cancel')}
</Button>
<Button
className={cn(styles.ban, 'talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={onPerform}
raised >
{t('bandialog.yes_ban_user')}
</Button>
</div>
</Dialog>
);
const initialState = { step: 0, message: '' };
class BanUserDialog extends React.Component {
state = initialState;
componentWillReceiveProps(next) {
if (this.props.open && !next.open) {
this.setState(initialState);
}
}
handleMessageChange = e => {
const { value: message } = e;
this.setState({ message });
};
goToStep1 = () => {
this.setState({
step: 1,
message: t('bandialog.email_message_ban', this.props.username),
});
};
renderStep0() {
const { onCancel, username, info } = this.props;
return (
<section>
<h2 className={styles.header}>{t('bandialog.ban_user')}</h2>
<h3 className={styles.subheader}>
{t('bandialog.are_you_sure', username)}
</h3>
<p className={styles.description}>{info}</p>
<div className={styles.buttons}>
<Button
className={cn('talk-ban-user-dialog-button-cancel')}
cStyle="white"
onClick={onCancel}
raised
>
{t('bandialog.cancel')}
</Button>
<Button
className={cn('talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={this.goToStep1}
raised
>
{t('bandialog.yes_ban_user')}
</Button>
</div>
</section>
);
}
renderStep1() {
const { onCancel, onPerform } = this.props;
const { message } = this.state;
return (
<section>
<h2 className={styles.header}>{t('bandialog.notify_ban_headline')}</h2>
<p className={styles.description}>
{t('bandialog.notify_ban_description')}
</p>
<fieldset>
<legend className={styles.legend}>
{t('bandialog.write_a_message')}
</legend>
<textarea
rows={5}
className={styles.messageInput}
value={message}
onChange={this.handleMessageChange}
/>
</fieldset>
<div className={styles.buttons}>
<Button
className={cn('talk-ban-user-dialog-button-cancel')}
cStyle="white"
onClick={onCancel}
raised
>
{t('bandialog.cancel')}
</Button>
<Button
className={cn('talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={onPerform}
raised
>
{t('bandialog.send')}
</Button>
</div>
</section>
);
}
render() {
const { step } = this.state;
const { open, onCancel } = this.props;
return (
<Dialog
className={cn(styles.dialog, 'talk-ban-user-dialog')}
id="banUserDialog"
open={open}
onCancel={onCancel}
title={t('bandialog.ban_user')}
>
<span className={styles.close} onClick={onCancel}>
×
</span>
{step === 0 && this.renderStep0()}
{step === 1 && this.renderStep1()}
</Dialog>
);
}
}
BanUserDialog.propTypes = {
open: PropTypes.bool,
@@ -1,15 +1,11 @@
import React from 'react';
import {Button} from 'coral-ui';
import { Button } from 'coral-ui';
import t from 'coral-framework/services/i18n';
import {withCopyToClipboard} from 'coral-framework/hocs';
import { withCopyToClipboard } from 'coral-framework/hocs';
class ButtonCopyToClipboard extends React.Component {
render () {
return (
<Button {...this.props} >
{t('common.copy')}
</Button>
);
render() {
return <Button {...this.props}>{t('common.copy')}</Button>;
}
}
@@ -1,10 +1,10 @@
import React from 'react';
import {murmur3} from 'murmurhash-js';
import {CSSTransitionGroup} from 'react-transition-group';
import { murmur3 } from 'murmurhash-js';
import { CSSTransitionGroup } from 'react-transition-group';
import styles from './CommentAnimatedEdit.css';
import PropTypes from 'prop-types';
const CommentBodyHighlighter = ({children, body}) => {
const CommentAnimatedEdit = ({ children, body }) => {
return (
<CSSTransitionGroup
component={'div'}
@@ -20,14 +20,16 @@ const CommentBodyHighlighter = ({children, body}) => {
transitionEnterTimeout={3600}
transitionLeaveTimeout={2800}
>
{React.cloneElement(React.Children.only(children), {key: murmur3(body)})}
{React.cloneElement(React.Children.only(children), {
key: murmur3(body),
})}
</CSSTransitionGroup>
);
};
CommentBodyHighlighter.propTypes = {
CommentAnimatedEdit.propTypes = {
children: PropTypes.node,
body: PropTypes.string,
};
export default CommentBodyHighlighter;
export default CommentAnimatedEdit;
@@ -1,86 +0,0 @@
import React from 'react';
import {matchLinks} from '../utils';
import memoize from 'lodash/memoize';
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
// generate a regulare expression that catches the `phrases`.
function generateRegExp(phrases) {
const inner = phrases
.map((phrase) =>
phrase.split(/\s+/)
.map((word) => escapeRegExp(word))
.join('[\\s"?!.]+')
).join('|');
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
try {
return new RegExp(pattern, 'iu');
}
catch (_err) {
// IE does not support unicode support, so we'll create one without.
return new RegExp(pattern, 'i');
}
}
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
function getPhrasesRegexp(suspectWords, bannedWords) {
return generateRegExp([...suspectWords, ...bannedWords]);
}
// Memoized version as arguments rarely change.
const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
// markPhrases looks for `supsectWords` and `bannedWords` inside `body` and highlights them by returning
// an array of React Elements.
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
const tokens = body.split(regexp);
return tokens.map((token, i) =>
i % 3 === 2
? <mark key={`${keyPrefix}_${i}`}>{token}</mark>
: token
);
}
// markLinks looks for links inside `body` and highlights them by returning
// an array of React Elements.
function markLinks(body) {
const matches = matchLinks(body);
const content = [];
let index = 0;
if (matches) {
matches
.forEach((match, i) => {
content.push(body.substring(index, match.index));
content.push(<mark key={i}>{match.text}</mark>);
index = match.lastIndex;
});
}
content.push(body.substring(index));
return content;
}
export default ({suspectWords, bannedWords, body, ...rest}) => {
// First highlight links.
const content = markLinks(body)
.map((element, index) => {
// Keep highlighted links.
if (typeof element !== 'string') {
return element;
}
// Highlight suspect and banned phrase inside this part of text.
return markPhrases(element, suspectWords, bannedWords, index);
});
return (
<div {...rest}>
{content}
</div>
);
};
@@ -5,7 +5,7 @@
.moreDetail {
position: absolute;
font-size: 12px;
font-size: 13px;
font-weight: 500;
color: black;
right: 16px;
@@ -1,4 +1,4 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styles from './CommentDetails.css';
import t from 'coral-framework/services/i18n';
@@ -7,26 +7,26 @@ import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
class CommentDetails extends Component {
state = {
showDetail: false
showDetail: false,
};
constructor () {
constructor() {
super();
this.state = {
showDetail: false
showDetail: false,
};
}
toggleDetail = () => {
this.setState((state) => ({
showDetail: !state.showDetail
this.setState(state => ({
showDetail: !state.showDetail,
}));
this.props.clearHeightCache && this.props.clearHeightCache();
}
};
render() {
const {data, root, comment, clearHeightCache} = this.props;
const {showDetail} = this.state;
const { data, root, comment, clearHeightCache } = this.props;
const { showDetail } = this.state;
const queryData = {
root,
comment,
@@ -49,12 +49,14 @@ class CommentDetails extends Component {
queryData={queryData}
more={showDetail}
/>
{showDetail && <Slot
fill="adminCommentMoreDetails"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>}
{showDetail && (
<Slot
fill="adminCommentMoreDetails"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>
)}
</div>
);
}
@@ -0,0 +1,109 @@
import React from 'react';
import PropTypes from 'prop-types';
import { matchLinks } from '../utils';
import memoize from 'lodash/memoize';
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
// generate a regulare expression that catches the `phrases`.
function generateRegExp(phrases) {
const inner = phrases
.map(phrase =>
phrase
.split(/\s+/)
.map(word => escapeRegExp(word))
.join('[\\s"?!.]+')
)
.join('|');
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
try {
return new RegExp(pattern, 'iu');
} catch (_err) {
// IE does not support unicode support, so we'll create one without.
return new RegExp(pattern, 'i');
}
}
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
function getPhrasesRegexp(suspectWords, bannedWords) {
return generateRegExp([...suspectWords, ...bannedWords]);
}
// Memoized version as arguments rarely change.
const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
// markPhrases looks for `supsectWords` and `bannedWords` inside `body` and highlights them by returning
// an array of React Elements.
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
const tokens = body.split(regexp);
return tokens.map(
(token, i) =>
i % 3 === 2 ? <mark key={`${keyPrefix}_${i}`}>{token}</mark> : token
);
}
// markLinks looks for links inside `body` and highlights them by returning
// an array of React Elements.
function markLinks(body) {
const matches = matchLinks(body);
const content = [];
let index = 0;
if (matches) {
matches.forEach((match, i) => {
content.push(body.substring(index, match.index));
content.push(<mark key={i}>{match.text}</mark>);
index = match.lastIndex;
});
}
content.push(body.substring(index));
return content;
}
const CommentFormatter = ({
body,
suspectWords,
bannedWords,
className = 'comment',
...rest
}) => {
// Breaking the body by line break
const textbreaks = body.split('\n');
return (
<span className={`${className}-text`} {...rest}>
{textbreaks.map((line, i) => {
const content = markLinks(line).map((element, index) => {
// Keep highlighted links.
if (typeof element !== 'string') {
return element;
}
// Highlight suspect and banned phrase inside this part of text.
return markPhrases(element, suspectWords, bannedWords, index);
});
return (
<span key={i} className={`${className}-line`}>
{content}
{i !== textbreaks.length - 1 && (
<br className={`${className}-linebreak`} />
)}
</span>
);
})}
</span>
);
};
CommentFormatter.propTypes = {
className: PropTypes.string,
bannedWords: PropTypes.array,
suspectWords: PropTypes.array,
body: PropTypes.string,
};
export default CommentFormatter;
@@ -5,41 +5,72 @@ import Slot from 'coral-framework/components/Slot';
import FlagLabel from 'coral-ui/components/FlagLabel';
import cn from 'classnames';
import styles from './CommentLabels.css';
import { ADMIN, MODERATOR, STAFF } from 'coral-framework/constants/roles';
const staffRoles = ['ADMIN', 'STAFF', 'MODERATOR'];
const staffRoles = [ADMIN, MODERATOR, STAFF];
function isUserFlagged(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.user);
return actions.some(
action => action.__typename === 'FlagAction' && action.user
);
}
function getUserFlaggedType(actions) {
return actions
.some((action) =>
return actions.some(
action =>
action.__typename === 'FlagAction' &&
action.user &&
action.user.roles.some((role) => staffRoles.includes(role))
) ? 'Staff' : 'User';
staffRoles.includes(action.user.role)
)
? 'Staff'
: 'User';
}
function hasSuspectedWords(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'Matched suspect word filter');
return actions.some(
action =>
action.__typename === 'FlagAction' && action.reason === 'SUSPECT_WORD'
);
}
function hasHistoryFlag(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'TRUST');
return actions.some(
action => action.__typename === 'FlagAction' && action.reason === 'TRUST'
);
}
const CommentLabels = ({comment, comment: {className, status, actions, hasParent}}) => {
const CommentLabels = ({
comment,
comment: { className, status, actions, hasParent },
}) => {
return (
<div className={cn(className, styles.root)}>
<div className={styles.coreLabels}>
{hasParent && <Label iconName="reply" className={styles.replyLabel}>reply</Label>}
{status === 'PREMOD' && <Label iconName="query_builder" className={styles.premodLabel}>Pre-Mod</Label>}
{isUserFlagged(actions) && <FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>}
{hasSuspectedWords(actions) && <FlagLabel iconName="sms_failed">Suspect</FlagLabel>}
{hasHistoryFlag(actions) && <FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>}
{hasParent && (
<Label iconName="reply" className={styles.replyLabel}>
reply
</Label>
)}
{status === 'PREMOD' && (
<Label iconName="query_builder" className={styles.premodLabel}>
Pre-Mod
</Label>
)}
{isUserFlagged(actions) && (
<FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>
)}
{hasSuspectedWords(actions) && (
<FlagLabel iconName="sms_failed">Suspect</FlagLabel>
)}
{hasHistoryFlag(actions) && (
<FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>
)}
</div>
<Slot className={styles.slot} fill="adminCommentLabels" queryData={{comment}} />
<Slot
className={styles.slot}
fill="adminCommentLabels"
queryData={{ comment }}
/>
</div>
);
};
@@ -4,7 +4,7 @@ import styles from './CountBadge.css';
import t from 'coral-framework/services/i18n';
const CountBadge = ({count}) => {
const CountBadge = ({ count }) => {
let number = count;
// shorten large counts to abbreviations
@@ -16,13 +16,11 @@ const CountBadge = ({count}) => {
number = `${(number / 1e3).toFixed(1)}${t('modqueue.thousand')}`;
}
return (
<span className={styles.count}>{number}</span>
);
return <span className={styles.count}>{number}</span>;
};
CountBadge.propTypes = {
count: PropTypes.number.isRequired
count: PropTypes.number.isRequired,
};
export default CountBadge;
+30 -30
View File
@@ -1,61 +1,61 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Navigation, Drawer} from 'react-mdl';
import {IndexLink, Link} from 'react-router';
import { Navigation, Drawer } from 'react-mdl';
import { IndexLink, Link } from 'react-router';
import styles from './Drawer.css';
import t from 'coral-framework/services/i18n';
import {can} from 'coral-framework/services/perms';
import { can } from 'coral-framework/services/perms';
import cn from 'classnames';
const CoralDrawer = ({handleLogout, auth = {}}) => (
const CoralDrawer = ({ handleLogout, auth = {} }) => (
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
{ auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
<div>
<Navigation className={styles.nav}>
{
can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}>
{t('configure.moderate')}
</IndexLink>
)
}
{can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}
>
{t('configure.moderate')}
</IndexLink>
)}
<Link
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}>
activeClassName={styles.active}
>
{t('configure.stories')}
</Link>
<Link
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}>
activeClassName={styles.active}
>
{t('configure.community')}
</Link>
{
can(auth.user, 'UPDATE_CONFIG') &&
(
<Link
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}>
{t('configure.configure')}
</Link>
)
}
{can(auth.user, 'UPDATE_CONFIG') && (
<Link
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}
>
{t('configure.configure')}
</Link>
)}
<a onClick={handleLogout}>Sign Out</a>
<span>{`v${process.env.VERSION}`}</span>
</Navigation>
</div> : null }
</div>
) : null}
</Drawer>
);
CoralDrawer.propTypes = {
handleLogout: PropTypes.func.isRequired,
restricted: PropTypes.bool, // hide app elements from a logged out user
auth: PropTypes.object
auth: PropTypes.object,
};
export default CoralDrawer;
@@ -1,15 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Card} from 'coral-ui';
import { Card } from 'coral-ui';
const EmptyCard = (props) => (
<Card style={{textAlign: 'center', maxWidth: 400, margin: '0 auto'}}>
const EmptyCard = props => (
<Card style={{ textAlign: 'center', maxWidth: 400, margin: '0 auto' }}>
{props.children}
</Card>
);
EmptyCard.propTypes = {
children: PropTypes.node.isRequired
children: PropTypes.node.isRequired,
};
export default EmptyCard;
@@ -1,11 +1,11 @@
import React from 'react';
import {Layout} from 'react-mdl';
import { Layout } from 'react-mdl';
import styles from './FullLoading.css';
import {CoralLogo} from 'coral-ui';
import { CoralLogo } from 'coral-ui';
export const FullLoading = () => (
<Layout fixedDrawer>
<div className={styles.layout} >
<div className={styles.layout}>
<h1>Loading</h1>
<CoralLogo />
</div>
+126
View File
@@ -0,0 +1,126 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import { Navigation, Header, IconButton, MenuItem, Menu } from 'react-mdl';
import { Link, IndexLink } from 'react-router';
import styles from './Header.css';
import t from 'coral-framework/services/i18n';
import { Logo } from './Logo';
import { can } from 'coral-framework/services/perms';
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
import CommunityIndicator from '../routes/Community/containers/Indicator';
const CoralHeader = ({
handleLogout,
showShortcuts = () => {},
auth,
root,
data,
}) => {
return (
<div className={styles.headerWrapper}>
<Header className={styles.header}>
<Logo className={styles.logo} />
<div>
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
<Navigation className={styles.nav}>
{can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
id="moderateNav"
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}
>
{t('configure.moderate')}
<ModerationIndicator root={root} data={data} />
</IndexLink>
)}
<Link
id="storiesNav"
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}
>
{t('configure.stories')}
</Link>
<Link
id="communityNav"
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}
>
{t('configure.community')}
<CommunityIndicator root={root} data={data} />
</Link>
{can(auth.user, 'UPDATE_CONFIG') && (
<Link
id="configureNav"
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}
>
{t('configure.configure')}
</Link>
)}
</Navigation>
) : null}
<div className={styles.rightPanel}>
<ul>
<li className={cn(styles.settings, 'talk-admin-header-settings')}>
<div>
<IconButton
name="settings"
id="menu-settings"
className="talk-admin-header-settings-button"
/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={() => showShortcuts(true)}>
{t('configure.shortcuts')}
</MenuItem>
<MenuItem>
<a
href="https://github.com/coralproject/talk/releases"
target="_blank"
rel="noopener noreferrer"
>
View latest version
</a>
</MenuItem>
<MenuItem>
<a
href="https://support.coralproject.net"
target="_blank"
rel="noopener noreferrer"
>
Report a bug or give feedback
</a>
</MenuItem>
<MenuItem
onClick={handleLogout}
className="talk-admin-header-sign-out"
>
{t('configure.sign_out')}
</MenuItem>
</Menu>
</div>
</li>
<li>{`v${process.env.VERSION}`}</li>
</ul>
</div>
</div>
</Header>
</div>
);
};
CoralHeader.propTypes = {
auth: PropTypes.object,
showShortcuts: PropTypes.func,
handleLogout: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
};
export default CoralHeader;
@@ -1,7 +1,7 @@
import React from 'react';
import {matchLinks} from '../utils';
import { matchLinks } from '../utils';
export default ({text, children}) => {
export default ({ text, children }) => {
const hasLinks = !!matchLinks(text);
if (!hasLinks) {
@@ -1,7 +1,6 @@
import React from 'react';
import styles from './Indicator.css';
const Indicator = () =>
<span className={styles.indicator}></span>;
const Indicator = () => <span className={styles.indicator} />;
export default Indicator;
@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Layout as LayoutMDL} from 'react-mdl';
import Header from '../../containers/Header';
import Drawer from '../Drawer';
import { Layout as LayoutMDL } from 'react-mdl';
import Header from '../containers/Header';
import Drawer from './Drawer';
import styles from './Layout.css';
const Layout = ({
@@ -18,14 +18,8 @@ const Layout = ({
showShortcuts={toggleShortcutModal}
auth={auth}
/>
<Drawer
handleLogout={handleLogout}
restricted={restricted}
auth={auth}
/>
<div className={styles.layout}>
{children}
</div>
<Drawer handleLogout={handleLogout} restricted={restricted} auth={auth} />
<div className={styles.layout}>{children}</div>
</LayoutMDL>
);
@@ -34,7 +28,7 @@ Layout.propTypes = {
auth: PropTypes.object,
handleLogout: PropTypes.func,
toggleShortcutModal: PropTypes.func,
restricted: PropTypes.bool // hide elements from a user that's logged out
restricted: PropTypes.bool, // hide elements from a user that's logged out
};
export default Layout;
@@ -1,23 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button} from 'coral-ui';
import { Button } from 'coral-ui';
import styles from './LoadMore.css';
import cn from 'classnames';
const LoadMore = ({loadMore, showLoadMore, className, ...rest}) =>
const LoadMore = ({ loadMore, showLoadMore, className = '', ...rest }) => (
<div {...rest} className={cn(className, styles.loadMoreContainer)}>
{
showLoadMore && <Button
className={styles.loadMore}
onClick={loadMore}>
{showLoadMore && (
<Button className={styles.loadMore} onClick={loadMore}>
Load More
</Button>
}
</div>;
)}
</div>
);
LoadMore.propTypes = {
className: PropTypes.string,
loadMore: PropTypes.func.isRequired,
showLoadMore: PropTypes.bool.isRequired
showLoadMore: PropTypes.bool.isRequired,
};
export default LoadMore;
@@ -1,9 +1,9 @@
import React from 'react';
import styles from './Logo.css';
import {CoralLogo} from 'coral-ui';
import { CoralLogo } from 'coral-ui';
import PropTypes from 'prop-types';
export const Logo = ({className = ''}) => (
export const Logo = ({ className = '' }) => (
<div className={`${styles.logo} ${className}`}>
<h1>
<CoralLogo className={styles.base} />
@@ -13,5 +13,5 @@ export const Logo = ({className = ''}) => (
);
Logo.propTypes = {
className: PropTypes.string
className: PropTypes.string,
};
+5 -3
View File
@@ -1,11 +1,13 @@
import React from 'react';
import {Button, Icon} from 'react-mdl';
import { Button, Icon } from 'react-mdl';
import styles from './Modal.css';
export default ({open, children, onClose}) => (
export default ({ open, children, onClose }) => (
<div className={`${styles.container} ${!open ? styles.hide : ''}`}>
<div className={styles.inner}>
<Button className={styles.close} onClick={onClose}><Icon name='close' /></Button>
<Button className={styles.close} onClick={onClose}>
<Icon name="close" />
</Button>
{children}
</div>
</div>
@@ -28,7 +28,7 @@
padding: 15px;
box-sizing: border-box;
box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15);
z-index: 10;
z-index: 4;
.ctaHeader {
font-size: 16px;
@@ -5,52 +5,75 @@ import styles from './ModerationKeysModal.css';
import t from 'coral-framework/services/i18n';
export default class ModerationKeysModal extends React.Component {
static propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
hideShortcutsNote: PropTypes.func.isRequired,
shortcutsNoteVisible: PropTypes.string.isRequired,
queueCount: PropTypes.number.isRequired
}
queueCount: PropTypes.number.isRequired,
};
buildShortcuts = () => {
return [
{
title: 'modqueue.navigation',
shortcuts: {
'j': 'modqueue.next_comment',
'k': 'modqueue.prev_comment',
j: 'modqueue.next_comment',
k: 'modqueue.prev_comment',
'ctrl+f': 'modqueue.toggle_search',
't': 'modqueue.next_queue',
t: 'modqueue.next_queue',
[`1...${this.props.queueCount}`]: 'modqueue.jump_to_queue',
's': 'modqueue.singleview',
'?': 'modqueue.thismenu'
}
z: 'modqueue.singleview',
'?': 'modqueue.thismenu',
},
},
{
title: 'modqueue.actions',
shortcuts: {
'd': 'modqueue.approve',
'f': 'modqueue.reject'
}
}
d: 'modqueue.approve',
f: 'modqueue.reject',
},
},
];
}
};
render () {
const {open, onClose, hideShortcutsNote, shortcutsNoteVisible} = this.props;
render() {
const {
open,
onClose,
hideShortcutsNote,
shortcutsNoteVisible,
} = this.props;
return (
<div>
<div className={styles.callToAction} style={{display: shortcutsNoteVisible === 'show' ? 'block' : 'none'}}>
<div onClick={hideShortcutsNote} className={styles.closeButton}>×</div>
<div
className={styles.callToAction}
style={{
display: shortcutsNoteVisible === 'show' ? 'block' : 'none',
}}
>
<div onClick={hideShortcutsNote} className={styles.closeButton}>
×
</div>
<p className={styles.ctaHeader}>{t('modqueue.mod_faster')}</p>
<p><strong>{t('modqueue.try_these')}:</strong></p>
<p>
<strong>{t('modqueue.try_these')}:</strong>
</p>
<ul>
<li><span>{t('modqueue.approve')}</span> <span className={styles.smallKey}>d</span></li>
<li><span>{t('modqueue.reject')}</span> <span className={styles.smallKey}>f</span></li>
<li>
<span>{t('modqueue.approve')}</span>{' '}
<span className={styles.smallKey}>d</span>
</li>
<li>
<span>{t('modqueue.reject')}</span>{' '}
<span className={styles.smallKey}>f</span>
</li>
</ul>
<p><span>{t('modqueue.view_more_shortcuts')}</span> <span className={styles.smallKey}>{t('modqueue.shift_key')}</span> + <span className={styles.smallKey}>/</span></p>
<p>
<span>{t('modqueue.view_more_shortcuts')}</span>{' '}
<span className={styles.smallKey}>{t('modqueue.shift_key')}</span> +{' '}
<span className={styles.smallKey}>/</span>
</p>
</div>
<Modal open={open} onClose={onClose}>
<h3>{t('modqueue.shortcuts')}</h3>
@@ -63,9 +86,11 @@ export default class ModerationKeysModal extends React.Component {
</tr>
</thead>
<tbody>
{Object.keys(shortcut.shortcuts).map((key) => (
{Object.keys(shortcut.shortcuts).map(key => (
<tr key={`${key}tr`}>
<td className={styles.shortcut}><span className={styles.key}>{key}</span></td>
<td className={styles.shortcut}>
<span className={styles.key}>{key}</span>
</td>
<td>{t(shortcut.shortcuts[key])}</td>
</tr>
))}
@@ -1,13 +1,16 @@
import React from 'react';
import {Layout} from 'react-mdl';
import { Layout } from 'react-mdl';
import styles from './NotFound.css';
export const NotFound = () => (
<Layout fixedDrawer>
<div className={styles.layout} >
<div className={styles.layout}>
<h1>Page Not Found</h1>
<p>The communicorn feels your pain.</p>
<img src="https://coralproject.net/images/communicorn.jpg" alt="Communicorn"/>
<img
src="https://coralproject.net/images/communicorn.jpg"
alt="Communicorn"
/>
</div>
</Layout>
);
@@ -5,23 +5,22 @@
background: white;
padding: 10px 11px;
box-sizing: border-box;
vertical-align: middle;
line-height: 24px;
font-size: 17px;
height: 47px;
border-radius: 3px;
text-transform: capitalize;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
width: 129px;
transform: scale(.8);
margin: 0;
width: 100%;
margin: 0 0 .5em;
&:hover {
&:not(:disabled):hover {
color: white;
background-color: #D03235;
box-shadow: none;
cursor: pointer;
}
&:disabled {
cursor: not-allowed;
}
}
.active {
@@ -3,16 +3,21 @@ import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './RejectButton.css';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
const RejectButton = ({active, minimal, onClick, className}) => {
const RejectButton = ({ active, minimal, onClick, className, disabled }) => {
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
className={cn(
styles.root,
{ [styles.minimal]: minimal, [styles.active]: active },
className
)}
onClick={onClick}
disabled={disabled || active}
>
<Icon name={'close'} className={styles.icon} />
{!minimal && text}
@@ -25,7 +30,7 @@ RejectButton.propTypes = {
active: PropTypes.bool,
minimal: PropTypes.bool,
onClick: PropTypes.func,
disabled: PropTypes.bool,
};
export default RejectButton;
@@ -1,25 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import {RadioGroup, Radio} from 'react-mdl';
import { Dialog } from 'coral-ui';
import { RadioGroup, Radio } from 'react-mdl';
import styles from './SuspendUserDialog.css';
import cn from 'classnames';
import Button from 'coral-ui/components/Button';
import t, {timeago} from 'coral-framework/services/i18n';
import {dateAdd} from 'coral-framework/utils';
import t, { timeago } from 'coral-framework/services/i18n';
import { dateAdd } from 'coral-framework/utils';
const initialState = {step: 0, duration: '3'};
const initialState = { step: 0, duration: '3' };
function durationsToDate(hours) {
// Add 1 minute more to help `timeago.js` to display the correct duration.
return dateAdd(new Date(), 'minute', hours * 60 + 1);
}
class SuspendUserDialog extends React.Component {
state = initialState;
componentWillReceiveProps(next) {
@@ -28,13 +26,13 @@ class SuspendUserDialog extends React.Component {
}
}
handleDurationChange = (event) => {
this.setState({duration: event.target.value});
}
handleDurationChange = event => {
this.setState({ duration: event.target.value });
};
handleMessageChange = (event) => {
this.setState({message: event.target.value});
}
handleMessageChange = event => {
this.setState({ message: event.target.value });
};
goToStep1 = () => {
this.setState({
@@ -43,13 +41,12 @@ class SuspendUserDialog extends React.Component {
'suspenduser.email_message_suspend',
this.props.username,
this.props.organizationName,
timeago(durationsToDate(this.state.duration)),
timeago(durationsToDate(this.state.duration))
),
});
}
};
handlePerform = () => {
this.props.onPerform({
message: this.state.message,
@@ -59,36 +56,49 @@ class SuspendUserDialog extends React.Component {
};
renderStep0() {
const {onCancel, username} = this.props;
const {duration} = this.state;
const { onCancel, username } = this.props;
const { duration } = this.state;
return (
<section>
<h1 className={styles.header}>
{t('suspenduser.title_suspend')}
</h1>
<section className="talk-admin-suspend-user-dialog-step-0">
<h1 className={styles.header}>{t('suspenduser.title_suspend')}</h1>
<p className={styles.description}>
{t('suspenduser.description_suspend', username)}
</p>
<fieldset>
<legend className={styles.legend}>{t('suspenduser.select_duration')}</legend>
<legend className={styles.legend}>
{t('suspenduser.select_duration')}
</legend>
<RadioGroup
name='status filter'
name="status filter"
value={duration}
childContainer='div'
childContainer="div"
onChange={this.handleDurationChange}
className={styles.radioGroup}
>
<Radio value='1'>{t('suspenduser.one_hour')}</Radio>
<Radio value='3'>{t('suspenduser.hours', 3)}</Radio>
<Radio value='24'>{t('suspenduser.hours', 24)}</Radio>
<Radio value='168'>{t('suspenduser.days', 7)}</Radio>
<Radio value="1">{t('suspenduser.one_hour')}</Radio>
<Radio value="3">{t('suspenduser.hours', 3)}</Radio>
<Radio value="24">{t('suspenduser.hours', 24)}</Radio>
<Radio value="168">{t('suspenduser.days', 7)}</Radio>
</RadioGroup>
</fieldset>
<div className={styles.buttons}>
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
<Button
cStyle="white"
className={styles.cancel}
onClick={onCancel}
raised
>
{t('suspenduser.cancel')}
</Button>
<Button cStyle="black" className={cn(styles.perform, 'talk-admin-suspend-user-dialog-confirm')} onClick={this.goToStep1} raised>
<Button
cStyle="black"
className={cn(
styles.perform,
'talk-admin-suspend-user-dialog-confirm'
)}
onClick={this.goToStep1}
raised
>
{t('suspenduser.suspend_user')}
</Button>
</div>
@@ -97,31 +107,40 @@ class SuspendUserDialog extends React.Component {
}
renderStep1() {
const {onCancel, username} = this.props;
const {message} = this.state;
const { message } = this.state;
const { onCancel, username } = this.props;
return (
<section>
<h1 className={styles.header}>
{t('suspenduser.title_notify')}
</h1>
<section className="talk-admin-suspend-user-dialog-step-1">
<h1 className={styles.header}>{t('suspenduser.title_notify')}</h1>
<p className={styles.description}>
{t('suspenduser.description_notify', username)}
</p>
<fieldset>
<legend className={styles.legend}>{t('suspenduser.write_message')}</legend>
<legend className={styles.legend}>
{t('suspenduser.write_message')}
</legend>
<textarea
rows={5}
className={styles.messageInput}
value={message}
onChange={this.handleMessageChange} />
onChange={this.handleMessageChange}
/>
</fieldset>
<div className={styles.buttons}>
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
<Button
cStyle="white"
className={styles.cancel}
onClick={onCancel}
raised
>
{t('suspenduser.cancel')}
</Button>
<Button
cStyle="black"
className={cn(styles.perform, 'talk-admin-suspend-user-dialog-send')}
className={cn(
styles.perform,
'talk-admin-suspend-user-dialog-send'
)}
onClick={this.handlePerform}
disabled={this.state.message.length === 0}
raised
@@ -134,8 +153,8 @@ class SuspendUserDialog extends React.Component {
}
render() {
const {open, onCancel} = this.props;
const {step} = this.state;
const { open, onCancel } = this.props;
const { step } = this.state;
return (
<Dialog
className={cn(styles.dialog, 'talk-admin-suspend-user-dialog')}
@@ -143,7 +162,13 @@ class SuspendUserDialog extends React.Component {
open={open}
>
<div className={styles.close}>
<button aria-label="Close" onClick={onCancel} className={styles.closeButton}>×</button>
<button
aria-label="Close"
onClick={onCancel}
className={styles.closeButton}
>
×
</button>
</div>
{step === 0 && this.renderStep0()}
{step === 1 && this.renderStep1()}
@@ -5,8 +5,9 @@ import AutosizeInput from 'react-input-autosize';
import PropTypes from 'prop-types';
import cn from 'classnames';
const autosizingRenderInput = ({onChange, value, addTag: _, ...other}) =>
<AutosizeInput type='text' onChange={onChange} value={value} {...other} />;
const autosizingRenderInput = ({ onChange, value, addTag: _, ...other }) => (
<AutosizeInput type="text" onChange={onChange} value={value} {...other} />
);
autosizingRenderInput.propTypes = {
onChange: PropTypes.func,
@@ -14,17 +15,16 @@ autosizingRenderInput.propTypes = {
addTag: PropTypes.func,
};
const TagsInputComponent = ({className = '', ...props}) => {
const TagsInputComponent = ({ className = '', ...props }) => {
return (
<TagsInput
addOnBlur={true}
addOnPaste={true}
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
pasteSplit={data => data.split(',').map(d => d.trim())}
className={cn(styles.root, 'tags-input', className)}
focusedClassName={styles.rootFocus}
renderInput={autosizingRenderInput}
{...props}
tagProps={{
className: styles.tag,
classNameRemove: styles.tagRemove,
@@ -207,7 +207,7 @@
.toastify__body {
color: white;
overflow-x: scroll;
overflow-x: auto;
font-size: 15px;
font-weight: 400;
}
@@ -1,6 +1,6 @@
import './ToastContainer.css';
import {defaultProps} from 'recompose';
import {ToastContainer} from 'react-toastify';
import { defaultProps } from 'recompose';
import { ToastContainer } from 'react-toastify';
export default defaultProps({
autoClose: 5000,
@@ -94,73 +94,52 @@
width: calc(100% - 90px);
}
.commentStatuses {
.tabBar {
padding: 0 0 0 10px;
margin: 0;
align-self: center;
list-style: none;
box-sizing: border-box;
li {
display: inline-block;
margin-right: 10px;
cursor: pointer;
padding: 0 10px;
}
border: none;
}
.active {
.tab {
display: inline-block;
margin-right: 10px;
cursor: pointer;
padding: 0 10px;
}
.tabButton {
border: none;
background: white;
border-radius: 0;
font-size: 1em;
padding: 5px;
cursor: pointer;
}
.tabButtonActive {
font-weight: bold;
border-bottom: 3px solid #F36451;
}
.bulkActionGroup {
height: 52px;
background-color: #efefef;
padding: 0 0 0 10px;
display: flex;
i {
margin-right: 0;
}
.bulkAction {
display: inline-block;
width: 48px;
height: 48px;
transform: scale(.7);
min-width: 0;
}
.bulkAction:last-child {
margin-left: -10px;
}
.username {
display: inline-block;
vertical-align: middle;
}
.selectedCommentsInfo {
align-self: center;
font-weight: 500;
margin-left: 15px;
.actionsMenu {
display: inline-block;
}
.loadMore>button {
background-color: #696969;
&:hover {
background-color: #404040;
color: white;
}
.actionsMenuSuspended {
background-color: #F29336;
border-color: #F29336;
color: white;
}
.toggleAll {
padding: 0 10px 0 0;
align-self: center;
.actionsMenuBanned {
background-color: #E45241;
border-color: #E45241;
color: white;
}
.commentList {
clear: both;
}
.bulkActionHeader {
display: flex;
justify-content: space-between;
height: 52px;
&.selected {
background-color: #efefef;
}
}
+281 -153
View File
@@ -1,94 +1,67 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import Comment from '../containers/UserDetailComment';
import capitalize from 'lodash/capitalize';
import styles from './UserDetail.css';
import {Icon, Drawer, Spinner} from 'coral-ui';
import {Slot} from 'coral-framework/components';
import AccountHistory from './AccountHistory';
import { Slot } from 'coral-framework/components';
import UserDetailCommentList from '../components/UserDetailCommentList';
import {
getReliability,
isSuspended,
isBanned,
} from 'coral-framework/utils/user';
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
import ClickOutside from 'coral-framework/components/ClickOutside';
import LoadMore from '../components/LoadMore';
import cn from 'classnames';
import capitalize from 'lodash/capitalize';
import {getReliability} from 'coral-framework/utils/user';
import ApproveButton from './ApproveButton';
import RejectButton from './RejectButton';
import {getErrorMessages} from 'coral-framework/utils';
import {
Icon,
Drawer,
Spinner,
TabBar,
Tab,
TabContent,
TabPane,
} from 'coral-ui';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import UserInfoTooltip from './UserInfoTooltip';
export default class UserDetail extends React.Component {
class UserDetail extends React.Component {
rejectThenReload = async info => {
await this.props.rejectComment(info);
this.props.data.refetch();
};
static propTypes = {
userId: PropTypes.string.isRequired,
hideUserDetail: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
changeStatus: PropTypes.func.isRequired,
toggleSelect: PropTypes.func.isRequired,
bulkAccept: PropTypes.func.isRequired,
bulkReject: PropTypes.func.isRequired,
toggleSelectAll: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
data: PropTypes.shape({
refetch: PropTypes.func.isRequired,
}),
activeTab: PropTypes.string.isRequired,
selectedCommentIds: PropTypes.array.isRequired,
viewUserDetail: PropTypes.any.isRequired,
loadMore: PropTypes.any.isRequired,
notify: PropTypes.func.isRequired
}
rejectThenReload = async (info) => {
try {
await this.props.rejectComment(info);
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
acceptThenReload = async (info) => {
try {
await this.props.acceptComment(info);
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
acceptThenReload = async info => {
await this.props.acceptComment(info);
this.props.data.refetch();
};
bulkAcceptThenReload = async () => {
try {
await this.props.bulkAccept();
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
await this.props.bulkAccept();
this.props.data.refetch();
};
bulkRejectThenReload = async () => {
try {
await this.props.bulkReject();
this.props.data.refetch();
} catch (err) {
await this.props.bulkReject();
this.props.data.refetch();
};
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
changeTab = tab => {
this.props.changeTab(tab);
};
showAll = () => {
this.props.changeStatus('all');
}
showSuspenUserDialog = () =>
this.props.showSuspendUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
showRejected = () => {
this.props.changeStatus('rejected');
}
showBanUserDialog = () =>
this.props.showBanUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
renderLoading() {
return (
@@ -100,36 +73,118 @@ export default class UserDetail extends React.Component {
);
}
renderError() {
return (
<ClickOutside onClickOutside={this.props.hideUserDetail}>
<Drawer onClose={this.props.hideUserDetail}>
<div>{this.props.data.error.message}</div>
</Drawer>
</ClickOutside>
);
}
getActionMenuLabel() {
const { root: { user } } = this.props;
if (isBanned(user)) {
return 'Banned';
} else if (isSuspended(user)) {
return 'Suspended';
}
return '';
}
renderLoaded() {
const {
data,
root,
root: {
user,
totalComments,
rejectedComments,
comments: {nodes, hasNextPage}
},
root: { me, user, totalComments, rejectedComments },
activeTab,
selectedCommentIds,
toggleSelect,
hideUserDetail,
viewUserDetail,
loadMore,
toggleSelectAll
toggleSelectAll,
unbanUser,
unsuspendUser,
modal,
} = this.props;
let rejectedPercent = (rejectedComments / totalComments) * 100;
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
// if totalComments is 0, you're dividing by zero
let rejectedPercent = rejectedComments / totalComments * 100;
// if totalComments is 0, you're dividing by zero, which is naughty
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
rejectedPercent = 0;
}
const banned = isBanned(user);
const suspended = isSuspended(user);
return (
<ClickOutside onClickOutside={hideUserDetail}>
<Drawer onClose={hideUserDetail}>
<h3>{user.username}</h3>
<ClickOutside onClickOutside={modal ? null : hideUserDetail}>
<Drawer
className="talk-admin-user-detail-drawer"
onClose={hideUserDetail}
>
<h3
className={cn(styles.username, 'talk-admin-user-detail-username')}
>
{user.username}
</h3>
{user.id && (
<ActionsMenu
icon="person"
className={cn(
styles.actionsMenu,
'talk-admin-user-detail-actions-menu'
)}
buttonClassNames={cn(
{
[styles.actionsMenuSuspended]: suspended,
[styles.actionsMenuBanned]: banned,
},
'talk-admin-user-detail-actions-button'
)}
label={this.getActionMenuLabel()}
>
{suspended ? (
<ActionsMenuItem onClick={() => unsuspendUser({ id: user.id })}>
Remove Suspension
</ActionsMenuItem>
) : (
<ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showSuspenUserDialog}
>
Suspend User
</ActionsMenuItem>
)}
{banned ? (
<ActionsMenuItem onClick={() => unbanUser({ id: user.id })}>
Remove Ban
</ActionsMenuItem>
) : (
<ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showBanUserDialog}
>
Ban User
</ActionsMenuItem>
)}
</ActionsMenu>
)}
{(banned || suspended) && (
<UserInfoTooltip
user={user}
banned={banned}
suspended={suspended}
/>
)}
<div>
<ul className={styles.userDetailList}>
@@ -139,13 +194,18 @@ export default class UserDetail extends React.Component {
{new Date(user.created_at).toLocaleString()}
</li>
{user.profiles.map(({id}) =>
{user.profiles.map(({ id }) => (
<li key={id}>
<Icon name="email" />
<span className={styles.userDetailItem}>Email:</span>
{id} <ButtonCopyToClipboard className={styles.copyButton} icon="content_copy" copyText={id} />
{id}{' '}
<ButtonCopyToClipboard
className={styles.copyButton}
icon="content_copy"
copyText={id}
/>
</li>
)}
))}
</ul>
<ul className={styles.stats}>
@@ -161,7 +221,12 @@ export default class UserDetail extends React.Component {
</li>
<li className={styles.stat}>
<span className={styles.statItem}>Reports</span>
<span className={cn(styles.statReportResult, styles[getReliability(user.reliable.flagger)])}>
<span
className={cn(
styles.statReportResult,
styles[getReliability(user.reliable.flagger)]
)}
>
{capitalize(getReliability(user.reliable.flagger))}
</span>
</li>
@@ -171,76 +236,139 @@ export default class UserDetail extends React.Component {
<Slot
fill="userProfile"
data={this.props.data}
queryData={{root, user}}
queryData={{ root, user }}
/>
<hr />
<div className={(selectedCommentIds.length > 0) ? cn(styles.bulkActionHeader, styles.selected) : styles.bulkActionHeader}>
{
selectedCommentIds.length === 0
? (
<ul className={styles.commentStatuses}>
<li className={activeTab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
<li className={activeTab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
</ul>
)
: (
<div className={styles.bulkActionGroup}>
<ApproveButton
onClick={this.bulkAcceptThenReload}
minimal
/>
<RejectButton
onClick={this.bulkRejectThenReload}
minimal
/>
<span className={styles.selectedCommentsInfo}> {selectedCommentIds.length} comments selected</span>
</div>
)
}
<div className={styles.toggleAll}>
<input
type='checkbox'
id='toogleAll'
checked={selectedCommentIds.length > 0 && selectedCommentIds.length === nodes.length}
onChange={(e) => {
toggleSelectAll(nodes.map((comment) => comment.id), e.target.checked);
}} />
<label htmlFor='toogleAll'>Select all</label>
</div>
</div>
<div className={styles.commentList}>
{
nodes.map((comment) => {
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return <Comment
key={comment.id}
user={user}
root={root}
data={data}
comment={comment}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selected={selected}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
/>;
})
}
</div>
<LoadMore
className={styles.loadMore}
loadMore={loadMore}
showLoadMore={hasNextPage}
/>
<TabBar
onTabClick={this.changeTab}
activeTab={activeTab}
className={cn(styles.tabBar, 'talk-admin-user-detail-tab-bar')}
aria-controls="talk-admin-user-detail-content"
tabClassNames={{
button: styles.tabButton,
buttonActive: styles.tabButtonActive,
}}
>
<Tab
tabId={'all'}
className={cn(
styles.tab,
styles.button,
'talk-admin-user-detail-all-tab'
)}
>
All
</Tab>
<Tab
tabId={'rejected'}
className={cn(styles.tab, 'talk-admin-user-detail-rejected-tab')}
>
Rejected
</Tab>
<Tab
tabId={'history'}
className={cn(
styles.tab,
styles.button,
'talk-admin-user-detail-history-tab'
)}
>
Account History
</Tab>
</TabBar>
<TabContent
activeTab={activeTab}
className="talk-admin-user-detail-content"
>
<TabPane
tabId={'all'}
className={'talk-admin-user-detail-all-tab-pane'}
>
<UserDetailCommentList
user={user}
root={root}
data={data}
loadMore={loadMore}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selectedCommentIds={selectedCommentIds}
toggleSelectAll={toggleSelectAll}
bulkAcceptThenReload={this.bulkAcceptThenReload}
bulkRejectThenReload={this.bulkRejectThenReload}
/>
</TabPane>
<TabPane
tabId={'rejected'}
className={'talk-admin-user-detail-rejected-tab-pane'}
>
<UserDetailCommentList
user={user}
root={root}
data={data}
loadMore={loadMore}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selectedCommentIds={selectedCommentIds}
toggleSelectAll={toggleSelectAll}
bulkAcceptThenReload={this.bulkAcceptThenReload}
bulkRejectThenReload={this.bulkRejectThenReload}
/>
</TabPane>
<TabPane
tabId={'history'}
className={'talk-admin-user-detail-history-tab-pane'}
>
<AccountHistory user={user} />
</TabPane>
</TabContent>
</Drawer>
</ClickOutside>
);
}
render() {
if (this.props.data.error) {
return this.renderError();
}
if (this.props.loading) {
return this.renderLoading();
}
return this.renderLoaded();
}
}
UserDetail.propTypes = {
userId: PropTypes.string.isRequired,
hideUserDetail: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
changeTab: PropTypes.func.isRequired,
toggleSelect: PropTypes.func.isRequired,
bulkAccept: PropTypes.func.isRequired,
bulkReject: PropTypes.func.isRequired,
toggleSelectAll: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
data: PropTypes.shape({
refetch: PropTypes.func.isRequired,
}),
activeTab: PropTypes.string.isRequired,
selectedCommentIds: PropTypes.array.isRequired,
viewUserDetail: PropTypes.any.isRequired,
loadMore: PropTypes.any.isRequired,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
modal: PropTypes.bool,
};
export default UserDetail;
@@ -81,6 +81,8 @@
}
.sideActions {
/* IE 11 needs this... */
width: 105px;
}
.editedMarker {
@@ -1,11 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Link} from 'react-router';
import { Link } from 'react-router';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import CommentDetails from './CommentDetails';
import styles from './UserDetailComment.css';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
import IfHasLink from 'coral-admin/src/components/IfHasLink';
import cn from 'classnames';
import CommentAnimatedEdit from './CommentAnimatedEdit';
@@ -13,19 +13,18 @@ import CommentLabels from '../containers/CommentLabels';
import ApproveButton from './ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import t, {timeago} from 'coral-framework/services/i18n';
import t, { timeago } from 'coral-framework/services/i18n';
class UserDetailComment extends React.Component {
approve = () =>
this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({ commentId: this.props.comment.id });
approve = () => (this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({commentId: this.props.comment.id})
);
reject = () => (this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({commentId: this.props.comment.id})
);
reject = () =>
this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({ commentId: this.props.comment.id });
render() {
const {
@@ -34,30 +33,35 @@ class UserDetailComment extends React.Component {
toggleSelect,
className,
data,
root: {settings: {wordlist: {banned, suspect}}},
root: { settings: { wordlist: { banned, suspect } } },
} = this.props;
return (
<li
tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
className={cn(className, styles.root, {
[styles.rootSelected]: selected,
})}
>
<div className={styles.container}>
<div className={styles.header}>
<input
className={styles.bulkSelectInput}
type='checkbox'
type="checkbox"
value={comment.id}
checked={selected}
onChange={(e) => toggleSelect(e.target.value, e.target.checked)} />
onChange={e => toggleSelect(e.target.value, e.target.checked)}
/>
<span className={styles.created}>
{timeago(comment.created_at)}
</span>
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
{comment.editing && comment.editing.edited ? (
<span>
&nbsp;<span className={styles.editedMarker}>
({t('comment.edited')})
</span>
</span>
) : null}
<div className={styles.labels}>
<CommentLabels comment={comment} />
@@ -65,17 +69,21 @@ class UserDetailComment extends React.Component {
</div>
<div className={styles.story}>
Story: {comment.asset.title}
{<Link to={`/admin/moderate/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
{
<Link to={`/admin/moderate/${comment.asset.id}`}>
{t('modqueue.moderate')}
</Link>
}
</div>
<CommentAnimatedEdit body={comment.body}>
<div className={styles.bodyContainer}>
<div className={styles.body}>
<CommentBodyHighlighter
<CommentFormatter
suspectWords={suspect}
bannedWords={banned}
body={comment.body}
className="talk-admin-user-detail-comment"
/>
{' '}
<a
className={styles.external}
href={`${comment.asset.url}?commentId=${comment.id}`}
@@ -106,17 +114,15 @@ class UserDetailComment extends React.Component {
</div>
</CommentAnimatedEdit>
</div>
<CommentDetails
data={data}
root={root}
comment={comment}
/>
<CommentDetails data={data} root={root} comment={comment} />
</li>
);
}
}
UserDetailComment.propTypes = {
selected: PropTypes.bool,
data: PropTypes.object,
user: PropTypes.object.isRequired,
viewUserDetail: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
@@ -140,7 +146,7 @@ UserDetailComment.propTypes = {
asset: PropTypes.shape({
title: PropTypes.string,
url: PropTypes.string,
id: PropTypes.string
id: PropTypes.string,
}),
}),
};
@@ -0,0 +1,56 @@
.commentList {
clear: both;
}
.loadMore > button {
background-color: #696969;
&:hover {
background-color: #404040;
color: white;
}
}
.bulkActionHeader {
display: flex;
justify-content: space-between;
height: 52px;
justify-content: flex-end;
&.selected {
background-color: #efefef;
}
}
.bulkActionGroup {
height: 52px;
background-color: #efefef;
padding: 0 0 0 10px;
display: flex;
flex: 1;
i {
margin-right: 0;
}
.bulkAction {
display: inline-block;
width: 48px;
height: 48px;
transform: scale(.7);
min-width: 0;
}
.bulkAction:last-child {
margin-left: -10px;
}
}
.selectedCommentsInfo {
align-self: center;
font-weight: 500;
margin-left: 15px;
}
.toggleAll {
padding-right: 14px;
align-self: center;
}
@@ -0,0 +1,106 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import styles from './UserDetailCommentList.css';
import LoadMore from '../components/LoadMore';
import Comment from '../containers/UserDetailComment';
import RejectButton from './RejectButton';
import ApproveButton from './ApproveButton';
const UserDetailCommentList = props => {
const {
data,
root,
root: { user, comments: { nodes, hasNextPage } },
acceptComment,
rejectComment,
selectedCommentIds,
toggleSelect,
viewUserDetail,
loadMore,
toggleSelectAll,
bulkAcceptThenReload,
bulkRejectThenReload,
} = props;
return (
<div
className={cn(styles.commentList, 'talk-admin-user-detail-comment-list')}
>
<div
className={
selectedCommentIds.length > 0
? cn(styles.bulkActionHeader, styles.selected)
: styles.bulkActionHeader
}
>
{selectedCommentIds.length > 0 && (
<div className={styles.bulkActionGroup}>
<ApproveButton onClick={bulkAcceptThenReload} minimal />
<RejectButton onClick={bulkRejectThenReload} minimal />
<span className={styles.selectedCommentsInfo}>
{' '}
{selectedCommentIds.length} comments selected
</span>
</div>
)}
<div className={styles.toggleAll}>
<input
type="checkbox"
id="toogleAll"
checked={
selectedCommentIds.length > 0 &&
selectedCommentIds.length === nodes.length
}
onChange={e => {
toggleSelectAll(
nodes.map(comment => comment.id),
e.target.checked
);
}}
/>
<label htmlFor="toogleAll">Select all</label>
</div>
</div>
{nodes.map(comment => {
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return (
<Comment
key={comment.id}
user={user}
root={root}
data={data}
comment={comment}
acceptComment={acceptComment}
rejectComment={rejectComment}
selected={selected}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
/>
);
})}
<LoadMore
className={styles.loadMore}
loadMore={loadMore}
showLoadMore={hasNextPage}
/>
</div>
);
};
UserDetailCommentList.propTypes = {
root: PropTypes.object.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
selectedCommentIds: PropTypes.array.isRequired,
viewUserDetail: PropTypes.any.isRequired,
loadMore: PropTypes.any.isRequired,
toggleSelect: PropTypes.func.isRequired,
toggleSelectAll: PropTypes.func.isRequired,
bulkAcceptThenReload: PropTypes.func.isRequired,
bulkRejectThenReload: PropTypes.func.isRequired,
};
export default UserDetailCommentList;
@@ -0,0 +1,69 @@
.userInfo {
position: relative;
display: inline-block;
}
.icon {
font-size: 16px;
color: #616161;
-ms-user-select:none;
-moz-user-select: none;
-webkit-user-select: none;
-webkit-touch-callout:none;
user-select: none;
-webkit-tap-highlight-color:rgba(0,0,0,0);
}
.icon:hover {
cursor: pointer;
}
.menu {
background-color: white;
border: solid 1px #999;
border-radius: 3px;
padding: 10px;
position: absolute;
-webkit-box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
z-index: 10;
top: 32px;
right: 0px;
width: 140px;
text-align: left;
color: #616161;
}
.menu::before{
content: '';
border: 10px solid transparent;
border-top-color: #999;
position: absolute;
right: 0px;
top: -20px;
transform: rotate(180deg);
}
.menu::after{
content: '';
border: 10px solid transparent;
border-top-color: white;
position: absolute;
right: 0px;
top: -19px;
transform: rotate(180deg);
}
.descriptionList {
padding: 0;
margin: 0;
list-style: none;
}
.strongItem {
margin-right: 3px;
}
.descriptionItem {
font-size: 0.9em;
}
@@ -0,0 +1,177 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import { Icon } from 'coral-ui';
import styles from './UserInfoTooltip.css';
import ClickOutside from 'coral-framework/components/ClickOutside';
import moment from 'moment';
const initialState = { menuVisible: false };
class UserInfoTooltip extends React.Component {
state = initialState;
toogleMenu = () => {
this.setState({ menuVisible: !this.state.menuVisible });
};
hideMenu = () => {
this.setState({ menuVisible: false });
};
getLastHistoryItem = (user, status = 'banned') => {
const userHistory = user.state.status[status].history;
return userHistory[userHistory.length - 1];
};
render() {
const { menuVisible } = this.state;
const { user, banned, suspended } = this.props;
return (
<ClickOutside onClickOutside={this.hideMenu}>
<div className={cn(styles.userInfo, 'talk-admin-user-info-tooltip')}>
<span
onClick={this.toogleMenu}
className={cn(styles.icon, 'talk-admin-user-info-tooltip-icon')}
>
<Icon name="info_outline" />
</span>
{menuVisible && (
<div
className={cn(styles.menu, 'talk-admin-user-info-tooltip-menu')}
>
{banned && (
<div
className={cn(
styles.description,
'talk-admin-user-info-tooltip-description-banned'
)}
>
<ul
className={cn(
styles.descriptionList,
'talk-admin-user-info-tooltip-description-list'
)}
>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>Banned On</strong>
<span>
{moment(
new Date(
this.getLastHistoryItem(user, 'banned').created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>By</strong>
<span>
{
this.getLastHistoryItem(user, 'banned').assigned_by
.username
}
</span>
</li>
</ul>
</div>
)}
{suspended && (
<div
className={cn(
styles.description,
'talk-admin-user-info-tooltip-description-suspended'
)}
>
<ul
className={cn(
styles.descriptionList,
'talk-admin-user-info-tooltip-description-list'
)}
>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>Suspension</strong>
<span />
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>By</strong>
<span>
{
this.getLastHistoryItem(user, 'suspension')
.assigned_by.username
}
</span>
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>Start</strong>
<span>
{moment(
new Date(
this.getLastHistoryItem(
user,
'suspension'
).created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>End</strong>
<span>
{moment(
new Date(
this.getLastHistoryItem(user, 'suspension').until
)
).format('MMMM Do YYYY, h:mm:ss a')}
</span>
</li>
</ul>
</div>
)}
</div>
)}
</div>
</ClickOutside>
);
}
}
UserInfoTooltip.propTypes = {
user: PropTypes.object,
banned: PropTypes.bool,
suspended: PropTypes.bool,
};
export default UserInfoTooltip;
@@ -1,111 +0,0 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
import {Link, IndexLink} from 'react-router';
import styles from './Header.css';
import t from 'coral-framework/services/i18n';
import {Logo} from './Logo';
import {can} from 'coral-framework/services/perms';
import Indicator from './Indicator';
const CoralHeader = ({
handleLogout,
showShortcuts = () => {},
auth,
root
}) => {
return (
<div className={styles.headerWrapper}>
<Header className={styles.header}>
<Logo className={styles.logo} />
<div>
{
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
<Navigation className={styles.nav}>
{
can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
id='moderateNav'
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}>
{t('configure.moderate')}
{(root.premodCount !== 0 || root.reportedCount !== 0) && <Indicator />}
</IndexLink>
)
}
<Link
id='storiesNav'
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}>
{t('configure.stories')}
</Link>
<Link
id='communityNav'
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}>
{t('configure.community')}
{root.flaggedUsernamesCount !== 0 && <Indicator />}
</Link>
{
can(auth.user, 'UPDATE_CONFIG') && (
<Link
id='configureNav'
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}>
{t('configure.configure')}
</Link>
)
}
</Navigation>
:
null
}
<div className={styles.rightPanel}>
<ul>
<li className={cn(styles.settings, 'talk-admin-header-settings')}>
<div>
<IconButton name="settings" id="menu-settings" className="talk-admin-header-settings-button"/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
<MenuItem>
<a href="https://github.com/coralproject/talk/releases" target="_blank" rel="noopener noreferrer">
View latest version
</a>
</MenuItem>
<MenuItem>
<a href="https://support.coralproject.net" target="_blank" rel="noopener noreferrer">
Report a bug or give feedback
</a>
</MenuItem>
<MenuItem onClick={handleLogout} className="talk-admin-header-sign-out">
{t('configure.sign_out')}
</MenuItem>
</Menu>
</div>
</li>
<li>
{`v${process.env.VERSION}`}
</li>
</ul>
</div>
</div>
</Header>
</div>
);
};
CoralHeader.propTypes = {
auth: PropTypes.object,
showShortcuts: PropTypes.func,
handleLogout: PropTypes.func.isRequired,
root: PropTypes.object.isRequired
};
export default CoralHeader;
+2 -1
View File
@@ -1 +1,2 @@
export const ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS = 'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS';
export const ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS =
'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS';
+4 -2
View File
@@ -2,8 +2,10 @@ export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
export const SHOW_SUSPENDUSER_DIALOG = 'SHOW_SUSPENDUSER_DIALOG';
export const HIDE_SUSPENDUSER_DIALOG = 'HIDE_SUSPENDUSER_DIALOG';
export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST = 'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST';
export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS = 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS';
export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST =
'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST';
export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS =
'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS';
export const COMMENT_CREATE_SUCCESS = 'COMMENT_CREATE_SUCCESS';
export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED';
export const COMMENT_STREAM_FETCH_SUCCESS = 'COMMENT_STREAM_FETCH_SUCCESS';
@@ -6,8 +6,6 @@ export const FETCH_USERS_FAILURE = `${prefix}_FETCH_USERS_FAILURE`;
export const SORT_UPDATE = `${prefix}_SORT_UPDATE`;
export const SET_PAGE = `${prefix}_SET_PAGE`;
export const SET_ROLE = `${prefix}_SET_ROLE`;
export const SET_COMMENTER_STATUS = `${prefix}_SET_COMMENTER_STATUS`;
export const FETCH_FLAGGED_COMMENTERS_REQUEST = `${prefix}_FETCH_FLAGGED_COMMENTERS_REQUEST`;
export const FETCH_FLAGGED_COMMENTERS_SUCCESS = `${prefix}_FETCH_FLAGGED_COMMENTERS_SUCCESS`;
@@ -20,3 +18,5 @@ export const SHOW_REJECT_USERNAME_DIALOG = `${prefix}_SHOW_REJECT_USERNAME_DIALO
export const HIDE_REJECT_USERNAME_DIALOG = `${prefix}_HIDE_REJECT_USERNAME_DIALOG`;
export const SET_SEARCH_VALUE = `${prefix}_SET_SEARCH_VALUE`;
export const SET_INDICATOR_TRACK = `${prefix}_SET_INDICATOR_TRACK`;
+4 -3
View File
@@ -2,15 +2,16 @@ export const NEXT_STEP = 'NEXT_STEP';
export const GO_TO_STEP = 'GO_TO_STEP';
export const PREVIOUS_STEP = 'PREVIOUS_STEP';
export const ADD_ERROR = 'ADD_ERROR';
export const HAS_ERROR = 'HAS_ERROR';
export const SHOW_ERRORS = 'SHOW_ERRORS';
export const HAS_ERROR = 'HAS_ERROR';
export const SHOW_ERRORS = 'SHOW_ERRORS';
export const CLEAR_ERRORS = 'CLEAR_ERRORS';
export const INSTALL_REQUEST = 'INSTALL_REQUEST';
export const INSTALL_SUCCESS = 'INSTALL_SUCCESS';
export const INSTALL_FAILURE = 'INSTALL_FAILURE';
export const UPDATE_FORMDATA_USER = 'UPDATE_FORMDATA_USER';
export const UPDATE_FORMDATA_SETTINGS = 'UPDATE_FORMDATA_SETTINGS';
export const UPDATE_PERMITTED_DOMAINS_SETTINGS = 'UPDATE_PERMITTED_DOMAINS_SETTINGS';
export const UPDATE_PERMITTED_DOMAINS_SETTINGS =
'UPDATE_PERMITTED_DOMAINS_SETTINGS';
export const CHECK_INSTALL_REQUEST = 'CHECK_INSTALL_REQUEST';
export const CHECK_INSTALL_SUCCESS = 'CHECK_INSTALL_SUCCESS';
+12 -9
View File
@@ -1,9 +1,12 @@
export const TOGGLE_MODAL = 'TOGGLE_MODAL';
export const SINGLE_VIEW = 'SINGLE_VIEW';
export const HIDE_SHORTCUTS_NOTE = 'HIDE_SHORTCUTS_NOTE';
export const SET_SORT_ORDER = 'MODERATION_SET_SORT_ORDER';
export const SHOW_STORY_SEARCH = 'SHOW_STORY_SEARCH';
export const HIDE_STORY_SEARCH = 'HIDE_STORY_SEARCH';
export const STORY_SEARCH_CHANGE_VALUE = 'STORY_SEARCH_CHANGE_VALUE';
export const MODERATION_CLEAR_STATE = 'MODERATION_CLEAR_STATE';
export const MODERATION_SELECT_COMMENT = 'MODERATION_SELECT_COMMENT';
const prefix = `MODERATION`;
export const TOGGLE_MODAL = `${prefix}_TOGGLE_MODAL`;
export const SINGLE_VIEW = `${prefix}_SINGLE_VIEW`;
export const HIDE_SHORTCUTS_NOTE = `${prefix}_HIDE_SHORTCUTS_NOTE`;
export const SET_SORT_ORDER = `${prefix}_SET_SORT_ORDER`;
export const SHOW_STORY_SEARCH = `${prefix}_SHOW_STORY_SEARCH`;
export const HIDE_STORY_SEARCH = `${prefix}_HIDE_STORY_SEARCH`;
export const STORY_SEARCH_CHANGE_VALUE = `${prefix}_STORY_SEARCH_CHANGE_VALUE`;
export const CLEAR_STATE = `${prefix}_CLEAR_STATE`;
export const SELECT_COMMENT = `${prefix}_SELECT_COMMENT`;
export const SET_INDICATOR_TRACK = `${prefix}_SET_INDICATOR_TRACK`;
@@ -1,6 +1,6 @@
export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL';
export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL';
export const CHANGE_USER_DETAIL_STATUSES = 'CHANGE_USER_DETAIL_STATUSES';
export const CHANGE_TAB_USER_DETAIL = 'CHANGE_TAB_USER_DETAIL';
export const SELECT_USER_DETAIL_COMMENT = 'SELECT_USER_DETAIL_COMMENT';
export const UNSELECT_USER_DETAIL_COMMENT = 'UNSELECT_USER_DETAIL_COMMENT';
export const CLEAR_USER_DETAIL_SELECTIONS = 'CLEAR_USER_DETAIL_SELECTIONS';
@@ -1,29 +1,32 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import BanUserDialog from '../components/BanUserDialog';
import {hideBanUserDialog} from '../actions/banUserDialog';
import {withSetUserStatus, withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {compose} from 'react-apollo';
import { hideBanUserDialog } from '../actions/banUserDialog';
import {
withBanUser,
withSetCommentStatus,
} from 'coral-framework/graphql/mutations';
import { compose } from 'react-apollo';
import t from 'coral-framework/services/i18n';
import {getErrorMessages} from 'coral-framework/utils';
import {notify} from 'coral-framework/actions/notification';
class BanUserDialogContainer extends Component {
banUser = async () => {
const {userId, commentId, commentStatus, setUserStatus, setCommentStatus, hideBanUserDialog, notify} = this.props;
try {
await setUserStatus({userId, status: 'BANNED'});
hideBanUserDialog();
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({commentId, status: 'REJECTED'});
}
const {
userId,
commentId,
commentStatus,
banUser,
setCommentStatus,
hideBanUserDialog,
} = this.props;
await banUser({ id: userId, message: '' });
hideBanUserDialog();
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({ commentId, status: 'REJECTED' });
}
catch(err) {
notify('error', getErrorMessages(err));
}
}
};
getInfo() {
let note = t('bandialog.note_ban_user');
@@ -46,7 +49,17 @@ class BanUserDialogContainer extends Component {
}
}
const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, commentStatus}}) => ({
BanUserDialogContainer.propTypes = {
banUser: PropTypes.func.isRequired,
hideBanUserDialog: PropTypes.func,
open: PropTypes.bool,
username: PropTypes.string,
commentStatus: PropTypes.string,
};
const mapStateToProps = ({
banUserDialog: { open, userId, username, commentId, commentStatus },
}) => ({
open,
userId,
username,
@@ -54,18 +67,17 @@ const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, com
commentStatus,
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
hideBanUserDialog,
notify,
}, dispatch),
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
hideBanUserDialog,
},
dispatch
),
});
export default compose(
withSetUserStatus,
withSetCommentStatus,
connect(
mapStateToProps,
mapDispatchToProps,
),
connect(mapStateToProps, mapDispatchToProps),
withBanUser,
withSetCommentStatus
)(BanUserDialogContainer);
@@ -1,12 +1,9 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import CommentDetails from '../components/CommentDetails';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import withFragments from 'coral-framework/hocs/withFragments';
const slots = [
'adminCommentDetailArea',
'adminCommentMoreDetails',
];
const slots = ['adminCommentDetailArea', 'adminCommentMoreDetails'];
export default withFragments({
root: gql`
@@ -20,5 +17,5 @@ export default withFragments({
__typename
${getSlotFragmentSpreads(slots, 'comment')}
}
`
`,
})(CommentDetails);
@@ -1,11 +1,9 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import CommentLabels from '../components/CommentLabels';
import withFragments from 'coral-framework/hocs/withFragments';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
const slots = [
'adminCommentLabels',
];
const slots = ['adminCommentLabels'];
export default withFragments({
root: gql`
@@ -25,10 +23,11 @@ export default withFragments({
}
user {
id
roles
role
}
created_at
}
${getSlotFragmentSpreads(slots, 'comment')}
}
`
`,
})(CommentLabels);
+19 -21
View File
@@ -1,24 +1,22 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import Header from '../components/ui/Header';
import Header from '../components/Header';
import CommunityIndicator from '../routes/Community/containers/Indicator';
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
import { getDefinitionName } from 'coral-framework/utils';
export default withQuery(gql`
query TalkAdmin_Header {
__typename
premodCount: commentCount(query: {
statuses: [PREMOD]
})
reportedCount: commentCount(query: {
statuses: [NONE, PREMOD, SYSTEM_WITHHELD],
action_type: FLAG
})
flaggedUsernamesCount: userCount(query: {
action_type: FLAG,
statuses: [PENDING]
})
}
`, {
options: {
pollInterval: 10000
export default withQuery(
gql`
query TalkAdmin_Header($nullID: ID) {
...${getDefinitionName(ModerationIndicator.fragments.root)}
...${getDefinitionName(CommunityIndicator.fragments.root)}
}
})(Header);
${ModerationIndicator.fragments.root}
${CommunityIndicator.fragments.root}
`,
{
options: {
variables: { nullID: null },
},
}
)(Header);
+36 -25
View File
@@ -1,35 +1,39 @@
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import Layout from '../components/ui/Layout';
import {fetchConfig} from '../actions/config';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Layout from '../components/Layout';
import { fetchConfig } from '../actions/config';
import AdminLogin from '../components/AdminLogin';
import {FullLoading} from '../components/FullLoading';
import { FullLoading } from '../components/FullLoading';
import BanUserDialog from './BanUserDialog';
import SuspendUserDialog from './SuspendUserDialog';
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
import {checkLogin, handleLogin, requestPasswordReset, logout} from '../actions/auth';
import {can} from 'coral-framework/services/perms';
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
import {
checkLogin,
handleLogin,
requestPasswordReset,
logout,
} from '../actions/auth';
import { can } from 'coral-framework/services/perms';
import UserDetail from 'coral-admin/src/containers/UserDetail';
import PropTypes from 'prop-types';
class LayoutContainer extends React.Component {
componentWillMount() {
const {checkLogin, fetchConfig} = this.props;
const { checkLogin, fetchConfig } = this.props;
checkLogin();
fetchConfig();
}
render() {
const {
user,
loggedIn,
loadingUser,
loginError,
loginMaxExceeded,
passwordRequestSuccess
passwordRequestSuccess,
} = this.props.auth;
const {
@@ -61,7 +65,8 @@ class LayoutContainer extends React.Component {
<Layout
handleLogout={logout}
toggleShortcutModal={toggleShortcutModal}
auth={this.props.auth} >
auth={this.props.auth}
>
<BanUserDialog />
<SuspendUserDialog />
<UserDetail />
@@ -71,7 +76,10 @@ class LayoutContainer extends React.Component {
} else if (loggedIn) {
return (
<Layout {...this.props}>
<p>This page is for team use only. Please contact an administrator if you want to join this team.</p>
<p>
This page is for team use only. Please contact an administrator if
you want to join this team.
</p>
</Layout>
);
}
@@ -89,22 +97,25 @@ LayoutContainer.propTypes = {
toggleShortcutModal: PropTypes.func,
TALK_RECAPTCHA_PUBLIC: PropTypes.string,
checkLogin: PropTypes.func,
fetchConfig: PropTypes.func
fetchConfig: PropTypes.func,
};
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
auth: state.auth,
TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
checkLogin,
fetchConfig,
handleLogin,
requestPasswordReset,
toggleShortcutModal,
logout
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
checkLogin,
fetchConfig,
handleLogin,
requestPasswordReset,
toggleShortcutModal,
logout,
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
@@ -1,34 +1,39 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import SuspendUserDialog from '../components/SuspendUserDialog';
import {hideSuspendUserDialog} from '../actions/suspendUserDialog';
import {withSetCommentStatus, withSuspendUser} from 'coral-framework/graphql/mutations';
import {compose, gql} from 'react-apollo';
import t, {timeago} from 'coral-framework/services/i18n';
import { hideSuspendUserDialog } from '../actions/suspendUserDialog';
import {
withSetCommentStatus,
withSuspendUser,
} from 'coral-framework/graphql/mutations';
import { compose, gql } from 'react-apollo';
import t, { timeago } from 'coral-framework/services/i18n';
import withQuery from 'coral-framework/hocs/withQuery';
import {getErrorMessages} from 'coral-framework/utils';
import get from 'lodash/get';
import {notify} from 'coral-framework/actions/notification';
import { notify } from 'coral-framework/actions/notification';
class SuspendUserDialogContainer extends Component {
suspendUser = async ({message, until}) => {
const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser, notify} = this.props;
suspendUser = async ({ message, until }) => {
const {
userId,
username,
commentStatus,
commentId,
hideSuspendUserDialog,
setCommentStatus,
suspendUser,
notify,
} = this.props;
hideSuspendUserDialog();
try {
await suspendUser({id: userId, message, until});
notify(
'success',
t('suspenduser.notify_suspend_until', username, timeago(until)),
);
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({commentId, status: 'REJECTED'});
}
}
catch(err) {
notify('error', getErrorMessages(err));
await suspendUser({ id: userId, message, until });
notify(
'success',
t('suspenduser.notify_suspend_until', username, timeago(until))
);
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({ commentId, status: 'REJECTED' });
}
};
@@ -53,14 +58,16 @@ SuspendUserDialogContainer.propTypes = {
const withOrganizationName = withQuery(gql`
query CoralAdmin_SuspendUserDialog {
__typename
settings {
organizationName
__typename
settings {
organizationName
}
}
`);
const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId, commentStatus}}) => ({
const mapStateToProps = ({
suspendUserDialog: { open, userId, username, commentId, commentStatus },
}) => ({
open,
userId,
username,
@@ -68,19 +75,19 @@ const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId,
commentStatus,
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
hideSuspendUserDialog,
notify,
}, dispatch),
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
hideSuspendUserDialog,
notify,
},
dispatch
),
});
export default compose(
connect(
mapStateToProps,
mapDispatchToProps,
),
connect(mapStateToProps, mapDispatchToProps),
withSuspendUser,
withSetCommentStatus,
withOrganizationName,
withOrganizationName
)(SuspendUserDialogContainer);
+160 -73
View File
@@ -1,22 +1,31 @@
import React from 'react';
import {compose, gql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import PropTypes from 'prop-types';
import { compose, gql } from 'react-apollo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import UserDetail from '../components/UserDetail';
import withQuery from 'coral-framework/hocs/withQuery';
import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils';
import {
getDefinitionName,
getSlotFragmentSpreads,
} from 'coral-framework/utils';
import {
viewUserDetail,
hideUserDetail,
changeUserDetailStatuses,
changeTab,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
toggleSelectAllCommentInUserDetail
toggleSelectAllCommentInUserDetail,
} from 'coral-admin/src/actions/userDetail';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {
withSetCommentStatus,
withUnbanUser,
withUnsuspendUser,
} from 'coral-framework/graphql/mutations';
import UserDetailComment from './UserDetailComment';
import update from 'immutability-helper';
import {notify} from 'coral-framework/actions/notification';
import { showBanUserDialog } from 'actions/banUserDialog';
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
const commentConnectionFragment = gql`
fragment CoralAdmin_UserDetail_CommentConnection on CommentConnection {
@@ -30,38 +39,36 @@ const commentConnectionFragment = gql`
${UserDetailComment.fragments.comment}
`;
const slots = [
'userProfile',
];
const slots = ['userProfile'];
class UserDetailContainer extends React.Component {
isLoadingMore = false;
// status can be 'ACCEPTED' or 'REJECTED'
bulkSetCommentStatus = async (status) => {
const changes = this.props.selectedCommentIds.map((commentId) => {
return this.props.setCommentStatus({commentId, status});
bulkSetCommentStatus = async status => {
const changes = this.props.selectedCommentIds.map(commentId => {
return this.props.setCommentStatus({ commentId, status });
});
await Promise.all(changes);
this.props.clearUserDetailSelections(); // un-select everything
}
};
bulkReject = () => {
return this.bulkSetCommentStatus('REJECTED');
}
};
bulkAccept = () => {
return this.bulkSetCommentStatus('ACCEPTED');
}
};
acceptComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'ACCEPTED'});
}
acceptComment = ({ commentId }) => {
return this.props.setCommentStatus({ commentId, status: 'ACCEPTED' });
};
rejectComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
}
rejectComment = ({ commentId }) => {
return this.props.setCommentStatus({ commentId, status: 'REJECTED' });
};
loadMore = () => {
if (this.isLoadingMore) {
@@ -75,24 +82,25 @@ class UserDetailContainer extends React.Component {
author_id: this.props.data.variables.author_id,
statuses: this.props.data.variables.statuses,
};
this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables,
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
return update(prev, {
comments: {
nodes: {$push: comments.nodes},
hasNextPage: {$set: comments.hasNextPage},
startCursor: {$set: comments.startCursor},
endCursor: {$set: comments.endCursor},
},
});
}
})
this.props.data
.fetchMore({
query: LOAD_MORE_QUERY,
variables,
updateQuery: (prev, { fetchMoreResult: { comments } }) => {
return update(prev, {
comments: {
nodes: { $push: comments.nodes },
hasNextPage: { $set: comments.hasNextPage },
startCursor: { $set: comments.startCursor },
endCursor: { $set: comments.endCursor },
},
});
},
})
.then(() => {
this.isLoadingMore = false;
})
.catch((err) => {
.catch(err => {
this.isLoadingMore = false;
throw err;
});
@@ -104,37 +112,67 @@ class UserDetailContainer extends React.Component {
}
}
render () {
render() {
if (!this.props.userId) {
return null;
}
const loading = this.props.data.loading;
return <UserDetail
bulkReject={this.bulkReject}
bulkAccept={this.bulkAccept}
changeStatus={this.props.changeUserDetailStatuses}
toggleSelect={this.props.toggleSelectCommentInUserDetail}
toggleSelectAll={this.props.toggleSelectAllCommentInUserDetail}
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
loading={loading}
loadMore={this.loadMore}
{...this.props} />;
return (
<UserDetail
bulkReject={this.bulkReject}
bulkAccept={this.bulkAccept}
changeTab={this.props.changeTab}
toggleSelect={this.props.toggleSelectCommentInUserDetail}
toggleSelectAll={this.props.toggleSelectAllCommentInUserDetail}
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
loading={loading}
error={this.props.data && this.props.data.error}
loadMore={this.loadMore}
{...this.props}
/>
);
}
}
UserDetailContainer.propTypes = {
changeTab: PropTypes.func,
toggleSelectCommentInUserDetail: PropTypes.func,
toggleSelectAllCommentInUserDetail: PropTypes.func,
data: PropTypes.object,
root: PropTypes.object,
setCommentStatus: PropTypes.func,
clearUserDetailSelections: PropTypes.func,
selectedCommentIds: PropTypes.array,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
};
const LOAD_MORE_QUERY = gql`
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
comments(query: {limit: $limit, cursor: $cursor, author_id: $author_id, statuses: $statuses}) {
query CoralAdmin_Moderation_LoadMore(
$limit: Int = 10
$cursor: Cursor
$author_id: ID!
$statuses: [COMMENT_STATUS!]
) {
comments(
query: {
limit: $limit
cursor: $cursor
author_id: $author_id
statuses: $statuses
}
) {
...CoralAdmin_UserDetail_CommentConnection
}
}
${commentConnectionFragment}
`;
export const withUserDetailQuery = withQuery(gql`
export const withUserDetailQuery = withQuery(
gql`
query CoralAdmin_UserDetail($author_id: ID!, $statuses: [COMMENT_STATUS!]) {
user(id: $author_id) {
id
@@ -147,8 +185,48 @@ export const withUserDetailQuery = withQuery(gql`
reliable {
flagger
}
state {
status {
suspension {
until
history {
until
created_at
assigned_by {
id
username
}
}
}
banned {
status
history {
status
assigned_by {
id
username
}
created_at
}
}
username {
status
history {
status
assigned_by {
id
username
}
created_at
}
}
}
}
${getSlotFragmentSpreads(slots, 'user')}
}
me {
id
}
totalComments: commentCount(query: {author_id: $author_id, statuses: []})
rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]})
comments: comments(query: {
@@ -162,37 +240,46 @@ export const withUserDetailQuery = withQuery(gql`
}
${UserDetailComment.fragments.root}
${commentConnectionFragment}
`, {
options: ({userId, statuses}) => {
return {
variables: {author_id: userId, statuses},
fetchPolicy: 'network-only',
};
},
skip: (ownProps) => !ownProps.userId,
});
`,
{
options: ({ userId, statuses }) => {
return {
variables: { author_id: userId, statuses },
fetchPolicy: 'network-only',
};
},
skip: ownProps => !ownProps.userId,
}
);
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
userId: state.userDetail.userId,
selectedCommentIds: state.userDetail.selectedCommentIds,
statuses: state.userDetail.statuses,
activeTab: state.userDetail.activeTab,
modal: state.ui.modal,
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
changeUserDetailStatuses,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
viewUserDetail,
hideUserDetail,
toggleSelectAllCommentInUserDetail,
notify
}, dispatch)
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
showBanUserDialog,
showSuspendUserDialog,
changeTab,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
viewUserDetail,
hideUserDetail,
toggleSelectAllCommentInUserDetail,
},
dispatch
),
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withUserDetailQuery,
withSetCommentStatus,
withUnbanUser,
withUnsuspendUser
)(UserDetailContainer);

Some files were not shown because too many files have changed in this diff Show More