mirror of
https://github.com/wassname/talk.git
synced 2026-09-09 11:38:08 +08:00
Merge branch 'master' into sory-139595043-qbox
This commit is contained in:
@@ -18,6 +18,9 @@ const routes = (
|
|||||||
<Route path='community' component={CommunityContainer} />
|
<Route path='community' component={CommunityContainer} />
|
||||||
<Route path='configure' component={Configure} />
|
<Route path='configure' component={Configure} />
|
||||||
<Route path='streams' component={Streams} />
|
<Route path='streams' component={Streams} />
|
||||||
|
|
||||||
|
<Route path='moderate' component={ModerationContainer} />
|
||||||
|
<Route path='moderate/:id' component={ModerationContainer} />
|
||||||
</Route>
|
</Route>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import {
|
|||||||
FETCH_ASSETS_FAILURE,
|
FETCH_ASSETS_FAILURE,
|
||||||
UPDATE_ASSET_STATE_REQUEST,
|
UPDATE_ASSET_STATE_REQUEST,
|
||||||
UPDATE_ASSET_STATE_SUCCESS,
|
UPDATE_ASSET_STATE_SUCCESS,
|
||||||
UPDATE_ASSET_STATE_FAILURE
|
UPDATE_ASSET_STATE_FAILURE,
|
||||||
|
UPDATE_ASSETS
|
||||||
} from '../constants/assets';
|
} from '../constants/assets';
|
||||||
|
|
||||||
import coralApi from '../../../coral-framework/helpers/response';
|
import coralApi from '../../../coral-framework/helpers/response';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,3 +36,7 @@ export const updateAssetState = (id, closedAt) => (dispatch) => {
|
|||||||
dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
|
dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
|
||||||
.catch(error => dispatch({type: UPDATE_ASSET_STATE_FAILURE, error}));
|
.catch(error => dispatch({type: UPDATE_ASSET_STATE_FAILURE, error}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateAssets = assets => dispatch => {
|
||||||
|
dispatch({type: UPDATE_ASSETS, assets});
|
||||||
|
};
|
||||||
|
|||||||
@@ -101,12 +101,3 @@ export const flagComment = id => (dispatch, getState) => {
|
|||||||
dispatch({type: commentTypes.COMMENT_FLAG, id});
|
dispatch({type: commentTypes.COMMENT_FLAG, id});
|
||||||
dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)});
|
dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dialog Actions
|
|
||||||
export const showBanUserDialog = (userId, userName, commentId) => {
|
|
||||||
return {type: commentTypes.SHOW_BANUSER_DIALOG, userId, userName, commentId};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hideBanUserDialog = (showDialog) => {
|
|
||||||
return {type: commentTypes.HIDE_BANUSER_DIALOG, showDialog};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import * as actions from 'constants/moderation';
|
||||||
|
|
||||||
|
export const setActiveTab = activeTab => ({type: actions.SET_ACTIVE_TAB, activeTab});
|
||||||
|
export const toggleModal = open => ({type: actions.TOGGLE_MODAL, open});
|
||||||
|
export const singleView = () => ({type: actions.SINGLE_VIEW});
|
||||||
|
|
||||||
|
// Ban User Dialog
|
||||||
|
export const showBanUserDialog = (user, commentId) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId});
|
||||||
|
export const hideBanUserDialog = (showDialog) => ({type: actions.HIDE_BANUSER_DIALOG, showDialog});
|
||||||
@@ -1,48 +1,22 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styles from './ModerationList.css';
|
import styles from './ModerationList.css';
|
||||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
import BanUserButton from './BanUserButton';
|
||||||
import translations from '../translations.json';
|
import {FabButton} from 'coral-ui';
|
||||||
import {FabButton, Button, Icon} from 'coral-ui';
|
import {menuActionsMap} from '../containers/ModerationQueue/helpers/moderationQueueActionsMap';
|
||||||
|
|
||||||
const ActionButton = ({option, type, comment = {}, user, menuOptionsMap, onClickAction, onClickShowBanDialog}) =>
|
const ActionButton = ({type = '', user, ...props}) => {
|
||||||
{
|
if (type === 'BAN') {
|
||||||
const banned = user.status === 'BANNED';
|
return <BanUserButton user={user} onClick={() => props.showBanUserDialog(props.user, props.id)} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (option === 'flag' && (type === 'USERS' || comment.status || comment.flagged === true)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (option === 'ban') {
|
|
||||||
return (
|
|
||||||
<div className={styles.ban}>
|
|
||||||
<Button
|
|
||||||
className={`ban ${styles.banButton}`}
|
|
||||||
cStyle='darkGrey'
|
|
||||||
disabled={banned ? 'disabled' : ''}
|
|
||||||
onClick={() => onClickShowBanDialog(user.id, user.username, comment.id)
|
|
||||||
}
|
|
||||||
raised
|
|
||||||
>
|
|
||||||
<Icon name='not_interested' className={styles.banIcon} />
|
|
||||||
{lang.t('comment.ban_user')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const menuOption = menuOptionsMap[option];
|
|
||||||
const action = {
|
|
||||||
item_type: type,
|
|
||||||
item_id: type === 'COMMENTS' ? comment.id : user.id
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<FabButton
|
<FabButton
|
||||||
className={`${option} ${styles.actionButton}`}
|
className={`${type.toLowerCase()} ${styles.actionButton}`}
|
||||||
cStyle={option}
|
cStyle={type.toLowerCase()}
|
||||||
icon={menuOption.icon}
|
icon={menuActionsMap[type].icon}
|
||||||
onClick={() => onClickAction(menuOption.status, type === 'COMMENTS' ? comment : user, action)}
|
onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ActionButton;
|
export default ActionButton;
|
||||||
|
|
||||||
const lang = new I18n(translations);
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.banButton {
|
||||||
|
width: 114px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import React, {PropTypes} from 'react';
|
||||||
|
import {Button, Icon} from 'coral-ui';
|
||||||
|
import styles from './BanUserButton.css';
|
||||||
|
|
||||||
|
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||||
|
import translations from 'coral-admin/src/translations.json';
|
||||||
|
const lang = new I18n(translations);
|
||||||
|
|
||||||
|
const BanUserButton = ({user, ...props}) => (
|
||||||
|
<div className={styles.ban}>
|
||||||
|
<Button cStyle='darkGrey'
|
||||||
|
className={`ban ${styles.banButton}`}
|
||||||
|
disabled={user.status === 'BANNED' ? 'disabled' : ''}
|
||||||
|
onClick={props.onClick}
|
||||||
|
raised>
|
||||||
|
<Icon name='not_interested' />
|
||||||
|
{lang.t('comment.ban_user')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
BanUserButton.propTypes = {
|
||||||
|
onClick: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BanUserButton;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, {PropTypes} from 'react';
|
||||||
import {Dialog} from 'coral-ui';
|
import {Dialog} from 'coral-ui';
|
||||||
import styles from './BanUserDialog.css';
|
import styles from './BanUserDialog.css';
|
||||||
|
|
||||||
@@ -8,34 +8,28 @@ import I18n from 'coral-framework/modules/i18n/i18n';
|
|||||||
import translations from '../translations';
|
import translations from '../translations';
|
||||||
const lang = new I18n(translations);
|
const lang = new I18n(translations);
|
||||||
|
|
||||||
const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => (
|
const BanUserDialog = ({open, handleClose, handleBanUser, user}) => (
|
||||||
<Dialog
|
<Dialog
|
||||||
className={styles.dialog}
|
className={styles.dialog}
|
||||||
id="banuserDialog"
|
id="banuserDialog"
|
||||||
open={open}
|
open={open}
|
||||||
onClose={() => handleClose()}
|
onClose={handleClose}
|
||||||
onCancel={() => handleClose()}
|
onCancel={handleClose}
|
||||||
title={lang.t('bandialog.ban_user')}>
|
title={lang.t('bandialog.ban_user')}>
|
||||||
<span className={styles.close} onClick={handleClose}>×</span>
|
<span className={styles.close} onClick={handleClose}>×</span>
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<h2>
|
<h2>{lang.t('bandialog.ban_user')}</h2>
|
||||||
{lang.t('bandialog.ban_user')}
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.separator}>
|
<div className={styles.separator}>
|
||||||
<h3>
|
<h3>{lang.t('bandialog.are_you_sure', user.name)}</h3>
|
||||||
{lang.t('bandialog.are_you_sure', user.userName)}
|
<i>{lang.t('bandialog.note')}</i>
|
||||||
</h3>
|
|
||||||
<i>
|
|
||||||
{lang.t('bandialog.note')}
|
|
||||||
</i>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
<Button cStyle="cancel" className={styles.cancel} onClick={() => handleClose()} raised>
|
<Button cStyle="cancel" className={styles.cancel} onClick={handleClose} raised>
|
||||||
{lang.t('bandialog.cancel')}
|
{lang.t('bandialog.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button cStyle="black" className={styles.ban} onClick={() => onClickBanUser('BANNED', user.userId, user.commentId)} raised>
|
<Button cStyle="black" className={styles.ban} onClick={() => handleBanUser({userId: user.id})} raised>
|
||||||
{lang.t('bandialog.yes_ban_user')}
|
{lang.t('bandialog.yes_ban_user')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,4 +37,10 @@ const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => (
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
BanUserDialog.propTypes = {
|
||||||
|
handleBanUser: PropTypes.func.isRequired,
|
||||||
|
handleClose: PropTypes.func.isRequired,
|
||||||
|
user: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default BanUserDialog;
|
export default BanUserDialog;
|
||||||
|
|||||||
@@ -96,11 +96,6 @@
|
|||||||
margin-left: 40px;
|
margin-left: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionButton {
|
|
||||||
transform: scale(.8);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -182,3 +177,9 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.actionButton {
|
||||||
|
transform: scale(.8);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,9 +25,8 @@ const User = props => {
|
|||||||
{props.modActions.map(
|
{props.modActions.map(
|
||||||
(action, i) =>
|
(action, i) =>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
option={action}
|
type={action.toUpperCase()}
|
||||||
key={i}
|
key={i}
|
||||||
type='USERS'
|
|
||||||
user={user}
|
user={user}
|
||||||
menuOptionsMap={props.menuOptionsMap}
|
menuOptionsMap={props.menuOptionsMap}
|
||||||
onClickAction={props.onClickAction}
|
onClickAction={props.onClickAction}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
.rightPanel {
|
.rightPanel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 170px;
|
width: 170px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -13,10 +13,14 @@ export default ({handleLogout, restricted = false}) => (
|
|||||||
!restricted ?
|
!restricted ?
|
||||||
<div>
|
<div>
|
||||||
<Navigation className={styles.nav}>
|
<Navigation className={styles.nav}>
|
||||||
<IndexLink className={styles.navLink} to="/admin"
|
<IndexLink className={styles.navLink} to="/admin/moderate"
|
||||||
activeClassName={styles.active}>
|
activeClassName={styles.active}>
|
||||||
{lang.t('configure.moderate')}
|
{lang.t('configure.moderate')}
|
||||||
</IndexLink>
|
</IndexLink>
|
||||||
|
<Link className={styles.navLink} to="/admin/streams"
|
||||||
|
activeClassName={styles.active}>
|
||||||
|
{lang.t('configure.streams')}
|
||||||
|
</Link>
|
||||||
<Link className={styles.navLink} to="/admin/community"
|
<Link className={styles.navLink} to="/admin/community"
|
||||||
activeClassName={styles.active}>
|
activeClassName={styles.active}>
|
||||||
{lang.t('configure.community')}
|
{lang.t('configure.community')}
|
||||||
@@ -25,10 +29,6 @@ export default ({handleLogout, restricted = false}) => (
|
|||||||
activeClassName={styles.active}>
|
activeClassName={styles.active}>
|
||||||
{lang.t('configure.configure')}
|
{lang.t('configure.configure')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link className={styles.navLink} to="/admin/streams"
|
|
||||||
activeClassName={styles.active}>
|
|
||||||
{lang.t('configure.streams')}
|
|
||||||
</Link>
|
|
||||||
</Navigation>
|
</Navigation>
|
||||||
<div className={styles.rightPanel}>
|
<div className={styles.rightPanel}>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
export const FETCH_ASSETS_REQUEST = 'FETCH_ASSETS_REQUEST';
|
export const FETCH_ASSETS_REQUEST = 'FETCH_ASSETS_REQUEST';
|
||||||
export const FETCH_ASSETS_SUCCESS = 'FETCH_ASSETS_SUCCESS';
|
export const FETCH_ASSETS_SUCCESS = 'FETCH_ASSETS_SUCCESS';
|
||||||
export const FETCH_ASSETS_FAILURE = 'FETCH_ASSETS_FAILURE';
|
export const FETCH_ASSETS_FAILURE = 'FETCH_ASSETS_FAILURE';
|
||||||
|
|
||||||
export const UPDATE_ASSET_STATE_REQUEST = 'UPDATE_ASSET_STATE_REQUEST';
|
export const UPDATE_ASSET_STATE_REQUEST = 'UPDATE_ASSET_STATE_REQUEST';
|
||||||
export const UPDATE_ASSET_STATE_SUCCESS = 'UPDATE_ASSET_STATE_SUCCESS';
|
export const UPDATE_ASSET_STATE_SUCCESS = 'UPDATE_ASSET_STATE_SUCCESS';
|
||||||
export const UPDATE_ASSET_STATE_FAILURE = 'UPDATE_ASSET_STATE_FAILURE';
|
export const UPDATE_ASSET_STATE_FAILURE = 'UPDATE_ASSET_STATE_FAILURE';
|
||||||
|
|
||||||
|
export const UPDATE_ASSETS = 'UPDATE_ASSETS';
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export const SET_ACTIVE_TAB = 'SET_ACTIVE_TAB';
|
||||||
|
export const TOGGLE_MODAL = 'TOGGLE_MODAL';
|
||||||
|
export const SINGLE_VIEW = 'SINGLE_VIEW';
|
||||||
|
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
|
||||||
|
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
|
||||||
@@ -1,42 +1,33 @@
|
|||||||
import React from 'react';
|
import React, {Component} from 'react';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
|
import {compose} from 'react-apollo';
|
||||||
import key from 'keymaster';
|
import key from 'keymaster';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
|
|
||||||
|
import {modQueueQuery} from '../../graphql/queries';
|
||||||
|
import {banUser, setCommentStatus} from '../../graphql/mutations';
|
||||||
|
|
||||||
import {
|
|
||||||
updateStatus,
|
|
||||||
showBanUserDialog,
|
|
||||||
hideBanUserDialog,
|
|
||||||
fetchPremodQueue,
|
|
||||||
fetchRejectedQueue,
|
|
||||||
fetchFlaggedQueue,
|
|
||||||
fetchModerationQueueComments,
|
|
||||||
} from 'actions/comments';
|
|
||||||
import {userStatusUpdate, sendNotificationEmail, enableUsernameEdit} from 'actions/users';
|
|
||||||
import {fetchSettings} from 'actions/settings';
|
import {fetchSettings} from 'actions/settings';
|
||||||
|
import {updateAssets} from 'actions/assets';
|
||||||
|
import {setActiveTab, toggleModal, singleView, showBanUserDialog, hideBanUserDialog} from 'actions/moderation';
|
||||||
|
|
||||||
|
import {Spinner} from 'coral-ui';
|
||||||
|
import BanUserDialog from '../../components/BanUserDialog';
|
||||||
import ModerationQueue from './ModerationQueue';
|
import ModerationQueue from './ModerationQueue';
|
||||||
|
import ModerationMenu from './components/ModerationMenu';
|
||||||
|
import ModerationHeader from './components/ModerationHeader';
|
||||||
|
import NotFoundAsset from './components/NotFoundAsset';
|
||||||
|
|
||||||
class ModerationContainer extends React.Component {
|
class ModerationContainer extends Component {
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
activeTab: 'all',
|
|
||||||
singleView: false,
|
|
||||||
modalOpen: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this.onClose = this.onClose.bind(this);
|
|
||||||
this.onTabClick = this.onTabClick.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.props.fetchModerationQueueComments();
|
const {toggleModal, singleView} = this.props;
|
||||||
|
|
||||||
this.props.fetchSettings();
|
this.props.fetchSettings();
|
||||||
key('s', () => this.setState({singleView: !this.state.singleView}));
|
|
||||||
key('shift+/', () => this.setState({modalOpen: true}));
|
key('s', () => singleView());
|
||||||
key('esc', () => this.setState({modalOpen: false}));
|
key('shift+/', () => toggleModal(true));
|
||||||
|
key('esc', () => toggleModal(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -45,90 +36,85 @@ class ModerationContainer extends React.Component {
|
|||||||
key.unbind('esc');
|
key.unbind('esc');
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentWillReceiveProps(nextProps) {
|
||||||
|
const {updateAssets} = this.props;
|
||||||
// Hack for dynamic mdl tabs
|
if(!isEqual(nextProps.data.assets, this.props.data.assets)) {
|
||||||
if (typeof componentHandler !== 'undefined') {
|
updateAssets(nextProps.data.assets);
|
||||||
|
|
||||||
// FIXME: fix this hack
|
|
||||||
componentHandler.upgradeAllRegistered(); // eslint-disable-line no-undef
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTabClick(activeTab) {
|
|
||||||
this.setState({activeTab});
|
|
||||||
|
|
||||||
if (activeTab === 'premod') {
|
|
||||||
this.props.fetchPremodQueue();
|
|
||||||
} else if (activeTab === 'rejected') {
|
|
||||||
this.props.fetchRejectedQueue();
|
|
||||||
} else if (activeTab === 'flagged') {
|
|
||||||
this.props.fetchFlaggedQueue();
|
|
||||||
} else {
|
|
||||||
this.props.fetchModerationQueueComments();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose() {
|
|
||||||
this.setState({modalOpen: false});
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {comments, actions, settings} = this.props;
|
const {data, moderation, settings, assets, ...props} = this.props;
|
||||||
const premodIds = comments.ids.filter(id => comments.byId[id].status === 'PREMOD');
|
const providedAssetId = this.props.params.id;
|
||||||
const rejectedIds = comments.ids.filter(id => comments.byId[id].status === 'REJECTED');
|
let asset;
|
||||||
const flaggedIds = comments.ids.filter(id =>
|
|
||||||
comments.byId[id].flagged === true &&
|
|
||||||
comments.byId[id].status !== 'REJECTED' &&
|
|
||||||
comments.byId[id].status !== 'ACCEPTED'
|
|
||||||
);
|
|
||||||
const userActionIds = actions.ids.filter(id => actions.byId[id].item_type === 'USERS');
|
|
||||||
|
|
||||||
// show the Pre-Mod tab if premod is enabled globally OR there are pre-mod comments in the db.
|
if (data.loading) {
|
||||||
let enablePremodTab = (settings.settings && settings.settings.moderation === 'PRE') || premodIds.length;
|
return <div><Spinner/></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
console.log(data);
|
||||||
|
return <div>Error</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (providedAssetId) {
|
||||||
|
asset = assets.find(asset => asset.id === this.props.params.id);
|
||||||
|
|
||||||
|
if (!asset) {
|
||||||
|
return <NotFoundAsset assetId={providedAssetId} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enablePremodTab = !!data.premod.length;
|
||||||
return (
|
return (
|
||||||
<ModerationQueue
|
<div>
|
||||||
enablePremodTab={enablePremodTab}
|
<ModerationHeader asset={asset} />
|
||||||
onTabClick={this.onTabClick}
|
<ModerationMenu
|
||||||
onClose={this.onClose}
|
onTabClick={props.onTabClick}
|
||||||
premodIds={premodIds}
|
enablePremodTab={enablePremodTab}
|
||||||
userActionIds={userActionIds}
|
activeTab={moderation.activeTab}
|
||||||
rejectedIds={rejectedIds}
|
/>
|
||||||
flaggedIds={flaggedIds}
|
<ModerationQueue
|
||||||
{...this.props}
|
data={data}
|
||||||
{...this.state}
|
currentAsset={asset}
|
||||||
/>
|
activeTab={moderation.activeTab}
|
||||||
|
enablePremodTab={enablePremodTab}
|
||||||
|
suspectWords={settings.wordlist.suspect}
|
||||||
|
showBanUserDialog={props.showBanUserDialog}
|
||||||
|
acceptComment={props.acceptComment}
|
||||||
|
rejectComment={props.rejectComment}
|
||||||
|
/>
|
||||||
|
<BanUserDialog
|
||||||
|
open={moderation.banDialog}
|
||||||
|
user={moderation.user}
|
||||||
|
handleClose={props.hideBanUserDialog}
|
||||||
|
handleBanUser={props.banUser}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
comments: state.comments.toJS(),
|
moderation: state.moderation.toJS(),
|
||||||
settings: state.settings.toJS(),
|
settings: state.settings.toJS(),
|
||||||
users: state.users.toJS(),
|
assets: state.assets.get('assets')
|
||||||
actions: state.actions.toJS(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => ({
|
||||||
return {
|
onTabClick: activeTab => dispatch(setActiveTab(activeTab)),
|
||||||
fetchSettings: () => dispatch(fetchSettings()),
|
toggleModal: toggle => dispatch(toggleModal(toggle)),
|
||||||
fetchModerationQueueComments: () => dispatch(fetchModerationQueueComments()),
|
onClose: () => dispatch(toggleModal(false)),
|
||||||
fetchPremodQueue: () => dispatch(fetchPremodQueue()),
|
singleView: () => dispatch(singleView()),
|
||||||
fetchRejectedQueue: () => dispatch(fetchRejectedQueue()),
|
updateAssets: assets => dispatch(updateAssets(assets)),
|
||||||
fetchFlaggedQueue: () => dispatch(fetchFlaggedQueue()),
|
fetchSettings: () => dispatch(fetchSettings()),
|
||||||
showBanUserDialog: (userId, userName, commentId) => dispatch(showBanUserDialog(userId, userName, commentId)),
|
showBanUserDialog: (user, commentId) => dispatch(showBanUserDialog(user, commentId)),
|
||||||
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
|
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
|
||||||
userStatusUpdate: (status, userId, commentId) => dispatch(userStatusUpdate(status, userId, commentId)).then(() => {
|
});
|
||||||
dispatch(fetchModerationQueueComments());
|
|
||||||
}),
|
|
||||||
suspendUser: (userId, subject, text) => dispatch(userStatusUpdate('BANNED', userId))
|
|
||||||
.then(() => dispatch(enableUsernameEdit(userId)))
|
|
||||||
.then(() => dispatch(sendNotificationEmail(userId, subject, text)))
|
|
||||||
.then(() => dispatch(fetchModerationQueueComments()))
|
|
||||||
,
|
|
||||||
updateStatus: (action, comment) => dispatch(updateStatus(action, comment))
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ModerationContainer);
|
export default compose(
|
||||||
|
connect(mapStateToProps, mapDispatchToProps),
|
||||||
|
setCommentStatus,
|
||||||
|
modQueueQuery,
|
||||||
|
banUser
|
||||||
|
)(ModerationContainer);
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
@custom-media --big-viewport (min-width: 780px);
|
|
||||||
|
|
||||||
.listContainer {
|
|
||||||
max-width: 860px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabBar {
|
|
||||||
background: #262626;
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
flex: 1;
|
|
||||||
color: white;
|
|
||||||
text-transform: capitalize;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 15px;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
transition: border-bottom 200ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
color: white;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-bottom: solid 5px #F36451;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active > span {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active:after {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showShortcuts {
|
|
||||||
position: absolute;
|
|
||||||
right: 130px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
span {
|
|
||||||
margin-left: 7px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (--big-viewport) {
|
|
||||||
.tab {
|
|
||||||
flex: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,212 +1,34 @@
|
|||||||
import React, {PropTypes} from 'react';
|
import React, {PropTypes} from 'react';
|
||||||
import styles from './ModerationQueue.css';
|
|
||||||
|
|
||||||
import ModerationKeysModal from 'components/ModerationKeysModal';
|
import Comment from './components/Comment';
|
||||||
import ModerationList from 'components/ModerationList';
|
import {actionsMap} from './helpers/moderationQueueActionsMap';
|
||||||
import BanUserDialog from 'components/BanUserDialog';
|
|
||||||
|
|
||||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
const ModerationQueue = props => {
|
||||||
import translations from '../../translations.json';
|
return (
|
||||||
|
<div id="moderationList">
|
||||||
const lang = new I18n(translations);
|
<ul>
|
||||||
|
|
||||||
const ModerationQueue = (props) => (
|
|
||||||
<div>
|
|
||||||
<div className='mdl-tabs'>
|
|
||||||
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
|
|
||||||
<a href='#all'
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
props.onTabClick('all');
|
|
||||||
}}
|
|
||||||
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'all' ? styles.active : ''}`}
|
|
||||||
>
|
|
||||||
{lang.t('modqueue.all')}
|
|
||||||
</a>
|
|
||||||
{
|
|
||||||
props.enablePremodTab
|
|
||||||
? <a href='#premod'
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
props.onTabClick('premod');
|
|
||||||
}}
|
|
||||||
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'premod' ? styles.active : ''}`}>
|
|
||||||
{lang.t('modqueue.premod')}
|
|
||||||
</a>
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
<a href='#account'
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
props.onTabClick('account');
|
|
||||||
}}
|
|
||||||
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'account' ? styles.active : ''}`}>
|
|
||||||
{lang.t('modqueue.account')}
|
|
||||||
</a>
|
|
||||||
<a href='#rejected'
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
props.onTabClick('rejected');
|
|
||||||
}}
|
|
||||||
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'rejected' ? styles.active : ''}`}
|
|
||||||
>
|
|
||||||
{lang.t('modqueue.rejected')}
|
|
||||||
</a>
|
|
||||||
<a href='#flagged'
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
props.onTabClick('flagged');
|
|
||||||
}}
|
|
||||||
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'flagged' ? styles.active : ''}`}
|
|
||||||
>
|
|
||||||
{lang.t('modqueue.flagged')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='all'>
|
|
||||||
{
|
|
||||||
props.activeTab === 'all' &&
|
|
||||||
<div>
|
|
||||||
<ModerationList
|
|
||||||
suspectWords={props.settings.settings.wordlist.suspect}
|
|
||||||
isActive={props.activeTab === 'all'}
|
|
||||||
singleView={props.singleView}
|
|
||||||
commentIds={[...props.premodIds, ...props.flaggedIds]}
|
|
||||||
comments={props.comments.byId}
|
|
||||||
users={props.users.byId}
|
|
||||||
actionIds={props.userActionIds}
|
|
||||||
actions={props.actions.byId}
|
|
||||||
userStatusUpdate={props.userStatusUpdate}
|
|
||||||
suspendUser={props.suspendUser}
|
|
||||||
updateCommentStatus={props.updateStatus}
|
|
||||||
onClickShowBanDialog={props.showBanUserDialog}
|
|
||||||
modActions={['reject', 'approve', 'ban']}
|
|
||||||
loading={props.comments.loading}/>
|
|
||||||
<BanUserDialog
|
|
||||||
open={props.comments.showBanUserDialog}
|
|
||||||
handleClose={props.hideBanUserDialog}
|
|
||||||
onClickBanUser={props.userStatusUpdate}
|
|
||||||
user={props.comments.banUser}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
{
|
{
|
||||||
props.enablePremodTab
|
props.data[props.activeTab].map((comment, i) => {
|
||||||
? <div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='premod'>
|
return <Comment
|
||||||
{
|
key={i}
|
||||||
props.activeTab === 'premod' &&
|
index={i}
|
||||||
<div>
|
comment={comment}
|
||||||
<ModerationList
|
suspectWords={props.suspectWords}
|
||||||
suspectWords={props.settings.settings.wordlist.suspect}
|
actions={actionsMap[comment.status]}
|
||||||
isActive={props.activeTab === 'premod'}
|
showBanUserDialog={props.showBanUserDialog}
|
||||||
singleView={props.singleView}
|
acceptComment={props.acceptComment}
|
||||||
commentIds={props.premodIds}
|
rejectComment={props.rejectComment}
|
||||||
comments={props.comments.byId}
|
currentAsset={props.currentAsset}
|
||||||
users={props.users.byId}
|
/>;
|
||||||
actions={props.actions.byId}
|
})
|
||||||
userStatusUpdate={props.userStatusUpdate}
|
|
||||||
suspendUser={props.suspendUser}
|
|
||||||
updateCommentStatus={props.updateStatus}
|
|
||||||
onClickShowBanDialog={props.showBanUserDialog}
|
|
||||||
modActions={['reject', 'approve', 'ban']}
|
|
||||||
loading={props.comments.loading}/>
|
|
||||||
<BanUserDialog
|
|
||||||
open={props.comments.showBanUserDialog}
|
|
||||||
handleClose={props.hideBanUserDialog}
|
|
||||||
onClickBanUser={props.userStatusUpdate}
|
|
||||||
user={props.comments.banUser}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
: null
|
|
||||||
}
|
}
|
||||||
|
</ul>
|
||||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='account'>
|
|
||||||
{
|
|
||||||
props.activeTab === 'account' &&
|
|
||||||
<div>
|
|
||||||
<ModerationList
|
|
||||||
suspectWords={props.settings.settings.wordlist.suspect}
|
|
||||||
isActive={props.activeTab === 'account'}
|
|
||||||
singleView={props.singleView}
|
|
||||||
users={props.users.byId}
|
|
||||||
actionIds={props.userActionIds}
|
|
||||||
actions={props.actions.byId}
|
|
||||||
userStatusUpdate={props.userStatusUpdate}
|
|
||||||
suspendUser={props.suspendUser}
|
|
||||||
updateCommentStatus={props.updateStatus}
|
|
||||||
onClickShowBanDialog={props.showBanUserDialog}
|
|
||||||
modActions={['reject', 'approve', 'ban']}
|
|
||||||
loading={props.comments.loading}/>
|
|
||||||
<BanUserDialog
|
|
||||||
open={props.comments.showBanUserDialog}
|
|
||||||
handleClose={props.hideBanUserDialog}
|
|
||||||
onClickBanUser={props.userStatusUpdate}
|
|
||||||
user={props.comments.banUser}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='flagged'>
|
|
||||||
{
|
|
||||||
props.activeTab === 'flagged' &&
|
|
||||||
<div>
|
|
||||||
<ModerationList
|
|
||||||
suspectWords={props.settings.settings.wordlist.suspect}
|
|
||||||
isActive={props.activeTab === 'flagged'}
|
|
||||||
singleView={props.singleView}
|
|
||||||
commentIds={props.flaggedIds}
|
|
||||||
userStatusUpdate={props.userStatusUpdate}
|
|
||||||
suspendUser={props.suspendUser}
|
|
||||||
comments={props.comments.byId}
|
|
||||||
users={props.users.byId}
|
|
||||||
updateCommentStatus={props.updateStatus}
|
|
||||||
modActions={['reject', 'approve']}
|
|
||||||
loading={props.comments.loading}/>
|
|
||||||
<BanUserDialog
|
|
||||||
open={props.comments.showBanUserDialog}
|
|
||||||
handleClose={props.hideBanUserDialog}
|
|
||||||
onClickBanUser={props.userStatusUpdate}
|
|
||||||
user={props.comments.banUser}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
|
|
||||||
{
|
|
||||||
props.activeTab === 'rejected' &&
|
|
||||||
<div>
|
|
||||||
<ModerationList
|
|
||||||
suspectWords={props.settings.settings.wordlist.suspect}
|
|
||||||
isActive={props.activeTab === 'rejected'}
|
|
||||||
singleView={props.singleView}
|
|
||||||
commentIds={props.rejectedIds}
|
|
||||||
userStatusUpdate={props.userStatusUpdate}
|
|
||||||
suspendUser={props.suspendUser}
|
|
||||||
comments={props.comments.byId}
|
|
||||||
users={props.users.byId}
|
|
||||||
updateCommentStatus={props.updateStatus}
|
|
||||||
modActions={['approve']}
|
|
||||||
loading={props.comments.loading}
|
|
||||||
/>
|
|
||||||
<BanUserDialog
|
|
||||||
open={props.comments.showBanUserDialog}
|
|
||||||
handleClose={props.hideBanUserDialog}
|
|
||||||
onClickBanUser={props.userStatusUpdate}
|
|
||||||
user={props.comments.banUser}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ModerationKeysModal open={props.modalOpen} onClose={props.closeModal} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
ModerationQueue.propTypes = {
|
ModerationQueue.propTypes = {
|
||||||
enablePremodTab: PropTypes.bool.isRequired
|
data: PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ModerationQueue;
|
export default ModerationQueue;
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import timeago from 'timeago.js';
|
||||||
|
import Linkify from 'react-linkify';
|
||||||
|
import Highlighter from 'react-highlight-words';
|
||||||
|
import {Link} from 'react-router';
|
||||||
|
|
||||||
|
import styles from './styles.css';
|
||||||
|
import {Icon} from 'coral-ui';
|
||||||
|
import ActionButton from '../../../components/ActionButton';
|
||||||
|
import FlagBox from './FlagBox';
|
||||||
|
|
||||||
|
const linkify = new Linkify();
|
||||||
|
|
||||||
|
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||||
|
import translations from 'coral-admin/src/translations.json';
|
||||||
|
const lang = new I18n(translations);
|
||||||
|
|
||||||
|
const Comment = ({actions = [], ...props}) => {
|
||||||
|
const links = linkify.getMatches(props.comment.body);
|
||||||
|
const actionSumaries = props.comment.action_summaries;
|
||||||
|
return (
|
||||||
|
<li tabIndex={props.index}
|
||||||
|
className={`mdl-card mdl-shadow--2dp ${styles.Comment} ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
|
||||||
|
<div className={styles.itemHeader}>
|
||||||
|
<div className={styles.author}>
|
||||||
|
<span>{props.comment.user.name}</span>
|
||||||
|
<span className={styles.created}>
|
||||||
|
{timeago().format(props.comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
|
||||||
|
</span>
|
||||||
|
{props.comment.action_summaries ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
|
||||||
|
</div>
|
||||||
|
<div className={styles.sideActions}>
|
||||||
|
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
|
||||||
|
<div className={`actions ${styles.actions}`}>
|
||||||
|
{actions.map((action, i) =>
|
||||||
|
<ActionButton key={i}
|
||||||
|
type={action}
|
||||||
|
user={props.comment.user}
|
||||||
|
acceptComment={() => props.acceptComment({commentId: props.comment.id})}
|
||||||
|
rejectComment={() => props.rejectComment({commentId: props.comment.id})}
|
||||||
|
showBanUserDialog={() => props.showBanUserDialog(props.comment.user, props.comment.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{props.comment.user.banned === 'banned' ?
|
||||||
|
<span className={styles.banned}>
|
||||||
|
<Icon name='error_outline'/>
|
||||||
|
{lang.t('comment.banned_user')}
|
||||||
|
</span>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!props.currentAsset && (
|
||||||
|
<div className={styles.moderateArticle}>
|
||||||
|
Article: {props.comment.asset.title} <Link to={`/admin/moderate/${props.comment.asset.id}`}>Moderate Article</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.itemBody}>
|
||||||
|
<p className={styles.body}>
|
||||||
|
<Linkify component='span' properties={{style: linkStyles}}>
|
||||||
|
<Highlighter searchWords={props.suspectWords} textToHighlight={props.comment.body}/>
|
||||||
|
</Linkify>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{actionSumaries && <FlagBox actionSumaries={actionSumaries} />}
|
||||||
|
|
||||||
|
{/* <span className={styles.context}>*/}
|
||||||
|
{/* <a>View context</a>*/}
|
||||||
|
{/* </span>*/}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkStyles = {
|
||||||
|
backgroundColor: 'rgb(255, 219, 135)',
|
||||||
|
padding: '1px 2px'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Comment;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import React, {PropTypes} from 'react';
|
||||||
|
import styles from './styles.css';
|
||||||
|
|
||||||
|
const FlagBox = props => (
|
||||||
|
<div className={styles.flagBox}>
|
||||||
|
<h3>Flags:</h3>
|
||||||
|
<ul>
|
||||||
|
{props.actionSumaries.map((action, i) =>
|
||||||
|
<li key={i}>{!action.reason ? <i>No reason provided</i> : action.reason} (<strong>{action.count}</strong>)</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
FlagBox.propTypes = {
|
||||||
|
actionSumaries: PropTypes.array.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlagBox;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {Link} from 'react-router';
|
||||||
|
import styles from './styles.css';
|
||||||
|
|
||||||
|
const ModerationHeader = props => (
|
||||||
|
<div className=''>
|
||||||
|
<div className={`mdl-tabs ${styles.header}`}>
|
||||||
|
{
|
||||||
|
props.asset ?
|
||||||
|
<div className={`mdl-tabs__tab-bar ${styles.moderateAsset}`}>
|
||||||
|
<Link className="mdl-tabs__tab" to="/admin/moderate">All Streams</Link>
|
||||||
|
<a className="mdl-tabs__tab">{props.asset.title}</a>
|
||||||
|
<Link className="mdl-tabs__tab" to="/admin/streams">Select Stream</Link>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<div className={`mdl-tabs__tab-bar ${styles.moderateAsset}`}>
|
||||||
|
<a className="mdl-tabs__tab" />
|
||||||
|
<a className="mdl-tabs__tab">All Streams</a>
|
||||||
|
<Link className="mdl-tabs__tab" to="/admin/streams">Select Stream</Link>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export default ModerationHeader;
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import React, {PropTypes} from 'react';
|
||||||
|
import styles from './styles.css';
|
||||||
|
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||||
|
import translations from 'coral-admin/src/translations.json';
|
||||||
|
|
||||||
|
const lang = new I18n(translations);
|
||||||
|
|
||||||
|
const ModerationMenu = (props) => (
|
||||||
|
<div className='mdl-tabs'>
|
||||||
|
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
|
||||||
|
<a href='#all'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
props.onTabClick('all');
|
||||||
|
}}
|
||||||
|
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'all' ? styles.active : ''}`}
|
||||||
|
>
|
||||||
|
{lang.t('modqueue.all')}
|
||||||
|
</a>
|
||||||
|
{
|
||||||
|
props.enablePremodTab
|
||||||
|
? <a href='#premod'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
props.onTabClick('premod');
|
||||||
|
}}
|
||||||
|
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'premod' ? styles.active : ''}`}>
|
||||||
|
{lang.t('modqueue.premod')}
|
||||||
|
</a>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
<a href='#rejected'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
props.onTabClick('rejected');
|
||||||
|
}}
|
||||||
|
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'rejected' ? styles.active : ''}`}
|
||||||
|
>
|
||||||
|
{lang.t('modqueue.rejected')}
|
||||||
|
</a>
|
||||||
|
<a href='#flagged'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
props.onTabClick('flagged');
|
||||||
|
}}
|
||||||
|
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'flagged' ? styles.active : ''}`}
|
||||||
|
>
|
||||||
|
{lang.t('modqueue.flagged')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
ModerationMenu.propTypes = {
|
||||||
|
activeTab: PropTypes.string.isRequired,
|
||||||
|
enablePremodTab: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModerationMenu;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {Link} from 'react-router';
|
||||||
|
import styles from './styles.css';
|
||||||
|
|
||||||
|
const NotFound = props => (
|
||||||
|
<div className={`mdl-card mdl-shadow--2dp ${styles.notFound}`}>
|
||||||
|
<p>
|
||||||
|
The provided asset id <Link to={`/admin/moderate/${props.assetId}`}>{props.assetId}</Link> does not exist.
|
||||||
|
<Link className={styles.goToStreams} to="/admin/streams">Go to Streams</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
@custom-media --big-viewport (min-width: 780px);
|
||||||
|
|
||||||
|
.listContainer {
|
||||||
|
max-width: 860px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabBar {
|
||||||
|
background-color: rgba(44, 44, 44, 0.89);
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
flex: 1;
|
||||||
|
color: white;
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
transition: border-bottom 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
color: white;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-bottom: solid 5px #F36451;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active > span {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active:after {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showShortcuts {
|
||||||
|
position: absolute;
|
||||||
|
right: 130px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-left: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (--big-viewport) {
|
||||||
|
.tab {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notFound {
|
||||||
|
position: relative;
|
||||||
|
margin: 20px auto;
|
||||||
|
text-align: center;
|
||||||
|
padding: 68px 45px;
|
||||||
|
vertical-align: middle;
|
||||||
|
min-width: 500px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgb(244, 126, 107);
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.goToStreams {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
|
||||||
|
.moderateAsset {
|
||||||
|
a {
|
||||||
|
-webkit-box-flex: 1;
|
||||||
|
-ms-flex: 1;
|
||||||
|
flex: 1;
|
||||||
|
color: white;
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
transition: opacity 200ms;
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: .8;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@custom-media --big-viewport (min-width: 780px);
|
||||||
|
|
||||||
|
.list {
|
||||||
|
padding: 8px 0;
|
||||||
|
list-style: none;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
&.singleView .listItem {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.singleView .listItem.activeItem {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 1.5em;
|
||||||
|
line-height: 1.5em;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 60px;
|
||||||
|
left: 25%;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
width: 50%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButton {
|
||||||
|
transform: scale(1.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem {
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 660px;
|
||||||
|
min-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px 14px;
|
||||||
|
position: relative;
|
||||||
|
transition: box-shadow 200ms;
|
||||||
|
margin-top: 0;
|
||||||
|
|
||||||
|
|
||||||
|
&: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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context {
|
||||||
|
a {
|
||||||
|
color: #f36451;
|
||||||
|
text-decoration: underline;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sideActions {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
padding: 40px 18px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.author {
|
||||||
|
min-width: 230px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemBody {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
margin-right: 16px;
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #757575;
|
||||||
|
font-size: 40px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.created {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButton {
|
||||||
|
transform: scale(.8);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
margin-top: 0px;
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.88em;
|
||||||
|
color: black;
|
||||||
|
max-width: 500px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flagged {
|
||||||
|
color: rgba(255, 0, 0, .5);
|
||||||
|
padding-top: 15px;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flagCount{
|
||||||
|
font-size: 12px;
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: #444;
|
||||||
|
margin-top: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (--big-viewport) {
|
||||||
|
.listItem {
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.activeItem {
|
||||||
|
border: 2px solid #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.hasLinks {
|
||||||
|
color: #f00;
|
||||||
|
text-align: right;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banned {
|
||||||
|
color: #f00;
|
||||||
|
text-align: left;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ban {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Comment {
|
||||||
|
.moderateArticle {
|
||||||
|
font-size: 12px;
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
color: #679af3;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: .5px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
opacity: .9;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flagBox {
|
||||||
|
border-top: 1px solid rgba(66, 66, 66, 0.12);
|
||||||
|
h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
export const actionsMap = {
|
||||||
|
PREMOD: ['REJECT', 'APPROVE', 'BAN'],
|
||||||
|
FLAGGED: ['REJECT', 'APPROVE'],
|
||||||
|
REJECTED: ['APPROVE']
|
||||||
|
};
|
||||||
|
|
||||||
|
export const menuActionsMap = {
|
||||||
|
'REJECT': {status: 'REJECTED', icon: 'close', key: 'r'},
|
||||||
|
'APPROVE': {status: 'ACCEPTED', icon: 'done', key: 't'},
|
||||||
|
'FLAGGED': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'},
|
||||||
|
'BAN': {status: 'BANNED', icon: 'not interested'},
|
||||||
|
'': {icon: 'done'}
|
||||||
|
};
|
||||||
@@ -55,6 +55,12 @@
|
|||||||
border-left: none;
|
border-left: none;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgb(44, 44, 44);
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,10 @@ import {connect} from 'react-redux';
|
|||||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||||
import {fetchAssets, updateAssetState} from '../../actions/assets';
|
import {fetchAssets, updateAssetState} from '../../actions/assets';
|
||||||
import translations from '../../translations.json';
|
import translations from '../../translations.json';
|
||||||
import {
|
import {Link} from 'react-router';
|
||||||
RadioGroup,
|
|
||||||
Radio,
|
import {Pager, Icon} from 'coral-ui';
|
||||||
Icon,
|
import {DataTable, TableHeader, RadioGroup, Radio} from 'react-mdl';
|
||||||
DataTable,
|
|
||||||
TableHeader
|
|
||||||
} from 'react-mdl';
|
|
||||||
import Pager from 'coral-ui/components/Pager';
|
|
||||||
|
|
||||||
const limit = 25;
|
const limit = 25;
|
||||||
|
|
||||||
@@ -74,6 +70,8 @@ class Streams extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderTitle = (title, {id}) => <Link to={`/admin/moderate/${id}`}>{title}</Link>
|
||||||
|
|
||||||
renderStatus = (closedAt, {id}) => {
|
renderStatus = (closedAt, {id}) => {
|
||||||
const closed = closedAt && new Date(closedAt).getTime() < Date.now();
|
const closed = closedAt && new Date(closedAt).getTime() < Date.now();
|
||||||
const statusMenuOpen = this.state.statusMenus[id];
|
const statusMenuOpen = this.state.statusMenus[id];
|
||||||
@@ -104,6 +102,9 @@ class Streams extends Component {
|
|||||||
render () {
|
render () {
|
||||||
const {search, sort, filter} = this.state;
|
const {search, sort, filter} = this.state;
|
||||||
const {assets} = this.props;
|
const {assets} = this.props;
|
||||||
|
|
||||||
|
const assetsIds = assets.ids.map((id) => assets.byId[id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.leftColumn}>
|
<div className={styles.leftColumn}>
|
||||||
@@ -142,16 +143,14 @@ class Streams extends Component {
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.mainContent}>
|
<div className={styles.mainContent}>
|
||||||
<DataTable
|
<DataTable className={styles.streamsTable} rows={assetsIds} onClick={this.goToModeration}>
|
||||||
className={styles.streamsTable}
|
<TableHeader name="title" cellFormatter={this.renderTitle}>{lang.t('streams.article')}</TableHeader>
|
||||||
rows={assets.ids.map((id) => assets.byId[id])}>
|
<TableHeader name="publication_date" cellFormatter={this.renderDate}>
|
||||||
<TableHeader name="title">{lang.t('streams.article')}</TableHeader>
|
{lang.t('streams.pubdate')}
|
||||||
<TableHeader name="publication_date" cellFormatter={this.renderDate}>
|
</TableHeader>
|
||||||
{lang.t('streams.pubdate')}
|
<TableHeader name="closedAt" cellFormatter={this.renderStatus} className={styles.status}>
|
||||||
</TableHeader>
|
{lang.t('streams.status')}
|
||||||
<TableHeader name="closedAt" cellFormatter={this.renderStatus} className={styles.status}>
|
</TableHeader>
|
||||||
{lang.t('streams.status')}
|
|
||||||
</TableHeader>
|
|
||||||
</DataTable>
|
</DataTable>
|
||||||
<Pager
|
<Pager
|
||||||
totalPages={Math.ceil((assets.count || 0) / limit)}
|
totalPages={Math.ceil((assets.count || 0) / limit)}
|
||||||
@@ -169,6 +168,7 @@ const mapStateToProps = ({assets}) => {
|
|||||||
assets: assets.toJS()
|
assets: assets.toJS()
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => {
|
const mapDispatchToProps = (dispatch) => {
|
||||||
return {
|
return {
|
||||||
fetchAssets: (...args) => {
|
fetchAssets: (...args) => {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
fragment commentView on Comment {
|
||||||
|
id
|
||||||
|
body
|
||||||
|
created_at
|
||||||
|
status
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
name: username
|
||||||
|
status
|
||||||
|
}
|
||||||
|
asset {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import {graphql} from 'react-apollo';
|
||||||
|
import SET_USER_STATUS from './setUserStatus.graphql';
|
||||||
|
import SET_COMMENT_STATUS from './setCommentStatus.graphql';
|
||||||
|
|
||||||
|
export const banUser = graphql(SET_USER_STATUS, {
|
||||||
|
props: ({mutate}) => ({
|
||||||
|
banUser: ({userId}) => {
|
||||||
|
return mutate({
|
||||||
|
variables: {
|
||||||
|
userId,
|
||||||
|
status: 'BANNED'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
|
||||||
|
props: ({mutate}) => ({
|
||||||
|
acceptComment: ({commentId}) => {
|
||||||
|
return mutate({
|
||||||
|
variables: {
|
||||||
|
commentId,
|
||||||
|
status: 'ACCEPTED'
|
||||||
|
},
|
||||||
|
refetchQueries: ['ModQueue']
|
||||||
|
});
|
||||||
|
},
|
||||||
|
rejectComment: ({commentId}) => {
|
||||||
|
return mutate({
|
||||||
|
variables: {
|
||||||
|
commentId,
|
||||||
|
status: 'REJECTED'
|
||||||
|
},
|
||||||
|
refetchQueries: ['ModQueue']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
mutation setCommentStatus($commentId: ID!, $status: COMMENT_STATUS!){
|
||||||
|
setCommentStatus(id: $commentId, status: $status) {
|
||||||
|
errors {
|
||||||
|
translation_key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
mutation setUserStatus($userId: ID!, $status: USER_STATUS!) {
|
||||||
|
setUserStatus(id: $userId, status: $status) {
|
||||||
|
errors {
|
||||||
|
translation_key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
query Assets {
|
||||||
|
assets {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import {graphql} from 'react-apollo';
|
||||||
|
import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
|
||||||
|
|
||||||
|
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
|
||||||
|
options: ({params: {id = ''}}) => {
|
||||||
|
return {
|
||||||
|
variables: {
|
||||||
|
asset_id: id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#import "../fragments/commentView.graphql"
|
||||||
|
|
||||||
|
query ModQueue ($asset_id: ID!) {
|
||||||
|
all: comments(query: {
|
||||||
|
statuses: [REJECTED, PREMOD],
|
||||||
|
asset_id: $asset_id
|
||||||
|
}) {
|
||||||
|
...commentView
|
||||||
|
}
|
||||||
|
premod: comments(query: {
|
||||||
|
statuses: [PREMOD],
|
||||||
|
asset_id: $asset_id
|
||||||
|
}) {
|
||||||
|
...commentView
|
||||||
|
}
|
||||||
|
flagged: comments(query: {
|
||||||
|
action_type: FLAG,
|
||||||
|
asset_id: $asset_id
|
||||||
|
}) {
|
||||||
|
...commentView
|
||||||
|
action_summaries {
|
||||||
|
count
|
||||||
|
... on FlagActionSummary {
|
||||||
|
reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rejected: comments(query: {
|
||||||
|
statuses: [REJECTED],
|
||||||
|
asset_id: $asset_id
|
||||||
|
}) {
|
||||||
|
...commentView
|
||||||
|
}
|
||||||
|
assets: assets {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import {Map, Set, fromJS} from 'immutable';
|
|
||||||
import * as types from '../constants/actions';
|
|
||||||
|
|
||||||
const initialState = Map({
|
|
||||||
ids: Set(),
|
|
||||||
byId: Map()
|
|
||||||
});
|
|
||||||
|
|
||||||
export default (state = initialState, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case types.ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS: return addActions(state, action);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addActions = (state, action) => {
|
|
||||||
|
|
||||||
// Make ids that are unique by item_id and by action type
|
|
||||||
const typeId = (action) => `${action.action_type}_${action.item_id}`;
|
|
||||||
const ids = action.actions.map(action => typeId(action));
|
|
||||||
const map = action.actions.reduce((memo, action) => {
|
|
||||||
memo[typeId(action)] = action;
|
|
||||||
return memo;
|
|
||||||
}, {});
|
|
||||||
return state.set('byId', fromJS(map)).set('ids', new Set(ids));
|
|
||||||
};
|
|
||||||
@@ -1,20 +1,26 @@
|
|||||||
import {Map, List, fromJS} from 'immutable';
|
import {Map, List, fromJS} from 'immutable';
|
||||||
import {FETCH_ASSETS_SUCCESS, UPDATE_ASSET_STATE_REQUEST} from '../constants/assets';
|
import * as actions from '../constants/assets';
|
||||||
|
|
||||||
const initialState = Map({
|
const initialState = Map({
|
||||||
byId: Map(),
|
byId: Map(),
|
||||||
ids: List()
|
ids: List(),
|
||||||
|
assets: List()
|
||||||
});
|
});
|
||||||
|
|
||||||
export default (state = initialState, action) => {
|
export default function assets (state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case FETCH_ASSETS_SUCCESS:
|
case actions.FETCH_ASSETS_SUCCESS:
|
||||||
return replaceAssets(action, state);
|
return replaceAssets(action, state);
|
||||||
case UPDATE_ASSET_STATE_REQUEST:
|
case actions.UPDATE_ASSET_STATE_REQUEST:
|
||||||
return state.setIn(['byId', action.id, 'closedAt'], action.closedAt);
|
return state
|
||||||
default: return state;
|
.setIn(['byId', action.id, 'closedAt'], action.closedAt);
|
||||||
|
case actions.UPDATE_ASSETS:
|
||||||
|
return state
|
||||||
|
.set('assets', List(action.assets));
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const replaceAssets = (action, state) => {
|
const replaceAssets = (action, state) => {
|
||||||
const assets = fromJS(action.assets.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {}));
|
const assets = fromJS(action.assets.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {}));
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
import * as actions from '../constants/comments';
|
|
||||||
import * as userActions from '../constants/users';
|
|
||||||
import {Map, List, fromJS} from 'immutable';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Comments state is stored using 2 structures:
|
|
||||||
* - byId is a Map holding the comments using the item_id property as keys
|
|
||||||
* - ids is a List of item_id, this allows us to order and iterate easily
|
|
||||||
* since maps are unordered and some times we just need a list of things
|
|
||||||
*/
|
|
||||||
|
|
||||||
const initialState = Map({
|
|
||||||
byId: Map(),
|
|
||||||
ids: List(),
|
|
||||||
loading: false,
|
|
||||||
showBanUserDialog: false,
|
|
||||||
banUser: {
|
|
||||||
'userName': '',
|
|
||||||
'userId': '',
|
|
||||||
'commentId': ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle the comment actions
|
|
||||||
export default (state = initialState, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case actions.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST: return state.set('loading', true);
|
|
||||||
case actions.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS: return replaceComments(action, state);
|
|
||||||
case actions.COMMENTS_MODERATION_QUEUE_FAILED: return state.set('loading', false);
|
|
||||||
case actions.COMMENT_STATUS_UPDATE_REQUEST: return updateStatus(state, action);
|
|
||||||
case actions.COMMENT_FLAG: return flag(state, action);
|
|
||||||
case actions.COMMENT_CREATE_SUCCESS: return addComment(state, action);
|
|
||||||
case actions.COMMENT_STREAM_FETCH_SUCCESS: return replaceComments(action, state);
|
|
||||||
case actions.SHOW_BANUSER_DIALOG: return setBanUser(state, true, action);
|
|
||||||
case actions.HIDE_BANUSER_DIALOG: return setBanUser(state, false, action);
|
|
||||||
case actions.USER_BAN_SUCCESS: return setBanUser(state, false, action);
|
|
||||||
case userActions.UPDATE_STATUS_SUCCESS: return setBanUser(state, false, action);
|
|
||||||
default: return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// hide or show the UI for the dialog confirming the ban
|
|
||||||
// set the user that is going to set and the comment that is the reason
|
|
||||||
const setBanUser = (state, showBanUser, action) => {
|
|
||||||
const banUser = {'userName': action.userName, 'userId': action.userId, 'commentId': action.commentId};
|
|
||||||
return state.set('showBanUserDialog', showBanUser)
|
|
||||||
.set('banUser', banUser);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update a comment status
|
|
||||||
const updateStatus = (state, action) => {
|
|
||||||
const byId = state.get('byId');
|
|
||||||
const data = byId.get(action.id).set('status', action.status.toLowerCase());
|
|
||||||
return state.set('byId', byId.set(action.id, data));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Flag a comment
|
|
||||||
const flag = (state, action) => {
|
|
||||||
const byId = state.get('byId');
|
|
||||||
const data = byId.get(action.id).set('flagged', true);
|
|
||||||
const comment = byId.get(action.id).set('data', data);
|
|
||||||
return state.set('byId', byId.set(action.id, comment));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Replace the comment list with a new one
|
|
||||||
const replaceComments = (action, state) => {
|
|
||||||
const comments = fromJS(action.comments.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {}));
|
|
||||||
return state.set('byId', comments).set('loading', false)
|
|
||||||
.set('ids', List(comments.keys()));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add a new comment
|
|
||||||
const addComment = (state, action) => {
|
|
||||||
const comment = fromJS(action.comment);
|
|
||||||
return state.set('byId', state.get('byId').set(comment.get('item_id'), comment))
|
|
||||||
.set('ids', state.get('ids').unshift(comment.get('item_id')));
|
|
||||||
};
|
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
import auth from 'reducers/auth';
|
import auth from './auth';
|
||||||
import users from 'reducers/users';
|
import assets from './assets';
|
||||||
import assets from 'reducers/assets';
|
import settings from './settings';
|
||||||
import actions from 'reducers/actions';
|
import community from './community';
|
||||||
import install from 'reducers/install';
|
import moderation from './moderation';
|
||||||
import comments from 'reducers/comments';
|
import install from './install';
|
||||||
import settings from 'reducers/settings';
|
|
||||||
import community from 'reducers/community';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
settings,
|
|
||||||
comments,
|
|
||||||
community,
|
|
||||||
auth,
|
auth,
|
||||||
actions,
|
|
||||||
assets,
|
assets,
|
||||||
users,
|
settings,
|
||||||
|
community,
|
||||||
|
moderation,
|
||||||
install
|
install
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import {Map} from 'immutable';
|
||||||
|
import * as actions from '../constants/moderation';
|
||||||
|
|
||||||
|
const initialState = Map({
|
||||||
|
activeTab: 'all',
|
||||||
|
singleView: false,
|
||||||
|
modalOpen: false,
|
||||||
|
user: Map({}),
|
||||||
|
commentId: null,
|
||||||
|
banDialog: false
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function moderation (state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.HIDE_BANUSER_DIALOG:
|
||||||
|
return state
|
||||||
|
.set('banDialog', false);
|
||||||
|
case actions.SHOW_BANUSER_DIALOG:
|
||||||
|
return state
|
||||||
|
.merge({
|
||||||
|
user: Map(action.user),
|
||||||
|
commentId: action.commentId,
|
||||||
|
banDialog: true
|
||||||
|
});
|
||||||
|
case actions.SET_ACTIVE_TAB:
|
||||||
|
return state
|
||||||
|
.set('activeTab', action.activeTab);
|
||||||
|
case actions.TOGGLE_MODAL:
|
||||||
|
return state
|
||||||
|
.set('modalOpen', action.open);
|
||||||
|
case actions.SINGLE_VIEW:
|
||||||
|
return state
|
||||||
|
.set('singleView', !state.get('singleView'));
|
||||||
|
default :
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Map, List} from 'immutable';
|
import {Map, List} from 'immutable';
|
||||||
import * as types from '../actions/settings';
|
import * as actions from '../actions/settings';
|
||||||
|
|
||||||
const initialState = Map({
|
const initialState = Map({
|
||||||
settings: Map({
|
settings: Map({
|
||||||
@@ -16,48 +16,49 @@ const initialState = Map({
|
|||||||
fetchingSettings: false
|
fetchingSettings: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle the comment actions
|
export default function settings (state = initialState, action) {
|
||||||
export default (state = initialState, action) => {
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case types.SETTINGS_LOADING: return state.set('fetchingSettings', true).set('fetchSettingsError', null);
|
case actions.SETTINGS_LOADING:
|
||||||
case types.SETTINGS_RECEIVED: return updateSettings(state, action);
|
return state
|
||||||
case types.SETTINGS_FETCH_ERROR: return settingsFetchFailed(state, action);
|
.set('fetchingSettings', true)
|
||||||
case types.SETTINGS_UPDATED: return updateSettings(state, action);
|
.set('fetchSettingsError', null);
|
||||||
case types.SAVE_SETTINGS_LOADING: return state.set('fetchingSettings', true).set('saveSettingsError', null);
|
case actions.SETTINGS_RECEIVED:
|
||||||
case types.SAVE_SETTINGS_SUCCESS: return saveComplete(state, action);
|
return state.merge({
|
||||||
case types.SAVE_SETTINGS_FAILED: return settingsSaveFailed(state, action);
|
fetchingSettings: null,
|
||||||
case types.WORDLIST_UPDATED: return updateWordlist(state, action);
|
fetchSettingsError: null,
|
||||||
case types.DOMAINLIST_UPDATED: return updateDomainlist(state, action);
|
...action.settings
|
||||||
default: return state;
|
});
|
||||||
|
case actions.SETTINGS_FETCH_ERROR:
|
||||||
|
return state
|
||||||
|
.set('fetchingSettings', false)
|
||||||
|
.set('fetchSettingsError', action.error);
|
||||||
|
case actions.SETTINGS_UPDATED:
|
||||||
|
return state.merge({
|
||||||
|
fetchingSettings: null,
|
||||||
|
fetchSettingsError: null,
|
||||||
|
...action.settings
|
||||||
|
});
|
||||||
|
case actions.SAVE_SETTINGS_LOADING:
|
||||||
|
return state
|
||||||
|
.set('fetchingSettings', true)
|
||||||
|
.set('saveSettingsError', null);
|
||||||
|
case actions.SAVE_SETTINGS_SUCCESS:
|
||||||
|
return state.merge({
|
||||||
|
fetchingSettings: false,
|
||||||
|
fetchSettingsError: null,
|
||||||
|
...action.settings
|
||||||
|
});
|
||||||
|
case actions.SAVE_SETTINGS_FAILED:
|
||||||
|
return state
|
||||||
|
.set('fetchingSettings', false)
|
||||||
|
.set('fetchSettingsError', action.error);
|
||||||
|
case actions.WORDLIST_UPDATED:
|
||||||
|
return state
|
||||||
|
.setIn(['settings', 'wordlist', action.listName], action.list);
|
||||||
|
case actions.DOMAINLIST_UPDATED:
|
||||||
|
return state
|
||||||
|
.setIn(['settings', 'domains', action.listName], action.list);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// only for updating top-level settings
|
|
||||||
const updateSettings = (state, action) => {
|
|
||||||
const s = state.set('fetchingSettings', false).set('fetchSettingsError', null);
|
|
||||||
const settings = s.get('settings').merge(action.settings);
|
|
||||||
return s.set('settings', settings);
|
|
||||||
};
|
|
||||||
|
|
||||||
// any nested settings must have a specialized setter
|
|
||||||
const updateWordlist = (state, action) => {
|
|
||||||
return state.setIn(['settings', 'wordlist', action.listName], action.list);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateDomainlist = (state, action) => {
|
|
||||||
return state.setIn(['settings', 'domains', action.listName], action.list);
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveComplete = (state, action) => {
|
|
||||||
const s = state.set('fetchingSettings', false).set('saveSettingsError', null);
|
|
||||||
const settings = s.get('settings').merge(action.settings);
|
|
||||||
return s.set('settings', settings);
|
|
||||||
};
|
|
||||||
|
|
||||||
const settingsFetchFailed = (state, action) => {
|
|
||||||
return state.set('fetchingSettings', false).set('fetchSettingsError', action.error);
|
|
||||||
};
|
|
||||||
|
|
||||||
const settingsSaveFailed = (state, action) => {
|
|
||||||
return state.set('fetchingSettings', false).set('fetchSettingsError', action.error);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import {Map, List, fromJS} from 'immutable';
|
|
||||||
|
|
||||||
const initialState = Map({
|
|
||||||
byId: Map(),
|
|
||||||
ids: List()
|
|
||||||
});
|
|
||||||
|
|
||||||
export default (state = initialState, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'USERS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceUsers(action, state);
|
|
||||||
case 'USER_STATUS_UPDATE': return updateUserStatus(state, action);
|
|
||||||
default: return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Replace the comment list with a new one
|
|
||||||
const replaceUsers = (action, state) => {
|
|
||||||
const users = fromJS(action.users.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {}));
|
|
||||||
return state.set('byId', users)
|
|
||||||
.set('ids', List(users.keys()));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update a user status
|
|
||||||
const updateUserStatus = (state, action) => {
|
|
||||||
const byId = state.get('byId');
|
|
||||||
const data = byId.get(action.author_id).set('status', action.status.toLowerCase());
|
|
||||||
return state.set('byId', byId.set(action.author_id, data));
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import Pym from '../../node_modules/pym.js';
|
||||||
|
|
||||||
|
const pym = new Pym.Child({polling: 100});
|
||||||
|
export default pym;
|
||||||
|
|
||||||
|
export const link = (url) => (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
pym.sendMessage('navigate', url);
|
||||||
|
};
|
||||||
@@ -2,7 +2,6 @@ import ApolloClient, {addTypename} from 'apollo-client';
|
|||||||
import getNetworkInterface from './transport';
|
import getNetworkInterface from './transport';
|
||||||
|
|
||||||
export const client = new ApolloClient({
|
export const client = new ApolloClient({
|
||||||
connectToDevTools: true,
|
|
||||||
queryTransformer: addTypename,
|
queryTransformer: addTypename,
|
||||||
dataIdFromObject: (result) => {
|
dataIdFromObject: (result) => {
|
||||||
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
|
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {Notification, notificationActions, authActions, assetActions, pym} from
|
|||||||
import Stream from './Stream';
|
import Stream from './Stream';
|
||||||
import InfoBox from 'coral-plugin-infobox/InfoBox';
|
import InfoBox from 'coral-plugin-infobox/InfoBox';
|
||||||
import QuestionBox from 'coral-plugin-questionbox/QuestionBox';
|
import QuestionBox from 'coral-plugin-questionbox/QuestionBox';
|
||||||
|
import {ModerationLink} from 'coral-plugin-moderation';
|
||||||
import Count from 'coral-plugin-comment-count/CommentCount';
|
import Count from 'coral-plugin-comment-count/CommentCount';
|
||||||
import CommentBox from 'coral-plugin-commentbox/CommentBox';
|
import CommentBox from 'coral-plugin-commentbox/CommentBox';
|
||||||
import UserBox from 'coral-sign-in/components/UserBox';
|
import UserBox from 'coral-sign-in/components/UserBox';
|
||||||
@@ -139,6 +140,7 @@ class Embed extends Component {
|
|||||||
charCount={asset.settings.charCountEnable && asset.settings.charCount} />
|
charCount={asset.settings.charCountEnable && asset.settings.charCount} />
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
<ModerationLink assetId={asset.id} isAdmin={isAdmin} />
|
||||||
</RestrictedContent>
|
</RestrictedContent>
|
||||||
</div>
|
</div>
|
||||||
: <p>{asset.settings.closedMessage}</p>
|
: <p>{asset.settings.closedMessage}</p>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.authorName {
|
.authorName {
|
||||||
color: black;
|
color: black;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 10px 5px 10px 0;
|
margin: 10px 8px 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hasBio {
|
.hasBio {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class LikeButton extends Component {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
like: PropTypes.shape({
|
like: PropTypes.shape({
|
||||||
current: PropTypes.obect,
|
current: PropTypes.object,
|
||||||
count: PropTypes.number
|
count: PropTypes.number
|
||||||
}),
|
}),
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React, {PropTypes} from 'react';
|
||||||
|
import styles from './styles.css';
|
||||||
|
|
||||||
|
import {I18n} from '../coral-framework';
|
||||||
|
import translations from './translations.json';
|
||||||
|
|
||||||
|
const ModerationLink = props => props.isAdmin ? (
|
||||||
|
<div className={styles.moderationLink}>
|
||||||
|
<a href={`/admin/moderate/${props.assetId}`} target="_blank">
|
||||||
|
{lang.t('MODERATE_THIS_STREAM')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
ModerationLink.propTypes = {
|
||||||
|
assetId: PropTypes.string.isRequired,
|
||||||
|
isAdmin: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const lang = new I18n(translations);
|
||||||
|
|
||||||
|
export default ModerationLink;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {default as ModerationLink} from './ModerationLink';
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.moderationLink {
|
||||||
|
a {
|
||||||
|
color: #679af3;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: .3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"en": {
|
||||||
|
"MODERATE_THIS_STREAM": "Moderate this stream"
|
||||||
|
},
|
||||||
|
"es": {
|
||||||
|
"MODERATE_THIS_STREAM": "Modera este stream"
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
-10
@@ -162,22 +162,37 @@ const createPublicComment = (context, commentInput) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the status of a comment
|
||||||
|
* @param {String} comment comment in graphql context
|
||||||
|
* @param {String} id identifier of the comment (uuid)
|
||||||
|
* @param {String} status the new status of the comment
|
||||||
|
*/
|
||||||
|
|
||||||
|
const setCommentStatus = ({comment}, {id, status}) => {
|
||||||
|
return CommentsService.setStatus(id, status)
|
||||||
|
.then(res => res);
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = (context) => {
|
module.exports = (context) => {
|
||||||
|
|
||||||
// TODO: refactor to something that'll return an error in the event an attempt
|
// TODO: refactor to something that'll return an error in the event an attempt
|
||||||
// is made to mutate state while not logged in. There's got to be a better way
|
// is made to mutate state while not logged in. There's got to be a better way
|
||||||
// to do this.
|
// to do this.
|
||||||
if (context.user && context.user.can('mutation:createComment')) {
|
let mutators = {
|
||||||
return {
|
|
||||||
Comment: {
|
|
||||||
create: (comment) => createPublicComment(context, comment)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
Comment: {
|
Comment: {
|
||||||
create: () => Promise.reject(errors.ErrNotAuthorized)
|
create: () => Promise.reject(errors.ErrNotAuthorized),
|
||||||
|
setCommentStatus: () => Promise.reject(errors.ErrNotAuthorized)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (context.user && context.user.can('mutation:createComment')) {
|
||||||
|
mutators.Comment.create = (comment) => createPublicComment(context, comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.user && context.user.can('mutation:setCommentStatus')) {
|
||||||
|
mutators.Comment.setCommentStatus = (action) => setCommentStatus(context, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mutators;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const _ = require('lodash');
|
|||||||
|
|
||||||
const Comment = require('./comment');
|
const Comment = require('./comment');
|
||||||
const Action = require('./action');
|
const Action = require('./action');
|
||||||
|
const User = require('./user');
|
||||||
|
|
||||||
module.exports = (context) => {
|
module.exports = (context) => {
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ module.exports = (context) => {
|
|||||||
return _.merge(...[
|
return _.merge(...[
|
||||||
Comment,
|
Comment,
|
||||||
Action,
|
Action,
|
||||||
|
User,
|
||||||
].map((mutators) => {
|
].map((mutators) => {
|
||||||
|
|
||||||
// Each set of mutators is a function which takes the context.
|
// Each set of mutators is a function which takes the context.
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
const errors = require('../../errors');
|
||||||
|
const UsersService = require('../../services/users');
|
||||||
|
|
||||||
|
const setUserStatus = ({user}, {id, status}) => {
|
||||||
|
return UsersService.setStatus(id, status)
|
||||||
|
.then(res => res);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = (context) => {
|
||||||
|
|
||||||
|
// TODO: refactor to something that'll return an error in the event an attempt
|
||||||
|
// is made to mutate state while not logged in. There's got to be a better way
|
||||||
|
// to do this.
|
||||||
|
if (context.user && context.user.can('mutation:setUserStatus')) {
|
||||||
|
return {
|
||||||
|
User: {
|
||||||
|
setUserStatus: (action) => setUserStatus(context, action)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
User: {
|
||||||
|
setUserStatus: () => Promise.reject(errors.ErrNotAuthorized)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -27,6 +27,12 @@ const RootMutation = {
|
|||||||
deleteAction(_, {id}, {mutators: {Action}}) {
|
deleteAction(_, {id}, {mutators: {Action}}) {
|
||||||
return wrapResponse(null)(Action.delete({id}));
|
return wrapResponse(null)(Action.delete({id}));
|
||||||
},
|
},
|
||||||
|
setUserStatus(_, {id, status}, {mutators: {User}}) {
|
||||||
|
return wrapResponse(null)(User.setUserStatus({id, status}));
|
||||||
|
},
|
||||||
|
setCommentStatus(_, {id, status}, {mutators: {Comment}}) {
|
||||||
|
return wrapResponse(null)(Comment.setCommentStatus({id, status}));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = RootMutation;
|
module.exports = RootMutation;
|
||||||
|
|||||||
@@ -29,12 +29,7 @@ const RootQuery = {
|
|||||||
|
|
||||||
if (user != null && user.hasRoles('ADMIN') && action_type) {
|
if (user != null && user.hasRoles('ADMIN') && action_type) {
|
||||||
return Actions.getByTypes({action_type, item_type: 'COMMENTS'})
|
return Actions.getByTypes({action_type, item_type: 'COMMENTS'})
|
||||||
.then((actions) => {
|
.then((ids) => {
|
||||||
|
|
||||||
// Map the actions from the items referenced byt this query. The actions
|
|
||||||
// returned by this query are explicitly going to be distinct by their
|
|
||||||
// `item_id`'s.
|
|
||||||
let ids = actions.map((action) => action.item_id);
|
|
||||||
|
|
||||||
// Perform the query using the available resolver.
|
// Perform the query using the available resolver.
|
||||||
return Comments.getByQuery({ids, statuses, asset_id, parent_id, limit, cursor, sort});
|
return Comments.getByQuery({ids, statuses, asset_id, parent_id, limit, cursor, sort});
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ type User {
|
|||||||
|
|
||||||
# returns all comments based on a query.
|
# returns all comments based on a query.
|
||||||
comments(query: CommentsQuery): [Comment]
|
comments(query: CommentsQuery): [Comment]
|
||||||
|
|
||||||
|
# returns user status
|
||||||
|
status: USER_STATUS
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tag {
|
type Tag {
|
||||||
@@ -362,6 +365,13 @@ enum SORT_ORDER {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# All queries that can be executed.
|
# All queries that can be executed.
|
||||||
|
enum USER_STATUS {
|
||||||
|
ACTIVE
|
||||||
|
BANNED
|
||||||
|
PENDING
|
||||||
|
APPROVED
|
||||||
|
}
|
||||||
|
|
||||||
type RootQuery {
|
type RootQuery {
|
||||||
|
|
||||||
# Site wide settings and defaults.
|
# Site wide settings and defaults.
|
||||||
@@ -468,6 +478,22 @@ type DeleteActionResponse implements Response {
|
|||||||
errors: [UserError]
|
errors: [UserError]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# SetUserStatusResponse is the response returned with possibly some errors
|
||||||
|
# relating to the delete action attempt.
|
||||||
|
type SetUserStatusResponse implements Response {
|
||||||
|
|
||||||
|
# An array of errors relating to the mutation that occured.
|
||||||
|
errors: [UserError]
|
||||||
|
}
|
||||||
|
|
||||||
|
# SetCommentStatusResponse is the response returned with possibly some errors
|
||||||
|
# relating to the delete action attempt.
|
||||||
|
type SetCommentStatusResponse implements Response {
|
||||||
|
|
||||||
|
# An array of errors relating to the mutation that occured.
|
||||||
|
errors: [UserError]
|
||||||
|
}
|
||||||
|
|
||||||
# All mutations for the application are defined on this object.
|
# All mutations for the application are defined on this object.
|
||||||
type RootMutation {
|
type RootMutation {
|
||||||
|
|
||||||
@@ -482,6 +508,12 @@ type RootMutation {
|
|||||||
|
|
||||||
# Delete an action based on the action id.
|
# Delete an action based on the action id.
|
||||||
deleteAction(id: ID!): DeleteActionResponse
|
deleteAction(id: ID!): DeleteActionResponse
|
||||||
|
|
||||||
|
# Sets User status
|
||||||
|
setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse
|
||||||
|
|
||||||
|
# Sets Comment status
|
||||||
|
setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|||||||
+7
-1
@@ -154,7 +154,9 @@ const USER_GRAPH_OPERATIONS = [
|
|||||||
'mutation:createComment',
|
'mutation:createComment',
|
||||||
'mutation:createAction',
|
'mutation:createAction',
|
||||||
'mutation:deleteAction',
|
'mutation:deleteAction',
|
||||||
'mutation:editName'
|
'mutation:editName',
|
||||||
|
'mutation:setUserStatus',
|
||||||
|
'mutation:setCommentStatus'
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,6 +172,10 @@ UserSchema.method('can', function(...actions) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:setCommentStatus') && !this.hasRoles('ADMIN')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ const ALLOWED_TAGS = [
|
|||||||
{name: 'STAFF'}
|
{name: 'STAFF'}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const STATUSES = [
|
||||||
|
'ACCEPTED',
|
||||||
|
'REJECTED',
|
||||||
|
'PREMOD',
|
||||||
|
];
|
||||||
|
|
||||||
module.exports = class CommentsService {
|
module.exports = class CommentsService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -249,4 +255,25 @@ module.exports = class CommentsService {
|
|||||||
|
|
||||||
return CommentModel.find(query);
|
return CommentModel.find(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets Comment Status
|
||||||
|
* @param {String} id identifier of the comment (uuid)
|
||||||
|
* @param {String} status the new status of the comment
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
|
||||||
|
static setStatus(id, status) {
|
||||||
|
|
||||||
|
// Check to see if the comment status is in the allowable set of statuses.
|
||||||
|
if (STATUSES.indexOf(status) === -1) {
|
||||||
|
|
||||||
|
// Comment status is not supported! Error out here.
|
||||||
|
return Promise.reject(new Error(`status ${status} is not supported`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CommentModel.update({id}, {
|
||||||
|
$set: {status}
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user