diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 8cc094d5f..e681e1311 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -18,7 +18,10 @@ export const checkLogin = () => dispatch => { const isAdmin = !!result.user.roles.filter(i => i === 'admin').length; dispatch(checkLoginSuccess(result.user, isAdmin)); }) - .catch(error => dispatch(checkLoginFailure(error))); + .catch(error => { + console.error(error); + dispatch(checkLoginFailure(`${error.message}`)); + }); }; // Set CSRF Token diff --git a/client/coral-admin/src/components/Comment.js b/client/coral-admin/src/components/Comment.js index dc350e815..2304e47c5 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -54,16 +54,16 @@ const Comment = props => { }; // Get the button of the action performed over a comment if any -const getActionButton = (action, i, props) => { - const {comment, author} = props; +const getActionButton = (option, i, props) => { + const {comment, author, menuOptionsMap, action} = props; const status = comment.status; const flagged = comment.flagged; const banned = (author.status === 'banned'); - if (action === 'flag' && (status || flagged === true)) { + if (option === 'flag' && (status || flagged === true)) { return null; } - if (action === 'ban') { + if (option === 'ban') { return ( { ); } + const menuOption = menuOptionsMap[option]; return ( props.onClickAction(props.actionsMap[action].status, comment)} + onClick={() => props.onClickAction(menuOption.status, comment, action)} /> ); }; diff --git a/client/coral-admin/src/components/ModerationList.js b/client/coral-admin/src/components/ModerationList.js index 02f873c5b..669d821ff 100644 --- a/client/coral-admin/src/components/ModerationList.js +++ b/client/coral-admin/src/components/ModerationList.js @@ -4,9 +4,10 @@ import key from 'keymaster'; import Hammer from 'hammerjs'; import Comment from './Comment'; import UserAction from './UserAction'; +import SuspendUserModal from './SuspendUserModal'; // Each action has different meaning and configuration -const actionsMap = { +const menuOptionsMap = { 'reject': {status: 'rejected', icon: 'close', key: 'r'}, 'approve': {status: 'accepted', icon: 'done', key: 't'}, 'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'}, @@ -23,7 +24,7 @@ export default class ModerationList extends React.Component { comments: PropTypes.object, users: PropTypes.object.isRequired, actions: PropTypes.object, - onClickAction: PropTypes.func.isRequired, + updateCommentStatus: PropTypes.func.isRequired, // list of actions (flags, etc) associated with the comments modActions: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -32,13 +33,7 @@ export default class ModerationList extends React.Component { suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired } - constructor (props) { - super(props); - - this.state = {active: null}; - this.onClickAction = this.onClickAction.bind(this); - this.onClickShowBanDialog = this.onClickShowBanDialog.bind(this); - } + state = {active: null, suspendUserModal: null}; // remove key handlers before leaving componentWillUnmount () { @@ -76,8 +71,8 @@ export default class ModerationList extends React.Component { // Add key handlers. Each action has one and added j/k for moving around bindKeyHandlers () { const {modActions, isActive} = this.props; - modActions.filter(action => actionsMap[action].key).forEach(action => { - key(actionsMap[action].key, 'moderationList', () => isActive && this.actionKeyHandler(actionsMap[action].status)); + modActions.filter(action => menuOptionsMap[action].key).forEach(action => { + key(menuOptionsMap[action].key, 'moderationList', () => isActive && this.actionKeyHandler(menuOptionsMap[action].status)); }); key('j', 'moderationList', () => isActive && this.moveKeyHandler('down')); key('k', 'moderationList', () => isActive && this.moveKeyHandler('up')); @@ -121,21 +116,52 @@ export default class ModerationList extends React.Component { // If we are performing an action over a comment (aka removing from the list) we need to select a new active. // TODO: In the future this can be improved and look at the actual state to // resolve since the content of the list could change externally. For now it works as expected - onClickAction (action, id, author_id) { + onClickAction = (menuOption, id, action) => { // activate the next comment if (id === this.state.active) { - const {commentIds} = this.props; - if (commentIds[commentIds.length - 1] === this.state.active) { - this.setState({active: commentIds[commentIds.length - 2]}); + const moderationIds = this.getModerationIds(); + if (moderationIds[moderationIds.length - 1] === this.state.active) { + this.setState({active: moderationIds[moderationIds.length - 2]}); } else { - this.setState({active: commentIds[Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.length - 1)]}); + this.setState({active: moderationIds[Math.min(moderationIds.indexOf(this.state.active) + 1, moderationIds.length - 1)]}); + } + } + + // Update the status right away if this is a comment + if (action.item_type === 'comment') { + this.props.updateCommentStatus(menuOption, id); + } else if (action.item_type === 'user') { + + // If a user bio or name is rejected, bring up a dialog before suspending them. + if (menuOption === 'rejected') { + this.setState({suspendUserModal: {stage: 0, actionType: action.action_type}}); + } else { + this.props.updateUserStatus(menuOption, action.item_id); } } - this.props.onClickAction(action, id, author_id); } - onClickShowBanDialog(userId, userName, commentId) { + /* + * When an admin clicks to suspend a user a dialog is shown, this function + * handles the possible actions for that dialog. + */ + onClickSuspendModalButton = (stage, menuOption) => () => { + const {updateUserStatus, action} = this.props; + const cancel = () => this.setState({suspendUserModal: null}); + const next = () => this.setState((prev) => { + prev.suspendUserModal.stage++; + return prev; + }); + const suspend = () => updateUserStatus('suspend', action.item_id); + const suspendModalActions = [ + [ cancel, next ], + [ cancel, suspend ] + ]; + return suspendModalActions[stage][menuOption](); + } + + onClickShowBanDialog = (userId, userName, commentId) => { this.props.onClickShowBanDialog(userId, userName, commentId); } @@ -162,7 +188,7 @@ export default class ModerationList extends React.Component { onClickAction={this.onClickAction} onClickShowBanDialog={this.onClickShowBanDialog} modActions={modActions} - actionsMap={actionsMap} + menuOptionsMap={menuOptionsMap} isActive={itemId === active} hideActive={hideActive} />; } else { @@ -178,29 +204,37 @@ export default class ModerationList extends React.Component { onClickAction={this.onClickAction} onClickShowBanDialog={this.onClickShowBanDialog} modActions={modActions} - actionsMap={actionsMap} + menuOptionsMap={menuOptionsMap} isActive={itemId === active} hideActive={hideActive} />; } return modItem; } - render () { - const {singleView, commentIds = [], actionIds = [], comments, actions, key} = this.props; - - // Combine moderations and actions into a single stream and sort by most recently updated. - const moderationIds = [ ...commentIds, ...actionIds ].sort((a, b) => { + getModerationIds = () => { + const {commentIds = [], actionIds = [], comments, actions} = this.props; + return [ ...commentIds, ...actionIds ].sort((a, b) => { const itemA = comments[a] || actions[a]; const itemB = comments[b] || actions[b]; return itemB.updated_at - itemA.updated_at; }); + } + + render () { + const {singleView, key} = this.props; + + // Combine moderations and actions into a single stream and sort by most recently updated. + const moderationIds = this.getModerationIds(); return ( + id='moderationList'> {moderationIds.map(this.mapModItems)} + this.setState({suspendUserModal:null})} + onButtonClick={this.onClickSuspendModalButton} /> ); } diff --git a/client/coral-admin/src/components/SuspendUserModal.css b/client/coral-admin/src/components/SuspendUserModal.css new file mode 100644 index 000000000..e69de29bb diff --git a/client/coral-admin/src/components/SuspendUserModal.js b/client/coral-admin/src/components/SuspendUserModal.js new file mode 100644 index 000000000..2204d797f --- /dev/null +++ b/client/coral-admin/src/components/SuspendUserModal.js @@ -0,0 +1,43 @@ +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../translations.json'; +import React from 'react'; +import Modal from 'components/Modal'; +import styles from './SuspendUserModal.css'; +import {Button} from 'coral-ui'; + +const stages = [ + { + title: 'suspenduser.title_0', + description: 'suspenduser.description_0', + options: { + 'j': 'suspenduser.no_cancel', + 'k': 'suspenduser.yes_suspend' + } + }, + { + title: 'suspenduser.title_1', + description: 'suspenduser.description_1', + options: { + 'j': 'suspenduser.cancel', + 'k': 'suspenduser.send' + } + } +]; + +export default ({stage, actionType, onClose, onButtonClick}) => + actionType ? + {lang.t(stages[stage].title)} + + + {lang.t(stages[stage].description)} + + {Object.keys(stages[stage].options).map((key, i) => ( + + ))} + + + : null; + +const lang = new I18n(translations); diff --git a/client/coral-admin/src/components/UserAction.js b/client/coral-admin/src/components/UserAction.js index 7c072f3b1..356628e7a 100644 --- a/client/coral-admin/src/components/UserAction.js +++ b/client/coral-admin/src/components/UserAction.js @@ -62,34 +62,35 @@ const UserAction = props => { export default UserAction; // Get the button of the action performed over a comment if any -const getActionButton = (action, i, props) => { - const {user} = props; +const getActionButton = (option, i, props) => { + const {user, onClickShowBanDialog, onClickAction, menuOptionsMap, action} = props; const status = user.status; const banned = (user.status === 'banned'); - if (action === 'flag' && status) { + if (option === 'flag' && status) { return null; } - if (action === 'ban') { + if (option === 'ban') { return ( props.onClickShowBanDialog(user.id, user.displayName)} + onClick={() => onClickShowBanDialog(user.id, user.displayName)} key={i} > {lang.t('comment.ban_user')} ); } + const menuOption = menuOptionsMap[option]; return ( props.onClickAction(props.actionsMap[action].status, user.id)} + onClick={() => onClickAction(menuOption.status, user.id, action)} /> ); }; diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js index 3866bc641..0aeb6f79b 100644 --- a/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue/ModerationQueue.js @@ -31,7 +31,7 @@ export default (props) => ( users={props.users.byId} actionIds={props.userActionIds} actions={props.actions.byId} - onClickAction={props.updateStatus} + updateCommentStatus={props.updateStatus} onClickShowBanDialog={props.showBanUserDialog} modActions={['reject', 'approve', 'ban']} loading={props.comments.loading}/> @@ -50,7 +50,7 @@ export default (props) => ( commentIds={props.rejectedIds} comments={props.comments.byId} users={props.users.byId} - onClickAction={props.updateStatus} + updateCommentStatus={props.updateStatus} modActions={['approve']} loading={props.comments.loading} /> @@ -63,7 +63,7 @@ export default (props) => ( commentIds={props.flaggedIds} comments={props.comments.byId} users={props.users.byId} - onClickAction={props.updateStatus} + updateCommentStatus={props.updateStatus} modActions={['reject', 'approve']} loading={props.comments.loading}/> diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 581fb729c..bb442427f 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -141,5 +141,8 @@ export const checkLogin = () => dispatch => { const isAdmin = !!result.user.roles.filter(i => i === 'admin').length; dispatch(checkLoginSuccess(result.user, isAdmin)); }) - .catch(error => dispatch(checkLoginFailure(error))); + .catch(error => { + console.error(error); + dispatch(checkLoginFailure(`${error.message}`)); + }); }; diff --git a/models/action.js b/models/action.js index 476e4de95..0778e2820 100644 --- a/models/action.js +++ b/models/action.js @@ -134,6 +134,9 @@ ActionSchema.statics.getActionSummaries = function(item_ids, current_user_id = ' item_type: { $last: '$item_type' }, + metadata: { + $push: '$metadata' + }, created_at: { $min: '$created_at' },