From fe279587b59df8a63856b1ec3b357e16a16ad7e5 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 10 May 2017 09:25:39 -0600 Subject: [PATCH 01/34] add staff to dropdown --- client/coral-admin/src/containers/Community/Table.js | 1 + models/user.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/client/coral-admin/src/containers/Community/Table.js b/client/coral-admin/src/containers/Community/Table.js index aacc9c82f..2924e7fd3 100644 --- a/client/coral-admin/src/containers/Community/Table.js +++ b/client/coral-admin/src/containers/Community/Table.js @@ -65,6 +65,7 @@ class Table extends Component { label={lang.t('community.role')} onChange={role => this.onRoleChange(row.id, role)}> + diff --git a/models/user.js b/models/user.js index 458ac68f0..ec215b3fa 100644 --- a/models/user.js +++ b/models/user.js @@ -5,7 +5,8 @@ const uuid = require('uuid'); // USER_ROLES is the array of roles that is permissible as a user role. const USER_ROLES = [ 'ADMIN', - 'MODERATOR' + 'MODERATOR', + 'STAFF' ]; // USER_STATUS is the list of statuses that are permitted for the user status. From 14b483d0894a07ca16b37da71158194f74e93be9 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Thu, 11 May 2017 14:23:02 -0600 Subject: [PATCH 02/34] create roles helper --- client/coral-admin/src/actions/auth.js | 8 +++----- client/coral-admin/src/containers/LayoutContainer.js | 7 ++++--- client/coral-admin/src/reducers/auth.js | 2 -- client/coral-embed-stream/src/components/Embed.js | 5 +++-- client/coral-embed-stream/src/components/Stream.js | 5 +++-- client/coral-framework/actions/auth.js | 7 +++---- client/coral-framework/reducers/auth.js | 6 +----- client/coral-framework/utils/roles.js | 10 ++++++++++ services/users.js | 11 ++++------- 9 files changed, 31 insertions(+), 30 deletions(-) create mode 100644 client/coral-framework/utils/roles.js diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 89b56d4dc..68da3577e 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -14,8 +14,7 @@ export const handleLogin = (email, password, recaptchaResponse) => dispatch => { return dispatch(checkLoginFailure('not logged in')); } - const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length; - dispatch(checkLoginSuccess(user, isAdmin)); + dispatch(checkLoginSuccess(user)); }) .catch(error => { @@ -41,7 +40,7 @@ export const requestPasswordReset = email => dispatch => { // Check Login const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST}); -const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin}); +const checkLoginSuccess = (user) => ({type: actions.CHECK_LOGIN_SUCCESS, user}); const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); export const checkLogin = () => dispatch => { @@ -52,8 +51,7 @@ export const checkLogin = () => dispatch => { return dispatch(checkLoginFailure('not logged in')); } - const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length; - dispatch(checkLoginSuccess(user, isAdmin)); + dispatch(checkLoginSuccess(user)); }) .catch(error => { console.error(error); diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js index 42e2baade..fea730c98 100644 --- a/client/coral-admin/src/containers/LayoutContainer.js +++ b/client/coral-admin/src/containers/LayoutContainer.js @@ -6,6 +6,7 @@ import {toggleModal as toggleShortcutModal} from '../actions/moderation'; import {fetchConfig} from '../actions/config'; import {FullLoading} from '../components/FullLoading'; import AdminLogin from '../components/AdminLogin'; +import roleUtils from 'coral-framework/utils/roles'; class LayoutContainer extends Component { componentWillMount () { @@ -16,7 +17,7 @@ class LayoutContainer extends Component { } render () { const { - isAdmin, + user, loggedIn, loadingUser, loginError, @@ -26,7 +27,7 @@ class LayoutContainer extends Component { const {handleLogout, toggleShortcutModal, TALK_RECAPTCHA_PUBLIC} = this.props; if (loadingUser) { return ; } - if (!isAdmin) { + if (roleUtils.canAccessAdmin(user)) { return ; } - if (isAdmin && loggedIn) { + if (roleUtils.canAccessAdmin(user) && loggedIn) { return ; } return ; diff --git a/client/coral-admin/src/reducers/auth.js b/client/coral-admin/src/reducers/auth.js index a7054ddfa..2bd42d112 100644 --- a/client/coral-admin/src/reducers/auth.js +++ b/client/coral-admin/src/reducers/auth.js @@ -4,7 +4,6 @@ import * as actions from '../constants/auth'; const initialState = Map({ loggedIn: false, user: null, - isAdmin: false, loginError: null, loginMaxExceeded: false, passwordRequestSuccess: null @@ -24,7 +23,6 @@ export default function auth (state = initialState, action) { return state .set('loggedIn', true) .set('loadingUser', false) - .set('isAdmin', action.isAdmin) .set('user', action.user); case actions.LOGOUT_SUCCESS: return initialState; diff --git a/client/coral-embed-stream/src/components/Embed.js b/client/coral-embed-stream/src/components/Embed.js index 0d26cf740..908fd4a90 100644 --- a/client/coral-embed-stream/src/components/Embed.js +++ b/client/coral-embed-stream/src/components/Embed.js @@ -1,6 +1,7 @@ import React from 'react'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from 'coral-framework/translations'; +import rolesHelper from 'coral-framework/utils/roles'; const lang = new I18n(translations); import {TabBar, Tab, TabContent, Button} from 'coral-ui'; @@ -38,7 +39,7 @@ export default class Embed extends React.Component { render () { const {activeTab, logout, viewAllComments, commentId} = this.props; const {asset: {totalCommentCount}} = this.props.root; - const {loggedIn, isAdmin, user} = this.props.auth; + const {loggedIn, user} = this.props.auth; const userBox = ; @@ -48,7 +49,7 @@ export default class Embed extends React.Component { {lang.t('myProfile')} - Configure Stream + Configure Stream { commentId && diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 46ac75dca..7efadc921 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -11,6 +11,7 @@ import QuestionBox from 'coral-plugin-questionbox/QuestionBox'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; import SuspendedAccount from 'coral-framework/components/SuspendedAccount'; import RestrictedContent from 'coral-framework/components/RestrictedContent'; +import rolesHelper from 'coral-framework/utils/roles'; import ChangeUsernameContainer from 'coral-sign-in/containers/ChangeUsernameContainer'; @@ -37,7 +38,7 @@ class Stream extends React.Component { removeCommentTag, pluginProps, ignoreUser, - auth: {loggedIn, isAdmin, user}, + auth: {loggedIn, user}, commentCountCache, editName } = this.props; @@ -111,7 +112,7 @@ class Stream extends React.Component { {loggedIn && user && } - {loggedIn && } + {loggedIn && } {/* the highlightedComment is isolated after the user followed a permalink */} {highlightedComment diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index d689aeb63..527369b60 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -108,7 +108,7 @@ export const cleanState = () => ({type: actions.CLEAN_STATE}); const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST}); // TODO: revisit login redux flow. -// const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin}); +// const signInSuccess = (user) => ({type: actions.FETCH_SIGNIN_SUCCESS, user}); // const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error}); @@ -236,7 +236,7 @@ export const invalidForm = error => ({type: actions.INVALID_FORM, error}); // Check Login const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST}); -const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin}); +const checkLoginSuccess = (user) => ({type: actions.CHECK_LOGIN_SUCCESS, user}); const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error}); export const checkLogin = () => dispatch => { @@ -247,8 +247,7 @@ export const checkLogin = () => dispatch => { throw new Error('Not logged in'); } - const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length; - dispatch(checkLoginSuccess(result.user, isAdmin)); + dispatch(checkLoginSuccess(result.user)); }) .catch(error => { console.error(error); diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 83ea49ce5..5ac862431 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -4,7 +4,6 @@ import * as actions from '../constants/auth'; const initialState = Map({ isLoading: false, loggedIn: false, - isAdmin: false, user: null, showSignInDialog: false, showCreateUsernameDialog: false, @@ -79,12 +78,10 @@ export default function auth (state = initialState, action) { return state .set('checkedInitialLogin', true) .set('loggedIn', true) - .set('isAdmin', action.isAdmin) .set('user', purge(action.user)); case actions.FETCH_SIGNIN_SUCCESS: return state .set('loggedIn', true) - .set('isAdmin', action.isAdmin) .set('user', purge(action.user)); case actions.FETCH_SIGNIN_FAILURE: return state @@ -120,8 +117,7 @@ export default function auth (state = initialState, action) { return state .set('user', null) .set('isLoading', false) - .set('loggedIn', false) - .set('isAdmin', false); + .set('loggedIn', false); case actions.INVALID_FORM: return state .set('error', action.error); diff --git a/client/coral-framework/utils/roles.js b/client/coral-framework/utils/roles.js new file mode 100644 index 000000000..b457ff1b3 --- /dev/null +++ b/client/coral-framework/utils/roles.js @@ -0,0 +1,10 @@ +import includes from 'lodash/includes'; + +export default { + canAccessConfig: (user) => includes(user.roles, 'ADMIN'), + canChangeRoles: user => includes(user.roles, 'ADMIN'), + hasStaffTag: user => includes(user.roles, 'ADMIN', 'MODERATOR', 'STAFF'), + canViewUserEmails: user => includes(user.roles, 'ADMIN'), + canModerate: user => includes(user.roles, 'ADMIN', 'MODERATOR'), + canAccessAdmin: user => includes(user.roles, 'ADMIN', 'MODERATOR') +}; diff --git a/services/users.js b/services/users.js index 14301ccf4..246d3fded 100644 --- a/services/users.js +++ b/services/users.js @@ -395,13 +395,10 @@ module.exports = class UsersService { return Promise.reject(new Error(`role ${role} is not supported`)); } - return UserModel.update({ - id: id - }, { - $addToSet: { - roles: role - } - }); + // 5.11.2017 - Restricting this to a hierarchical system like WordPress + // where you can only set one role at a time. + // I'm not changing the data structure here, because I don't want a migration + return UserModel.update({id}, {$set: {roles: [role]}}); } /** From e32ab99edf17a85a41c7e1bc7e4f2cfabb3b83d6 Mon Sep 17 00:00:00 2001 From: riley Date: Fri, 12 May 2017 09:57:07 -0600 Subject: [PATCH 03/34] remove hasRoles --- .../src/containers/LayoutContainer.js | 2 +- graph/loaders/comments.js | 6 +- graph/mutators/comment.js | 2 +- graph/resolvers/comment.js | 7 +- graph/resolvers/root_query.js | 12 +-- graph/resolvers/user.js | 6 +- models/user.js | 78 ++++++++++++++++--- test/server/graph/mutations/addCommentTag.js | 1 + 8 files changed, 87 insertions(+), 27 deletions(-) diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js index fea730c98..8a3e0b0d2 100644 --- a/client/coral-admin/src/containers/LayoutContainer.js +++ b/client/coral-admin/src/containers/LayoutContainer.js @@ -27,7 +27,7 @@ class LayoutContainer extends Component { const {handleLogout, toggleShortcutModal, TALK_RECAPTCHA_PUBLIC} = this.props; if (loadingUser) { return ; } - if (roleUtils.canAccessAdmin(user)) { + if (!loggedIn) { return { */ const genComments = ({user}, ids) => { let comments; - if (user && user.hasRoles('ADMIN')) { + if (user && user.canViewOthersComments()) { comments = CommentModel.find({ id: { $in: ids diff --git a/graph/mutators/comment.js b/graph/mutators/comment.js index e2f2d3bac..9557c1dcf 100644 --- a/graph/mutators/comment.js +++ b/graph/mutators/comment.js @@ -22,7 +22,7 @@ const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, par tags = tags.map(tag => ({name: tag})); // If admin or moderator, adding STAFF tag - if (user.hasRoles('ADMIN') || user.hasRoles('MODERATOR')) { + if (user.isStaff()) { tags.push({name: 'STAFF'}); } diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 19ea11efe..fb01a1585 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -23,14 +23,13 @@ const Comment = { }, replyCount({id}, {excludeIgnored}, {user, loaders: {Comments}}) { if (user && excludeIgnored) { - return Comments.countByParentIDPersonalized({id, excludeIgnored}); + return Comments.countByParentIDPersonalized({id, excludeIgnored}); } - return Comments.countByParentID.load(id); + return Comments.countByParentID.load(id); }, actions({id}, _, {user, loaders: {Actions}}) { - // Only return the actions if the user is not an admin. - if (user && user.hasRoles('ADMIN')) { + if (user && user.canViewActions()) { return Actions.getByID.load(id); } diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 4dcb7ea11..9deaba5f6 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -1,6 +1,6 @@ const RootQuery = { assets(_, args, {loaders: {Assets}, user}) { - if (user == null || !user.hasRoles('ADMIN')) { + if (user == null || !user.canQueryAssets()) { return null; } @@ -22,7 +22,7 @@ const RootQuery = { comments(_, {query: {action_type, statuses, asset_id, parent_id, limit, cursor, sort, excludeIgnored}}, {user, loaders: {Comments, Actions}}) { let query = {statuses, asset_id, parent_id, limit, cursor, sort, excludeIgnored}; - if (user != null && user.hasRoles('ADMIN') && action_type) { + if (user != null && user.canViewOthersComments() && action_type) { return Actions.getByTypes({action_type, item_type: 'COMMENTS'}) .then((ids) => { @@ -37,7 +37,7 @@ const RootQuery = { return Comments.get.load(id); }, commentCount(_, {query: {action_type, statuses, asset_id, parent_id}}, {user, loaders: {Actions, Comments}}) { - if (user == null || !user.hasRoles('ADMIN')) { + if (user == null || !user.canViewOthersComments()) { return null; } @@ -54,7 +54,7 @@ const RootQuery = { }, assetMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Assets}}}) { - if (user == null || !user.hasRoles('ADMIN')) { + if (user == null || !user.canQueryAssets()) { return null; } @@ -66,7 +66,7 @@ const RootQuery = { }, commentMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Comments}}}) { - if (user == null || !user.hasRoles('ADMIN')) { + if (user == null || !user.canViewCommentMetrics()) { return null; } @@ -100,7 +100,7 @@ const RootQuery = { // so hide it in the event that we aren't an admin. users(_, {query: {action_type, limit, cursor, sort}}, {user, loaders: {Users, Actions}}) { - if (user == null || !user.hasRoles('ADMIN')) { + if (user == null || !user.canViewOtherUsers()) { return null; } diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index d8ed7ee15..03e1b121a 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -5,7 +5,7 @@ const User = { actions({id}, _, {user, loaders: {Actions}}) { // Only return the actions if the user is not an admin. - if (user && user.hasRoles('ADMIN')) { + if (user && user.canViewActions()) { return Actions.getByID.load(id); } @@ -14,7 +14,7 @@ const User = { // If the user is not an admin, only return comment list for the owner of // the comments. - if (user && (user.hasRoles('ADMIN') || user.id === id)) { + if (user && (user.canViewOthersComments() || user.id === id)) { return Comments.getByQuery({author_id: id, sort: 'REVERSE_CHRONOLOGICAL'}); } @@ -23,7 +23,7 @@ const User = { roles({id, roles}, _, {user}) { // If the user is not an admin, only return the current user's roles. - if (user && (user.hasRoles('ADMIN') || user.id === id)) { + if (user && (user.canChangeRoles() || user.id === id)) { return roles; } diff --git a/models/user.js b/models/user.js index ec215b3fa..179e6f7a5 100644 --- a/models/user.js +++ b/models/user.js @@ -1,6 +1,7 @@ const mongoose = require('../services/mongoose'); const bcrypt = require('bcrypt'); const uuid = require('uuid'); +const intersection = require('lodash/intersection'); // USER_ROLES is the array of roles that is permissible as a user role. const USER_ROLES = [ @@ -158,14 +159,74 @@ UserSchema.index({ }); /** - * Returns true if the user has all the roles specified. + * returns true if the user can look up assets through the api */ -UserSchema.method('hasRoles', function(...roles) { - return roles.every((role) => { +UserSchema.method('canQueryAssets', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); - // TODO: remove toUpperCase() once we've migrated usage. - return this.roles.indexOf(role.toUpperCase()) >= 0; - }); +/** + * returns true if the user can view actions + */ +UserSchema.method('canViewActions', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * returns true if the user can view non-null or non-ACCEPTED comments + */ +UserSchema.method('canViewNonNullOrAcceptedComments', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * returns true when a user can view comments that are not their own + */ +UserSchema.method('canViewOthersComments', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * returns true when a user can view comment metrics + */ +UserSchema.method('canViewCommentMetrics', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * returns true if a commenter is staff + */ +UserSchema.method('isStaff', function () { + return !!intersection(['ADMIN', 'MODERATOR', 'STAFF'], this.roles).length; +}); + +/** + * returns true when a user can see other user info + */ +UserSchema.method('canViewOtherUsers', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * when a user can modify tags + */ +UserSchema.method('canModifyTags', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +/** + * when a user can change roles + */ +UserSchema.method('canChangeUserRoles', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +UserSchema.method('canSetCommentStatus', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; +}); + +UserSchema.method('canSetUserStatus', function () { + return !!intersection(['ADMIN', 'MODERATOR'], this.roles).length; }); /** @@ -216,13 +277,12 @@ UserSchema.method('can', function(...actions) { return false; } - if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:suspendUser' || action === 'mutation:setCommentStatus') && !this.hasRoles('ADMIN')) { + if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:suspendUser' || action === 'mutation:setCommentStatus') && !this.canSetUserStatus()) { return false; } // {add,remove}CommentTag - requires admin and/or moderator role - const userCanModifyTags = user => ['ADMIN', 'MODERATOR'].some(r => user.hasRoles(r)); - if (actions.some(a => ['mutation:removeCommentTag', 'mutation:addCommentTag'].includes(a)) && ! userCanModifyTags(this)) { + if (actions.some(a => ['mutation:removeCommentTag', 'mutation:addCommentTag'].includes(a)) && ! this.canModifyTags()) { return false; } diff --git a/test/server/graph/mutations/addCommentTag.js b/test/server/graph/mutations/addCommentTag.js index 018e96631..1386d8339 100644 --- a/test/server/graph/mutations/addCommentTag.js +++ b/test/server/graph/mutations/addCommentTag.js @@ -44,6 +44,7 @@ describe('graph.mutations.addCommentTag', () => { Object.entries({ 'anonymous': undefined, 'regular commenter': new UserModel({}), + 'staff': new UserModel({roles: ['STAFF']}), 'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'}) }).forEach(([ userDescription, user ]) => { it(userDescription, async function () { From 63fa3c4de79e5fb0c04eeac11966f909239adfa2 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 15 May 2017 22:28:04 +0700 Subject: [PATCH 04/34] Implement suspend user components --- client/coral-admin/src/actions/moderation.js | 6 + .../src/components/ActionsMenu.css | 53 ++++ .../coral-admin/src/components/ActionsMenu.js | 64 +++++ .../src/components/ActionsMenuItem.js | 9 + client/coral-admin/src/components/App.js | 10 +- .../src/components/ToastContainer.css | 226 ++++++++++++++++++ .../src/components/ToastContainer.js | 7 + .../coral-admin/src/constants/moderation.js | 2 + .../Community/components/ActionButton.js | 2 +- .../Community}/components/BanUserButton.css | 0 .../Community}/components/BanUserButton.js | 0 .../Community/components/SuspendUserDialog.js | 10 +- .../ModerationQueue/ModerationContainer.js | 38 ++- .../ModerationQueue/ModerationQueue.js | 2 + .../components/BanUserDialog.css | 7 +- .../components/BanUserDialog.js | 2 +- .../ModerationQueue/components/Comment.js | 19 +- .../components/SuspendUserDialog.css | 90 +++++++ .../components/SuspendUserDialog.js | 147 ++++++++++++ client/coral-admin/src/reducers/moderation.js | 28 ++- client/coral-admin/src/translations.json | 41 ++-- client/coral-ui/components/Button.css | 36 ++- client/coral-ui/components/Button.js | 2 +- package.json | 5 +- yarn.lock | 24 ++ 25 files changed, 770 insertions(+), 60 deletions(-) create mode 100644 client/coral-admin/src/components/ActionsMenu.css create mode 100644 client/coral-admin/src/components/ActionsMenu.js create mode 100644 client/coral-admin/src/components/ActionsMenuItem.js create mode 100644 client/coral-admin/src/components/ToastContainer.css create mode 100644 client/coral-admin/src/components/ToastContainer.js rename client/coral-admin/src/{ => containers/Community}/components/BanUserButton.css (100%) rename client/coral-admin/src/{ => containers/Community}/components/BanUserButton.js (100%) rename client/coral-admin/src/{ => containers/ModerationQueue}/components/BanUserDialog.css (97%) rename client/coral-admin/src/{ => containers/ModerationQueue}/components/BanUserDialog.js (97%) create mode 100644 client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.css create mode 100644 client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js index c93dc0c76..52d5134a5 100644 --- a/client/coral-admin/src/actions/moderation.js +++ b/client/coral-admin/src/actions/moderation.js @@ -7,6 +7,12 @@ export const singleView = () => ({type: actions.SINGLE_VIEW}); export const showBanUserDialog = (user, commentId, commentStatus, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, commentStatus, showRejectedNote}); export const hideBanUserDialog = (showDialog) => ({type: actions.HIDE_BANUSER_DIALOG, showDialog}); +// Suspend User Dialog +export const showSuspendUserDialog = (userId, username, commentId, commentStatus) => + ({type: actions.SHOW_SUSPEND_USER_DIALOG, userId, username, commentId, commentStatus}); + +export const hideSuspendUserDialog = () => ({type: actions.HIDE_SUSPEND_USER_DIALOG}); + // hide shortcuts note export const hideShortcutsNote = () => { try { diff --git a/client/coral-admin/src/components/ActionsMenu.css b/client/coral-admin/src/components/ActionsMenu.css new file mode 100644 index 000000000..1d2f91f74 --- /dev/null +++ b/client/coral-admin/src/components/ActionsMenu.css @@ -0,0 +1,53 @@ +.button { + -webkit-transform: scale(.8); + transform: scale(.8); + margin: 0; +} + +.root { + color: black; + > :global(.mdl-menu__container) { + margin-left: 10px; + > :global(.mdl-menu__outline) { + box-shadow: none; + } + } +} + +.buttonOpen { + box-shadow: none; + color: white; + background-color: #616161; +} + +.arrowIcon { + margin-left: 6px; + margin-right: 0; + vertical-align: middle; + margin-right: 0; + font-size: 14px; +} + +.menu { + padding: 0; +} + +.menuItem { + background-color: #2a2a2a; + color: white; + &:first-child { + margin-bottom: 1px; + border-radius: 2px 2px 0px 0px; + } + &:last-child { + border-radius: 0px 0px 2px 2px; + } + &:hover, &:active, &:focus { + background-color: #767676; + } + &[disabled], &[disabled]:hover, &[disabled]:focus, &[disabled]:active { + background-color: #262626; + color: rgba(255, 255, 255, 0.5); + } +} + diff --git a/client/coral-admin/src/components/ActionsMenu.js b/client/coral-admin/src/components/ActionsMenu.js new file mode 100644 index 000000000..eb173eb81 --- /dev/null +++ b/client/coral-admin/src/components/ActionsMenu.js @@ -0,0 +1,64 @@ +import React, {PropTypes} from 'react'; +import {Button, Icon} from 'coral-ui'; +import {Menu} from 'react-mdl'; +import cn from 'classnames'; +import {findDOMNode} from 'react-dom'; +import styles from './ActionsMenu.css'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from 'coral-admin/src/translations.json'; +const lang = new I18n(translations); + +let count = 0; + +class ActionsMenu extends React.Component { + id = `actions-dropdown-${count++}`; + menu = null; + state = {open: false}; + timeout = null; + + componentWillUnmount() { + clearTimeout(this.timeout); + } + + 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}); + }, 150); + }; + + render() { + return ( +
+ + + {this.props.children} + +
+ ); + } +} + +ActionsMenu.propTypes = { + icon: PropTypes.string, +}; + +export default ActionsMenu; diff --git a/client/coral-admin/src/components/ActionsMenuItem.js b/client/coral-admin/src/components/ActionsMenuItem.js new file mode 100644 index 000000000..82dab96f0 --- /dev/null +++ b/client/coral-admin/src/components/ActionsMenuItem.js @@ -0,0 +1,9 @@ +import React from 'react'; +import cn from 'classnames'; +import {MenuItem} from 'react-mdl'; +import styles from './ActionsMenu.css'; + +const ActionsMenuItem = (props) => + ; + +export default ActionsMenuItem; diff --git a/client/coral-admin/src/components/App.js b/client/coral-admin/src/components/App.js index 3c6e88a14..1a15c72c3 100644 --- a/client/coral-admin/src/components/App.js +++ b/client/coral-admin/src/components/App.js @@ -1,16 +1,16 @@ import React from 'react'; -import {Provider} from 'react-redux'; +import ToastContainer from './ToastContainer'; import 'material-design-lite'; -import store from 'services/store'; import AppRouter from '../AppRouter'; export default class App extends React.Component { render () { return ( - - - +
+ + +
); } } diff --git a/client/coral-admin/src/components/ToastContainer.css b/client/coral-admin/src/components/ToastContainer.css new file mode 100644 index 000000000..319872ceb --- /dev/null +++ b/client/coral-admin/src/components/ToastContainer.css @@ -0,0 +1,226 @@ +@keyframes :global(bounceInRight) { + from, 60%, 75%, 90%, to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } + from { + opacity: 0; + transform: translate3d(3000px, 0, 0); } + 60% { + opacity: 1; + transform: translate3d(-25px, 0, 0); } + 75% { + transform: translate3d(10px, 0, 0); } + 90% { + transform: translate3d(-5px, 0, 0); } + to { + transform: none; } } + +@keyframes :global(bounceOutRight) { + 20% { + opacity: 1; + transform: translate3d(-20px, 0, 0); } + to { + opacity: 0; + transform: translate3d(2000px, 0, 0); } } + +@keyframes :global(bounceInLeft) { + from, 60%, 75%, 90%, to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } + 0% { + opacity: 0; + transform: translate3d(-3000px, 0, 0); } + 60% { + opacity: 1; + transform: translate3d(25px, 0, 0); } + 75% { + transform: translate3d(-10px, 0, 0); } + 90% { + transform: translate3d(5px, 0, 0); } + to { + transform: none; } } + +@keyframes :global(bounceOutLeft) { + 20% { + opacity: 1; + transform: translate3d(20px, 0, 0); } + to { + opacity: 0; + transform: translate3d(-2000px, 0, 0); } } + +@keyframes :global(bounceInUp) { + from, 60%, 75%, 90%, to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } + from { + opacity: 0; + transform: translate3d(0, 3000px, 0); } + 60% { + opacity: 1; + transform: translate3d(0, -20px, 0); } + 75% { + transform: translate3d(0, 10px, 0); } + 90% { + transform: translate3d(0, -5px, 0); } + to { + transform: translate3d(0, 0, 0); } } + +@keyframes :global(bounceOutUp) { + 20% { + transform: translate3d(0, -10px, 0); } + 40%, 45% { + opacity: 1; + transform: translate3d(0, 20px, 0); } + to { + opacity: 0; + transform: translate3d(0, -2000px, 0); } } + +@keyframes :global(bounceInDown) { + from, 60%, 75%, 90%, to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } + 0% { + opacity: 0; + transform: translate3d(0, -3000px, 0); } + 60% { + opacity: 1; + transform: translate3d(0, 25px, 0); } + 75% { + transform: translate3d(0, -10px, 0); } + 90% { + transform: translate3d(0, 5px, 0); } + to { + transform: none; } } + +@keyframes :global(bounceOutDown) { + 20% { + transform: translate3d(0, 10px, 0); } + 40%, 45% { + opacity: 1; + transform: translate3d(0, -20px, 0); } + to { + opacity: 0; + transform: translate3d(0, 2000px, 0); } } + +@keyframes :global(track-progress) { + 0% { + width: 100%; } + 100% { + width: 0; } } + +:global { + .bounceOutRight, .toast-exit--top-right, .toast-exit--bottom-right { + animation-name: bounceOutRight; } + + .bounceInRight, .toast-enter--top-right, .toast-enter--bottom-right { + animation-name: bounceInRight; } + + .bounceInLeft, .toast-enter--top-left, .toast-enter--bottom-left { + animation-name: bounceInLeft; } + + .bounceOutLeft, .toast-exit--top-left, .toast-exit--bottom-left { + animation-name: bounceOutLeft; } + + .bounceInUp, .toast-enter--bottom-center { + animation-name: bounceInUp; } + .bounceOutUp, .toast-exit--top-center { + animation-name: bounceOutUp; } + + .bounceInDown, .toast-enter--top-center { + animation-name: bounceInDown; } + + .bounceOutDown, .toast-exit--bottom-center { + animation-name: bounceOutDown; } + + .animated { + animation-duration: 0.75s; + animation-fill-mode: both; } + + .toastify { + z-index: 999; + position: fixed; + padding: 4px; + width: 350px; + max-width: 98%; + color: #999; + box-sizing: border-box; } + .toastify--top-left { + top: 1em; + left: 1em; } + .toastify--top-center { + top: 1em; + left: 50%; + margin-left: -175px; } + .toastify--top-right { + top: 1em; + right: 2em; } + .toastify--bottom-left { + bottom: 1em; + left: 1em; } + .toastify--bottom-center { + bottom: 1em; + left: 50%; + margin-left: -175px; } + .toastify--bottom-right { + bottom: 1em; + right: 2em; } + .toastify__img { + float: left; + margin-right: 8px; + vertical-align: middle; } + + .toastify__close { + position: absolute; + top: 18px; + left: 12px; + width: 20px; + height: 16px; + padding: 0; + text-align: center; + text-decoration: none; + color: white; + font-weight: bold; + font-size: 14px; + background: transparent; + outline: none; + border: none; + cursor: pointer; + opacity: 0.8; + transition: .3s ease; } + .toastify__close:hover, .toastify__close:focus { + opacity: 1; + } + + .toastify-content { + position: relative; + width: 100%; + margin-bottom: 12px; + padding: 18px 24px 20px 48px; + box-sizing: border-box; + background: #404040; + border-radius: 2px; + box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1), 0 3px 20px 0 rgba(0, 0, 0, 0.05); } + .toastify-content--info { + background: #2488cb; } + .toastify-content--success { + background: #008577; } + .toastify-content--warning { + background: #ef6c2b; } + .toastify-content--error { + background: #ef342b; } + + .toastify__body { + color: white; + font-size: 15px; + font-weight: 400; + } + + .toastify__progress { + position: absolute; + bottom: 0; + left: 0; + width: 0; + height: 4px; + z-index: 999; + opacity: 0.8; + border-radius: 2px; + animation: track-progress linear 1; + background-color: white; + } +} diff --git a/client/coral-admin/src/components/ToastContainer.js b/client/coral-admin/src/components/ToastContainer.js new file mode 100644 index 000000000..a751d2714 --- /dev/null +++ b/client/coral-admin/src/components/ToastContainer.js @@ -0,0 +1,7 @@ +import './ToastContainer.css'; +import {defaultProps} from 'recompose'; +import {ToastContainer} from 'react-toastify'; + +export default defaultProps({ + autoClose: 5000, +})(ToastContainer); diff --git a/client/coral-admin/src/constants/moderation.js b/client/coral-admin/src/constants/moderation.js index 14672146e..7e918ed4a 100644 --- a/client/coral-admin/src/constants/moderation.js +++ b/client/coral-admin/src/constants/moderation.js @@ -3,3 +3,5 @@ export const SINGLE_VIEW = 'SINGLE_VIEW'; export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG'; export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG'; export const HIDE_SHORTCUTS_NOTE = 'HIDE_SHORTCUTS_NOTE'; +export const SHOW_SUSPEND_USER_DIALOG = 'SHOW_SUSPEND_USER_DIALOG'; +export const HIDE_SUSPEND_USER_DIALOG = 'HIDE_SUSPEND_USER_DIALOG'; diff --git a/client/coral-admin/src/containers/Community/components/ActionButton.js b/client/coral-admin/src/containers/Community/components/ActionButton.js index 6352480fd..ad5346ac5 100644 --- a/client/coral-admin/src/containers/Community/components/ActionButton.js +++ b/client/coral-admin/src/containers/Community/components/ActionButton.js @@ -1,6 +1,6 @@ import React from 'react'; import styles from '../Community.css'; -import BanUserButton from '../../../components/BanUserButton'; +import BanUserButton from './BanUserButton'; import {Button} from 'coral-ui'; import {menuActionsMap} from '../../../containers/ModerationQueue/helpers/moderationQueueActionsMap'; diff --git a/client/coral-admin/src/components/BanUserButton.css b/client/coral-admin/src/containers/Community/components/BanUserButton.css similarity index 100% rename from client/coral-admin/src/components/BanUserButton.css rename to client/coral-admin/src/containers/Community/components/BanUserButton.css diff --git a/client/coral-admin/src/components/BanUserButton.js b/client/coral-admin/src/containers/Community/components/BanUserButton.js similarity index 100% rename from client/coral-admin/src/components/BanUserButton.js rename to client/coral-admin/src/containers/Community/components/BanUserButton.js diff --git a/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js index 20e221c48..827352c42 100644 --- a/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js +++ b/client/coral-admin/src/containers/Community/components/SuspendUserDialog.js @@ -10,16 +10,16 @@ const lang = new I18n(translations); const stages = [ { - title: 'suspenduser.title_0', - description: 'suspenduser.description_0', + title: 'suspenduser.title_reject', + description: 'suspenduser.description_reject', options: { 'j': 'suspenduser.no_cancel', 'k': 'suspenduser.yes_suspend' } }, { - title: 'suspenduser.title_1', - description: 'suspenduser.description_1', + title: 'suspenduser.title_notify', + description: 'suspenduser.description_notify', options: { 'j': 'bandialog.cancel', 'k': 'suspenduser.send' @@ -38,7 +38,7 @@ class SuspendUserDialog extends Component { } componentDidMount() { - this.setState({email: lang.t('suspenduser.email'), about: lang.t('suspenduser.username')}); + this.setState({email: lang.t('suspenduser.email_message_reject'), about: lang.t('suspenduser.username')}); } /* diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js index 9606be543..e8535f854 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js @@ -1,6 +1,8 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; import {compose} from 'react-apollo'; +import {toast} from 'react-toastify'; import key from 'keymaster'; import isEqual from 'lodash/isEqual'; import styles from './components/styles.css'; @@ -10,10 +12,19 @@ import {banUser, setCommentStatus} from '../../graphql/mutations'; import {fetchSettings} from 'actions/settings'; import {updateAssets} from 'actions/assets'; -import {toggleModal, singleView, showBanUserDialog, hideBanUserDialog, hideShortcutsNote} from 'actions/moderation'; +import { + toggleModal, + singleView, + showBanUserDialog, + hideBanUserDialog, + showSuspendUserDialog, + hideSuspendUserDialog, + hideShortcutsNote, +} from 'actions/moderation'; import {Spinner} from 'coral-ui'; -import BanUserDialog from '../../components/BanUserDialog'; +import BanUserDialog from './components/BanUserDialog'; +import SuspendUserDialog from './components/SuspendUserDialog'; import ModerationQueue from './ModerationQueue'; import ModerationMenu from './components/ModerationMenu'; import ModerationHeader from './components/ModerationHeader'; @@ -175,6 +186,7 @@ class ModerationContainer extends Component { bannedWords={settings.wordlist.banned} suspectWords={settings.wordlist.suspect} showBanUserDialog={props.showBanUserDialog} + showSuspendUserDialog={props.showSuspendUserDialog} acceptComment={props.acceptComment} rejectComment={props.rejectComment} loadMore={props.loadMore} @@ -192,6 +204,12 @@ class ModerationContainer extends Component { showRejectedNote={moderation.showRejectedNote} rejectComment={props.rejectComment} /> + toast('User admin has been suspended for 24 hours. this suspension will automatically end after 24 hours.', {type: 'success'}) && props.hideSuspendUserDialog()} + /> ({ }); const mapDispatchToProps = dispatch => ({ - toggleModal: toggle => dispatch(toggleModal(toggle)), onClose: () => dispatch(toggleModal(false)), - singleView: () => dispatch(singleView()), - updateAssets: assets => dispatch(updateAssets(assets)), - fetchSettings: () => dispatch(fetchSettings()), - showBanUserDialog: (user, commentId, commentStatus, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, commentStatus, showRejectedNote)), hideBanUserDialog: () => dispatch(hideBanUserDialog(false)), - hideShortcutsNote: () => dispatch(hideShortcutsNote()), + ...bindActionCreators({ + toggleModal, + singleView, + updateAssets, + fetchSettings, + showBanUserDialog, + hideShortcutsNote, + showSuspendUserDialog, + hideSuspendUserDialog, + }, dispatch), }); export default compose( diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index c4bf0f85c..b93f185f0 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -16,6 +16,7 @@ class ModerationQueue extends React.Component { suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired, currentAsset: PropTypes.object, showBanUserDialog: PropTypes.func.isRequired, + showSuspendUserDialog: PropTypes.func.isRequired, rejectComment: PropTypes.func.isRequired, acceptComment: PropTypes.func.isRequired, comments: PropTypes.array.isRequired @@ -51,6 +52,7 @@ class ModerationQueue extends React.Component { bannedWords={props.bannedWords} actions={actionsMap[status]} showBanUserDialog={props.showBanUserDialog} + showSuspendUserDialog={props.showSuspendUserDialog} acceptComment={props.acceptComment} rejectComment={props.rejectComment} currentAsset={props.currentAsset} diff --git a/client/coral-admin/src/components/BanUserDialog.css b/client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.css similarity index 97% rename from client/coral-admin/src/components/BanUserDialog.css rename to client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.css index a46b9da32..f13f0e6aa 100644 --- a/client/coral-admin/src/components/BanUserDialog.css +++ b/client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.css @@ -152,13 +152,14 @@ input.error{ .cancel { margin-right: 10px; - width: 47%; + width: 48%; } .ban { - width: 47%; + width: 48%; } .buttons { - margin: 20px 0; + margin: 20px; + text-align: center; } diff --git a/client/coral-admin/src/components/BanUserDialog.js b/client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.js similarity index 97% rename from client/coral-admin/src/components/BanUserDialog.js rename to client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.js index b259012f3..87a5fe55a 100644 --- a/client/coral-admin/src/components/BanUserDialog.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/BanUserDialog.js @@ -5,7 +5,7 @@ import styles from './BanUserDialog.css'; import Button from 'coral-ui/components/Button'; import I18n from 'coral-framework/modules/i18n/i18n'; -import translations from '../translations'; +import translations from '../../../translations'; const lang = new I18n(translations); const onBanClick = (userId, commentId, commentStatus, handleBanUser, rejectComment, handleClose) => (e) => { diff --git a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js index ffd4db2a6..2e940d0d8 100644 --- a/client/coral-admin/src/containers/ModerationQueue/components/Comment.js +++ b/client/coral-admin/src/containers/ModerationQueue/components/Comment.js @@ -9,7 +9,8 @@ import {Icon} from 'coral-ui'; import FlagBox from './FlagBox'; import CommentType from './CommentType'; import ActionButton from 'coral-admin/src/components/ActionButton'; -import BanUserButton from 'coral-admin/src/components/BanUserButton'; +import ActionsMenu from 'coral-admin/src/components/ActionsMenu'; +import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem'; import {getActionSummary} from 'coral-framework/utils'; const linkify = new Linkify(); @@ -48,7 +49,19 @@ const Comment = ({actions = [], comment, suspectWords, bannedWords, ...props}) = {timeago().format(comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))} - props.showBanUserDialog(comment.user, comment.id, comment.status, comment.status !== 'REJECTED')} /> + + + props.showSuspendUserDialog(comment.user.id, comment.user.name, comment.id, comment.status)}> + Suspend User + props.showBanUserDialog(comment.user, comment.id, comment.status, comment.status !== 'REJECTED')}> + Ban User + + + {comment.user.status === 'banned' ? @@ -103,6 +116,8 @@ Comment.propTypes = { suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired, bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired, currentAsset: PropTypes.object, + showBanUserDialog: PropTypes.func.isRequired, + showSuspendUserDialog: PropTypes.func.isRequired, comment: PropTypes.shape({ body: PropTypes.string.isRequired, action_summaries: PropTypes.array, diff --git a/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.css b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.css new file mode 100644 index 000000000..1c96f509a --- /dev/null +++ b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.css @@ -0,0 +1,90 @@ +.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: 400px; + top: 50%; + transform: translateY(-50%); + padding: 20px; + border-radius: 4px; +} + +.header { + color: black; + font-size: 1.5em; + font-weight: 500; + margin: 0 0 8px 0; +} + +.close { + display: block; + position: absolute; + top: 24px; + right: 20px; +} + +.closeButton { + userSelect: none; + outline: none; + border: none; + touchAction: manipulation; + &::-moz-focus-inner: { + border: 0; + } + background: 0; + padding: 0; + font-size: 24px; + line-height: 14px; + cursor: pointer; + color: #363636; + &:hover { + color: #6b6b6b; + } +} + +.legend { + padding: 0; + font-weight: bold; +} + +div.radioGroup { + margin-top: 6px; +} + +label.radioGroup { + + &:global(.is-checked) > :global(.mdl-radio__outer-circle), + > :global(.mdl-radio__outer-circle) { + border-color: #212121; + } + + > :global(.mdl-radio__inner-circle) { + background: #212121; + } + + > :global(.mdl-radio__label) { + font-size: 14px; + line-height: 20px; + } +} + +.messageInput { + border-radius: 3px; + width: 100%; + padding: 10px; + font-size: 14px; + box-sizing: border-box; +} + +.cancel { + margin-right: 5px; +} + +.perform { + min-width: 90px; +} + +.buttons { + margin-top: 8px; + margin-bottom: 6px; + text-align: right; +} diff --git a/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js new file mode 100644 index 000000000..0ce0a43d1 --- /dev/null +++ b/client/coral-admin/src/containers/ModerationQueue/components/SuspendUserDialog.js @@ -0,0 +1,147 @@ +import React, {PropTypes} from 'react'; +import {Dialog} from 'coral-ui'; +import {RadioGroup, Radio} from 'react-mdl'; +import styles from './SuspendUserDialog.css'; + +import Button from 'coral-ui/components/Button'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../../../translations'; +const lang = new I18n(translations); + +const initialState = {step: 0, duration: '3', message: lang.t('suspenduser.email_message_suspend')}; + +class SuspendUserDialog extends React.Component { + + state = initialState; + + componentWillReceiveProps(next) { + if (this.props.open && !next.open) { + this.setState(initialState); + } + } + + handleDurationChange = (event) => { + this.setState({duration: event.target.value}); + } + + handleMessageChange = (event) => { + this.setState({message: event.target.value}); + } + + increaseStep = () => { + this.setState({step: this.state.step + 1}); + } + + resetStep = () => { + this.setState({step: 0}); + } + + handlePerform = () => { + this.props.onPerform({ + duration: this.state.duration, + message: this.state.message, + }); + }; + + renderStep0() { + const {onCancel, username} = this.props; + const {duration} = this.state; + return ( +
+

+ {lang.t('suspenduser.title_suspend')} +

+

+ {lang.t('suspenduser.description_suspend', username)} +

+
+ {lang.t('suspenduser.select_duration')} + + {lang.t('suspenduser.hours', 3)} + {lang.t('suspenduser.hours', 24)} + {lang.t('suspenduser.days', 7)} + +
+
+ + +
+
+ ); + } + + renderStep1() { + const {onCancel, username} = this.props; + const {message} = this.state; + return ( +
+

+ {lang.t('suspenduser.title_notify')} +

+

+ {lang.t('suspenduser.description_notify', username)} +

+
+ {lang.t('suspenduser.write_message')} +