Move userDetail out of Moderation route

This commit is contained in:
Chi Vinh Le
2017-07-27 23:23:40 +07:00
parent 57fb77008a
commit ce2f8f97e8
29 changed files with 171 additions and 242 deletions
@@ -15,33 +15,11 @@ export const hideShortcutsNote = () => {
return {type: actions.HIDE_SHORTCUTS_NOTE};
};
export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, userId});
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
export const setSortOrder = (order) => ({
type: actions.SET_SORT_ORDER,
order
});
export const changeUserDetailStatuses = (tab) => {
let statuses;
if (tab === 'all') {
statuses = ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'];
} else if (tab === 'rejected') {
statuses = ['REJECTED'];
}
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
};
};
export const toggleStorySearch = (active) => ({
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH
});
@@ -0,0 +1,24 @@
import * as actions from 'constants/moderation';
export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, userId});
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
export const changeUserDetailStatuses = (tab) => {
let statuses;
if (tab === 'all') {
statuses = ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'];
} else if (tab === 'rejected') {
statuses = ['REJECTED'];
}
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
};
};
@@ -1,7 +1,7 @@
import React, {PropTypes} from 'react';
import styles from './ModerationList.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../routes/Moderation/helpers/moderationQueueActionsMap';
import {menuActionsMap} from '../utils/moderationQueueActionsMap';
import t from 'coral-framework/services/i18n';
@@ -0,0 +1,24 @@
.bodyLeave {
position: absolute;
width: 100%;
top: 0;
background-color: white;
opacity: 1.0;
transition: background 400ms, opacity 800ms 1600ms;
pointer-events: none;
}
.bodyLeaveActive {
opacity: 0;
background-color: rgba(255,255,0, 0.2);
}
.bodyEnter {
opacity: 0;
pointer-events: none;
}
.bodyEnterActive {
opacity: 1.0;
transition: opacity 800ms 2400ms;
}
@@ -1,7 +1,7 @@
import React from 'react';
import {murmur3} from 'murmurhash-js';
import {CSSTransitionGroup} from 'react-transition-group';
import styles from './styles.css';
import styles from './CommentAnimatedEdit.css';
export default ({children, body}) => {
return (
@@ -1,10 +1,10 @@
import React, {PropTypes} from 'react';
import Comment from './UserDetailComment';
import styles from './UserDetail.css';
import {Button, Drawer} from 'coral-ui';
import {Button, Drawer, Spinner} from 'coral-ui';
import {Slot} from 'coral-framework/components';
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
import {actionsMap} from '../helpers/moderationQueueActionsMap';
import {actionsMap} from '../utils/moderationQueueActionsMap';
import ClickOutside from 'coral-framework/components/ClickOutside';
export default class UserDetail extends React.Component {
@@ -43,7 +43,17 @@ export default class UserDetail extends React.Component {
this.props.changeStatus('rejected');
}
render () {
renderLoading() {
return (
<ClickOutside onClickOutside={this.props.hideUserDetail}>
<Drawer onClose={this.props.hideUserDetail}>
<Spinner />
</Drawer>
</ClickOutside>
);
}
renderLoaded() {
const {
root: {
user,
@@ -61,6 +71,7 @@ export default class UserDetail extends React.Component {
hideUserDetail,
viewUserDetail,
} = this.props;
const localProfile = user.profiles.find((p) => p.provider === 'local');
let profile;
@@ -160,4 +171,11 @@ export default class UserDetail extends React.Component {
</ClickOutside>
);
}
render () {
if (this.props.loading) {
return this.renderLoading();
}
return this.renderLoaded();
}
}
@@ -0,0 +1,7 @@
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 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';
@@ -10,6 +10,7 @@ import SuspendUserDialog from './SuspendUserDialog';
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
import {checkLogin, handleLogin, requestPasswordReset} from '../actions/auth';
import {can} from 'coral-framework/services/perms';
import UserDetail from 'coral-admin/src/containers/UserDetail';
class LayoutContainer extends Component {
componentWillMount() {
@@ -57,6 +58,7 @@ class LayoutContainer extends Component {
>
<BanUserDialog />
<SuspendUserDialog />
<UserDetail />
{this.props.children}
</Layout>
);
+35 -16
View File
@@ -1,4 +1,4 @@
import React, {PropTypes} from 'react';
import React from 'react';
import {compose, gql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
@@ -6,23 +6,25 @@ import UserDetail from '../components/UserDetail';
import withQuery from 'coral-framework/hocs/withQuery';
import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils';
import {
viewUserDetail,
hideUserDetail,
changeUserDetailStatuses,
clearUserDetailSelections,
toggleSelectCommentInUserDetail
} from 'coral-admin/src/actions/moderation';
toggleSelectCommentInUserDetail,
} from 'coral-admin/src/actions/userDetail';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import Comment from './Comment';
import UserDetailComment from './UserDetailComment';
const commentConnectionFragment = gql`
fragment CoralAdmin_Moderation_CommentConnection on CommentConnection {
nodes {
...${getDefinitionName(Comment.fragments.comment)}
...${getDefinitionName(UserDetailComment.fragments.comment)}
}
hasNextPage
startCursor
endCursor
}
${Comment.fragments.comment}
${UserDetailComment.fragments.comment}
`;
const slots = [
@@ -30,14 +32,10 @@ const slots = [
];
class UserDetailContainer extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
hideUserDetail: PropTypes.func.isRequired
}
// status can be 'ACCEPTED' or 'REJECTED'
bulkSetCommentStatus = (status) => {
const changes = this.props.moderation.userDetailSelectedIds.map((commentId) => {
const changes = this.props.selectedIds.map((commentId) => {
return this.props.setCommentStatus({commentId, status});
});
@@ -55,16 +53,29 @@ class UserDetailContainer extends React.Component {
this.bulkSetCommentStatus('ACCEPTED');
}
acceptComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'ACCEPTED'});
}
rejectComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
}
render () {
if (!('user' in this.props.root)) {
if (!this.props.id) {
return null;
}
const loading = !('user' in this.props.root) || this.props.root.user.id !== this.props.id;
return <UserDetail
bulkReject={this.bulkReject}
bulkAccept={this.bulkAccept}
changeStatus={this.props.changeUserDetailStatuses}
toggleSelect={this.props.toggleSelectCommentInUserDetail}
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
loading={loading}
{...this.props} />;
}
}
@@ -93,22 +104,30 @@ export const withUserDetailQuery = withQuery(gql`
}
${commentConnectionFragment}
`, {
options: ({id, moderation: {userDetailStatuses: statuses}}) => {
options: ({id, statuses}) => {
return {
variables: {author_id: id, statuses}
};
}
},
skip: (ownProps) => !ownProps.id,
});
const mapStateToProps = (state) => ({
moderation: state.moderation.toJS()
id: state.userDetail.userDetailId,
selectedIds: state.userDetail.userDetailSelectedIds,
statuses: state.userDetail.userDetailStatuses,
activeTab: state.userDetail.userDetailActiveTab,
bannedWords: state.settings.toJS().wordlist.banned,
suspectWords: state.settings.toJS().wordlist.suspect,
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
changeUserDetailStatuses,
clearUserDetailSelections,
toggleSelectCommentInUserDetail
toggleSelectCommentInUserDetail,
viewUserDetail,
hideUserDetail,
}, dispatch)
});
+2
View File
@@ -7,11 +7,13 @@ import install from './install';
import config from './config';
import banUserDialog from './banUserDialog';
import suspendUserDialog from './suspendUserDialog';
import userDetail from './userDetail';
export default {
auth,
banUserDialog,
suspendUserDialog,
userDetail,
assets,
settings,
community,
+1 -24
View File
@@ -1,13 +1,9 @@
import {fromJS, Set} from 'immutable';
import {fromJS} from 'immutable';
import * as actions from '../constants/moderation';
const initialState = fromJS({
singleView: false,
modalOpen: false,
userDetailId: null,
userDetailActiveTab: 'all',
userDetailStatuses: ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'],
userDetailSelectedIds: new Set(),
storySearchVisible: false,
storySearchString: '',
shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show',
@@ -18,9 +14,6 @@ export default function moderation (state = initialState, action) {
switch (action.type) {
case actions.MODERATION_CLEAR_STATE:
return initialState;
case actions.SET_ACTIVE_TAB:
return state
.set('activeTab', action.activeTab);
case actions.TOGGLE_MODAL:
return state
.set('modalOpen', action.open);
@@ -30,22 +23,6 @@ export default function moderation (state = initialState, action) {
case actions.HIDE_SHORTCUTS_NOTE:
return state
.set('shortcutsNoteVisible', 'hide');
case actions.VIEW_USER_DETAIL:
return state.set('userDetailId', action.userId);
case actions.HIDE_USER_DETAIL:
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.SHOW_STORY_SEARCH:
return state.set('storySearchVisible', true);
case actions.HIDE_STORY_SEARCH:
@@ -0,0 +1,47 @@
import * as actions from '../constants/userDetail';
const initialState = {
userDetailId: null,
userDetailActiveTab: 'all',
userDetailStatuses: ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'],
userDetailSelectedIds: [],
};
export default function banUserDialog(state = initialState, action) {
switch (action.type) {
case actions.VIEW_USER_DETAIL:
return {
...state,
userDetailId: action.userId,
};
case actions.HIDE_USER_DETAIL:
return {
...state,
userDetailId: null,
userDetailSelectedIds: [],
};
case actions.CLEAR_USER_DETAIL_SELECTIONS:
return {
...state,
userDetailSelectedIds: [],
};
case actions.CHANGE_USER_DETAIL_STATUSES:
return {
...state,
userDetailActiveTab: action.tab,
userDetailStatuses: action.statuses,
};
case actions.SELECT_USER_DETAIL_COMMENT:
return {
...state,
userDetailSelectedIds: [...state.userDetailSelectedIds, action.id],
};
case actions.UNSELECT_USER_DETAIL_COMMENT:
return {
...state,
userDetailSelectedIds: state.userDetailSelectedIds.filter((id) => id !== action.id),
};
default:
return state;
}
}
@@ -1,7 +1,7 @@
import React from 'react';
import styles from './Community.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../../Moderation/helpers/moderationQueueActionsMap';
import {menuActionsMap} from '../../../utils/moderationQueueActionsMap';
import t from 'coral-framework/services/i18n';
@@ -2,10 +2,10 @@ import React, {PropTypes} from 'react';
import {Link} from 'react-router';
import {Icon} from 'coral-ui';
import FlagBox from './FlagBox';
import FlagBox from 'coral-admin/src/components/FlagBox';
import styles from './styles.css';
import CommentType from './CommentType';
import CommentAnimatedEdit from './CommentAnimatedEdit';
import CommentType from 'coral-admin/src/components/CommentType';
import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
import Slot from 'coral-framework/components/Slot';
import {getActionSummary} from 'coral-framework/utils';
import ActionButton from 'coral-admin/src/components/ActionButton';
@@ -6,7 +6,6 @@ import ModerationQueue from './ModerationQueue';
import ModerationMenu from './ModerationMenu';
import ModerationHeader from './ModerationHeader';
import ModerationKeysModal from '../../../components/ModerationKeysModal';
import UserDetail from '../containers/UserDetail';
import StorySearch from '../containers/StorySearch';
export default class Moderation extends Component {
@@ -173,12 +172,6 @@ export default class Moderation extends Component {
open={moderation.modalOpen}
onClose={this.onClose}/>
{moderation.userDetailId && (
<UserDetail
id={moderation.userDetailId}
/>
)}
<StorySearch
assetId={assetId}
moderation={this.props.moderation}
@@ -3,7 +3,7 @@ import React, {PropTypes} from 'react';
import Comment from '../containers/Comment';
import styles from './styles.css';
import EmptyCard from '../../../components/EmptyCard';
import {actionsMap} from '../helpers/moderationQueueActionsMap';
import {actionsMap} from '../../../utils/moderationQueueActionsMap';
import LoadMore from './LoadMore';
import t from 'coral-framework/services/i18n';
import {CSSTransitionGroup} from 'react-transition-group';
@@ -480,31 +480,6 @@ span {
opacity: 1.0;
}
.bodyLeave {
position: absolute;
width: 100%;
top: 0;
background-color: white;
opacity: 1.0;
transition: background 400ms, opacity 800ms 1600ms;
pointer-events: none;
}
.bodyLeaveActive {
opacity: 0;
background-color: rgba(255,255,0, 0.2);
}
.bodyEnter {
opacity: 0;
pointer-events: none;
}
.bodyEnterActive {
opacity: 1.0;
transition: opacity 800ms 2400ms;
}
.editedMarker {
font-style: italic;
color: #666;
@@ -15,13 +15,12 @@ import {handleCommentChange} from '../../../graphql/utils';
import {fetchSettings} from 'actions/settings';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import {viewUserDetail} from '../../../actions/userDetail';
import {
toggleModal,
singleView,
hideShortcutsNote,
toggleStorySearch,
viewUserDetail,
hideUserDetail,
setSortOrder,
storySearchChange,
clearState
@@ -426,7 +425,6 @@ const mapDispatchToProps = (dispatch) => ({
toggleStorySearch,
showSuspendUserDialog,
viewUserDetail,
hideUserDetail,
setSortOrder,
storySearchChange,
clearState
@@ -1,135 +0,0 @@
import React, {PropTypes} from 'react';
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 {viewUserDetail, hideUserDetail} from 'actions/moderation';
import {
changeUserDetailStatuses,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
} from 'coral-admin/src/actions/moderation';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import UserDetailComment from './UserDetailComment';
const commentConnectionFragment = gql`
fragment CoralAdmin_Moderation_CommentConnection on CommentConnection {
nodes {
...${getDefinitionName(UserDetailComment.fragments.comment)}
}
hasNextPage
startCursor
endCursor
}
${UserDetailComment.fragments.comment}
`;
const slots = [
'userProfile',
];
class UserDetailContainer extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
}
// status can be 'ACCEPTED' or 'REJECTED'
bulkSetCommentStatus = (status) => {
const changes = this.props.selectedIds.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');
}
acceptComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'ACCEPTED'});
}
rejectComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
}
render () {
if (!('user' in this.props.root)) {
return null;
}
return <UserDetail
bulkReject={this.bulkReject}
bulkAccept={this.bulkAccept}
changeStatus={this.props.changeUserDetailStatuses}
toggleSelect={this.props.toggleSelectCommentInUserDetail}
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
{...this.props} />;
}
}
export const withUserDetailQuery = withQuery(gql`
query CoralAdmin_UserDetail($author_id: ID!, $statuses: [COMMENT_STATUS!]) {
user(id: $author_id) {
id
username
created_at
profiles {
id
provider
}
${getSlotFragmentSpreads(slots, 'user')}
}
totalComments: commentCount(query: {author_id: $author_id})
rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]})
comments: comments(query: {
author_id: $author_id,
statuses: $statuses
}) {
...CoralAdmin_Moderation_CommentConnection
}
${getSlotFragmentSpreads(slots, 'root')}
}
${commentConnectionFragment}
`, {
options: ({id, statuses}) => {
return {
variables: {author_id: id, statuses}
};
}
});
const mapStateToProps = (state) => ({
selectedIds: state.moderation.toJS().userDetailSelectedIds,
statuses: state.moderation.toJS().userDetailStatuses,
activeTab: state.moderation.toJS().userDetailActiveTab,
bannedWords: state.settings.toJS().wordlist.banned,
suspectWords: state.settings.toJS().wordlist.suspect,
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
changeUserDetailStatuses,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
viewUserDetail,
hideUserDetail,
}, dispatch)
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withUserDetailQuery,
withSetCommentStatus,
)(UserDetailContainer);