diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js
index 3f8d975df..37e09590d 100644
--- a/client/coral-admin/src/actions/moderation.js
+++ b/client/coral-admin/src/actions/moderation.js
@@ -42,3 +42,12 @@ export const changeUserDetailStatuses = (tab) => {
}
return {type: actions.CHANGE_USER_DETAIL_STATUSES, tab, statuses};
};
+
+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
+ };
+};
diff --git a/client/coral-admin/src/constants/moderation.js b/client/coral-admin/src/constants/moderation.js
index aa8a51a24..374de616a 100644
--- a/client/coral-admin/src/constants/moderation.js
+++ b/client/coral-admin/src/constants/moderation.js
@@ -9,3 +9,6 @@ export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL';
export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL';
export const SET_SORT_ORDER = 'MODERATION_SET_SORT_ORDER';
export const CHANGE_USER_DETAIL_STATUSES = 'CHANGE_USER_DETAIL_STATUSES';
+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';
diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js
index 4201ffb06..1b95bdb90 100644
--- a/client/coral-admin/src/reducers/moderation.js
+++ b/client/coral-admin/src/reducers/moderation.js
@@ -1,4 +1,4 @@
-import {fromJS, Map} from 'immutable';
+import {fromJS, Map, Set} from 'immutable';
import * as actions from '../constants/moderation';
const initialState = fromJS({
@@ -10,6 +10,7 @@ const initialState = fromJS({
userDetailId: null,
userDetailActiveTab: 'all',
userDetailStatuses: ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'],
+ userDetailSelectedIds: new Set(),
banDialog: false,
shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show',
sortOrder: 'REVERSE_CHRONOLOGICAL',
@@ -66,11 +67,19 @@ export default function moderation (state = initialState, action) {
case actions.VIEW_USER_DETAIL:
return state.set('userDetailId', action.userId);
case actions.HIDE_USER_DETAIL:
- return state.set('userDetailId', null);
+ return state
+ .set('userDetailId', null)
+ .update('userDetailSelectedIds', (set) => set.clear());
+ case actions.CLEAR_USER_DETAIL_SELECTIONS:
+ return state.update('userDetailSelectedIds', (set) => set.clear());
case actions.CHANGE_USER_DETAIL_STATUSES:
return state
.set('userDetailActiveTab', action.tab)
.set('userDetailStatuses', action.statuses);
+ case actions.SELECT_USER_DETAIL_COMMENT:
+ return state.update('userDetailSelectedIds', (set) => set.add(action.id));
+ case actions.UNSELECT_USER_DETAIL_COMMENT:
+ return state.update('userDetailSelectedIds', (set) => set.delete(action.id));
case actions.SET_SORT_ORDER:
return state.set('sortOrder', action.order);
default :
diff --git a/client/coral-admin/src/routes/Moderation/components/Comment.js b/client/coral-admin/src/routes/Moderation/components/Comment.js
index 1bc2b50d7..9d1019b5e 100644
--- a/client/coral-admin/src/routes/Moderation/components/Comment.js
+++ b/client/coral-admin/src/routes/Moderation/components/Comment.js
@@ -24,6 +24,8 @@ const Comment = ({
suspectWords,
bannedWords,
minimal,
+ selected,
+ toggleSelect,
...props
}) => {
const links = linkify.getMatches(comment.body);
@@ -48,10 +50,17 @@ const Comment = ({
})
.concat(linkText);
+ let selectionStateCSS;
+ if (minimal) {
+ selectionStateCSS = selected ? styles.minimalSelection : '';
+ } else {
+ selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
+ }
+
return (
@@ -63,6 +72,16 @@ const Comment = ({
)
}
+ {
+ minimal && typeof selected === 'boolean' && typeof toggleSelect === 'function' && (
+
toggleSelect(e.target.value, e.target.checked)} />
+ )
+ }
{timeago(comment.created_at || Date.now() - props.index * 60 * 1000)}
@@ -187,6 +206,7 @@ Comment.propTypes = {
showBanUserDialog: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
currentUserId: PropTypes.string.isRequired,
+ toggleSelect: PropTypes.func,
comment: PropTypes.shape({
body: PropTypes.string.isRequired,
action_summaries: PropTypes.array,
diff --git a/client/coral-admin/src/routes/Moderation/components/UserDetail.css b/client/coral-admin/src/routes/Moderation/components/UserDetail.css
index 1119be8c4..d4b5f0db7 100644
--- a/client/coral-admin/src/routes/Moderation/components/UserDetail.css
+++ b/client/coral-admin/src/routes/Moderation/components/UserDetail.css
@@ -41,8 +41,11 @@
}
.commentStatuses {
- padding: 0;
+ padding: 10px 0 0 0;
+ margin: 0;
+ height: 52px;
list-style: none;
+ box-sizing: border-box;
li {
display: inline-block;
@@ -56,3 +59,24 @@
font-weight: bold;
border-bottom: 3px solid #F36451;
}
+
+.bulkActionGroup {
+ height: 52px;
+ background-color: #efefef;
+
+ i {
+ margin-right: 0;
+ }
+
+ .bulkAction {
+ display: inline-block;
+ width: 48px;
+ height: 48px;
+ transform: scale(.7);
+ min-width: 0;
+ }
+
+ .bulkAction:last-child {
+ margin-left: -10px;
+ }
+}
diff --git a/client/coral-admin/src/routes/Moderation/components/UserDetail.js b/client/coral-admin/src/routes/Moderation/components/UserDetail.js
index c461220c8..0fb355486 100644
--- a/client/coral-admin/src/routes/Moderation/components/UserDetail.js
+++ b/client/coral-admin/src/routes/Moderation/components/UserDetail.js
@@ -16,6 +16,10 @@ export default class UserDetail extends React.Component {
showSuspendUserDialog: PropTypes.func.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,
}
copyPermalink = () => {
@@ -28,12 +32,24 @@ export default class UserDetail extends React.Component {
}
}
- changeStatus = (tab) => {
- if (tab === 'all') {
- this.props.changeStatus('all');
- } else if (tab === 'rejected') {
- this.props.changeStatus('rejected');
- }
+ rejectThenReload = (info) => {
+ this.props.rejectComment(info).then(() => {
+ this.props.data.refetch();
+ });
+ }
+
+ acceptThenReload = (info) => {
+ this.props.acceptComment(info).then(() => {
+ this.props.data.refetch();
+ });
+ }
+
+ showAll = () => {
+ this.props.changeStatus('all');
+ }
+
+ showRejected = () => {
+ this.props.changeStatus('rejected');
}
render () {
@@ -44,13 +60,17 @@ export default class UserDetail extends React.Component {
rejectedComments,
comments: {nodes}
},
- moderation: {userDetailActiveTab: tab},
+ moderation: {
+ userDetailActiveTab: tab,
+ userDetailSelectedIds: selectedIds
+ },
bannedWords,
suspectWords,
+ toggleSelect,
+ bulkAccept,
+ bulkReject,
showBanUserDialog,
showSuspendUserDialog,
- acceptComment,
- rejectComment,
hideUserDetail
} = this.props;
const localProfile = user.profiles.find((p) => p.provider === 'local');
@@ -94,14 +114,38 @@ export default class UserDetail extends React.Component {
{`${(rejectedPercent).toFixed(1)}%`}
-
+ {
+ selectedIds.length === 0
+ ? (
+
+ )
+ : (
+
+
+
+ {`${selectedIds.length} comments selected`}
+
+ )
+ }
+
{
nodes.map((comment, i) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
+ const selected = selectedIds.indexOf(comment.id) !== -1;
return ;
diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css
index e7bea9430..d10311f74 100644
--- a/client/coral-admin/src/routes/Moderation/components/styles.css
+++ b/client/coral-admin/src/routes/Moderation/components/styles.css
@@ -185,10 +185,6 @@ span {
padding: 0 14px;
}
- &:hover {
- box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
- }
-
&:last-child {
border-bottom: none;
}
@@ -291,7 +287,6 @@ span {
@media (--big-viewport) {
.listItem {
- border: 1px solid #e0e0e0;
margin-bottom: 30px;
&:last-child {
@@ -460,3 +455,15 @@ span {
position: relative;
}
}
+
+.minimal {
+ margin: 0;
+}
+
+.minimalSelection {
+ background-color: #ecf4ff;
+}
+
+.bulkSelectInput {
+ cursor: pointer;
+}
diff --git a/client/coral-admin/src/routes/Moderation/containers/UserDetail.js b/client/coral-admin/src/routes/Moderation/containers/UserDetail.js
index 9afd10efb..0e36ed51a 100644
--- a/client/coral-admin/src/routes/Moderation/containers/UserDetail.js
+++ b/client/coral-admin/src/routes/Moderation/containers/UserDetail.js
@@ -6,7 +6,12 @@ import UserDetail from '../components/UserDetail';
import withQuery from 'coral-framework/hocs/withQuery';
import {getSlotsFragments} from 'coral-framework/helpers/plugins';
import {getDefinitionName} from 'coral-framework/utils';
-import {changeUserDetailStatuses} from 'coral-admin/src/actions/moderation';
+import {
+ changeUserDetailStatuses,
+ clearUserDetailSelections,
+ toggleSelectCommentInUserDetail
+} from 'coral-admin/src/actions/moderation';
+import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import Comment from './Comment';
const commentConnectionFragment = gql`
@@ -31,12 +36,37 @@ class UserDetailContainer extends React.Component {
hideUserDetail: PropTypes.func.isRequired
}
+ // status can be 'ACCEPTED' or 'REJECTED'
+ bulkSetCommentStatus = (status) => {
+ const changes = this.props.moderation.userDetailSelectedIds.map((commentId) => {
+ return this.props.setCommentStatus({commentId, status});
+ });
+
+ Promise.all(changes).then(() => {
+ this.props.data.refetch(); // some comments may have moved out of this tab
+ this.props.clearUserDetailSelections(); // un-select everything
+ });
+ }
+
+ bulkReject = () => {
+ this.bulkSetCommentStatus('REJECTED');
+ }
+
+ bulkAccept = () => {
+ this.bulkSetCommentStatus('ACCEPTED');
+ }
+
render () {
if (!('user' in this.props.root)) {
return null;
}
- return ;
+ return ;
}
}
@@ -79,10 +109,15 @@ const mapStateToProps = (state) => ({
});
const mapDispatchToProps = (dispatch) => ({
- ...bindActionCreators({changeUserDetailStatuses}, dispatch)
+ ...bindActionCreators({
+ changeUserDetailStatuses,
+ clearUserDetailSelections,
+ toggleSelectCommentInUserDetail
+ }, dispatch)
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withUserDetailQuery,
+ withSetCommentStatus,
)(UserDetailContainer);