banUsers and moderate separate streams, not found assets etc

This commit is contained in:
Belen Curcio
2017-02-10 17:15:26 -03:00
parent 9320ece35b
commit bc14b908eb
23 changed files with 310 additions and 105 deletions
+7 -1
View File
@@ -4,8 +4,10 @@ import {
FETCH_ASSETS_FAILURE,
UPDATE_ASSET_STATE_REQUEST,
UPDATE_ASSET_STATE_SUCCESS,
UPDATE_ASSET_STATE_FAILURE
UPDATE_ASSET_STATE_FAILURE,
UPDATE_ASSETS
} from '../constants/assets';
import coralApi from '../../../coral-framework/helpers/response';
/**
@@ -34,3 +36,7 @@ export const updateAssetState = (id, closedAt) => (dispatch) => {
dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
.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: '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};
};
@@ -3,3 +3,12 @@ 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 = (userId, userName, commentId) => {
return {type: actions.SHOW_BANUSER_DIALOG, userId, userName, commentId};
};
export const hideBanUserDialog = (showDialog) => {
return {type: actions.HIDE_BANUSER_DIALOG, showDialog};
};
@@ -16,6 +16,7 @@
.rightPanel {
position: absolute;
top: 0;
right: 0;
width: 170px;
height: 100%;
@@ -1,6 +1,9 @@
export const FETCH_ASSETS_REQUEST = 'FETCH_ASSETS_REQUEST';
export const FETCH_ASSETS_SUCCESS = 'FETCH_ASSETS_SUCCESS';
export const FETCH_ASSETS_FAILURE = 'FETCH_ASSETS_FAILURE';
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_FAILURE = 'UPDATE_ASSET_STATE_FAILURE';
export const UPDATE_ASSETS = 'UPDATE_ASSETS';
@@ -1,3 +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,28 +1,20 @@
import React, {Component} from 'react';
import key from 'keymaster';
import {connect} from 'react-redux';
import {compose} from 'react-apollo';
import {Spinner} from 'coral-ui';
import {withRouter} from 'react-router';
import key from 'keymaster';
import isEqual from 'lodash/isEqual';
import {modQueueQuery} from '../../graphql/queries';
import {
updateStatus,
showBanUserDialog,
hideBanUserDialog,
fetchPremodQueue,
fetchRejectedQueue,
fetchFlaggedQueue,
fetchModerationQueueComments,
} from 'actions/comments';
import {fetchSettings} from 'actions/settings';
import {userStatusUpdate, sendNotificationEmail} from 'actions/users';
import {updateAssets} from 'actions/assets';
import {setActiveTab, toggleModal, singleView} from 'actions/moderation';
import {Spinner} from 'coral-ui';
import ModerationQueue from './ModerationQueue';
import ModerationQueueHeader from './components/ModerationQueueHeader';
import ModerationMenu from './components/ModerationMenu';
import ModerationHeader from './components/ModerationHeader';
import NotFoundAsset from './components/NotFoundAsset';
class ModerationContainer extends Component {
@@ -42,29 +34,37 @@ class ModerationContainer extends Component {
key.unbind('esc');
}
onTabClick = (activeTab) => {
const {setActiveTab} = this.props;
setActiveTab(activeTab);
}
onClose = () => {
const {toggleModal} = this.props;
toggleModal(false);
componentWillReceiveProps(nextProps) {
const {updateAssets} = this.props;
if(!isEqual(nextProps.data.assets, this.props.data.assets)) {
updateAssets(nextProps.data.assets);
}
}
render () {
const {data, moderation, settings} = this.props;
const {data, moderation, settings, assets} = this.props;
const providedAssetId = this.props.params.id;
let asset;
if (data.loading) {
return <div><Spinner/></div>;
}
const enablePremodTab = data.premod.length;
if (providedAssetId) {
asset = assets.find(asset => asset.id === this.props.params.id);
if (!asset) {
return <NotFoundAsset assetId={providedAssetId} />;
}
}
const enablePremodTab = !!data.premod.length;
return (
<div>
<ModerationQueueHeader
onTabClick={this.onTabClick}
<ModerationHeader asset={asset} />
<ModerationMenu
onTabClick={this.props.onTabClick}
enablePremodTab={enablePremodTab}
{...moderation} />
<ModerationQueue
@@ -80,33 +80,20 @@ class ModerationContainer extends Component {
const mapStateToProps = state => ({
moderation: state.moderation.toJS(),
settings: state.settings.toJS()
settings: state.settings.toJS(),
assets: state.assets.get('assets')
});
const mapDispatchToProps = dispatch => ({
setActiveTab: tab => dispatch(setActiveTab(tab)),
toggleModal: open => dispatch(toggleModal(open)),
onTabClick: activeTab => dispatch(setActiveTab(activeTab)),
toggleModal: toggle => dispatch(toggleModal(toggle)),
onClose: () => dispatch(toggleModal(false)),
singleView: () => dispatch(singleView()),
fetchSettings: () => dispatch(fetchSettings()),
fetchModerationQueueComments: () => dispatch(fetchModerationQueueComments()),
fetchPremodQueue: () => dispatch(fetchPremodQueue()),
fetchRejectedQueue: () => dispatch(fetchRejectedQueue()),
fetchFlaggedQueue: () => dispatch(fetchFlaggedQueue()),
showBanUserDialog: (userId, userName, commentId) => dispatch(showBanUserDialog(userId, userName, commentId)),
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
userStatusUpdate: (status, userId, commentId) => dispatch(userStatusUpdate(status, userId, commentId)).then(() => {
dispatch(fetchModerationQueueComments());
}),
suspendUser: (userId, subject, text) => dispatch(userStatusUpdate('suspended', userId))
.then(() => dispatch(sendNotificationEmail(userId, subject, text)))
.then(() => dispatch(fetchModerationQueueComments()))
,
updateStatus: (action, comment) => dispatch(updateStatus(action, comment))
updateAssets: assets => dispatch(updateAssets(assets)),
fetchSettings: () => dispatch(fetchSettings())
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withRouter,
modQueueQuery
)(ModerationContainer);
@@ -14,7 +14,6 @@ import translations from '../../../translations.json';
const Comment = props => {
const links = linkify.getMatches(props.body);
return (
<li tabIndex={props.index}
className={`mdl-card mdl-shadow--2dp ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
@@ -50,14 +49,24 @@ const Comment = props => {
: null}
</div>
</div>
{/* <div className={styles.itemBody}> */}
{/* Article title */}
{/* <a>Moderate this Article</a> */}
{/* </div> */}
<div className={styles.itemBody}>
<span className={styles.body}>
<p className={styles.body}>
<Linkify component='span' properties={{style: linkStyles}}>
<Highlighter searchWords={props.suspectWords} textToHighlight={props.body}/>
</Linkify>
</span>
</p>
</div>
<span className={styles.context}><a>View context</a></span>
{/* <span className={styles.context}> */}
{/* <a>View context</a> */}
{/* </span> */}
</li>
);
};
@@ -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;
@@ -5,7 +5,7 @@ import translations from '../../../translations.json';
const lang = new I18n(translations);
const ModerationQueueHeader = (props) => (
const ModerationMenu = (props) => (
<div className='mdl-tabs'>
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<a href='#all'
@@ -29,14 +29,6 @@ const ModerationQueueHeader = (props) => (
</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();
@@ -59,9 +51,9 @@ const ModerationQueueHeader = (props) => (
</div>
);
ModerationQueueHeader.propTypes = {
ModerationMenu.propTypes = {
activeTab: PropTypes.string.isRequired,
enablePremodTab: PropTypes.bool
};
export default ModerationQueueHeader;
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;
@@ -1,3 +1,62 @@
.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: #3949AB;
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 {
@@ -55,6 +55,12 @@
border-left: none;
border-right: none;
a {
color: rgb(44, 44, 44);
font-weight: 500;
text-decoration: none;
}
th {
font-size: 1.1em;
}
@@ -4,14 +4,10 @@ import {connect} from 'react-redux';
import I18n from 'coral-framework/modules/i18n/i18n';
import {fetchAssets, updateAssetState} from '../../actions/assets';
import translations from '../../translations.json';
import {
RadioGroup,
Radio,
Icon,
DataTable,
TableHeader
} from 'react-mdl';
import Pager from 'coral-ui/components/Pager';
import {Link} from 'react-router';
import {Pager, Icon} from 'coral-ui';
import {DataTable, TableHeader, RadioGroup, Radio} from 'react-mdl';
const limit = 25;
@@ -74,6 +70,8 @@ class Streams extends Component {
}
}
renderTitle = (title, {id}) => <Link to={`/admin/moderate/${id}`}>{title}</Link>
renderStatus = (closedAt, {id}) => {
const closed = closedAt && new Date(closedAt).getTime() < Date.now();
const statusMenuOpen = this.state.statusMenus[id];
@@ -104,6 +102,9 @@ class Streams extends Component {
render () {
const {search, sort, filter} = this.state;
const {assets} = this.props;
const assetsIds = assets.ids.map((id) => assets.byId[id]);
return (
<div className={styles.container}>
<div className={styles.leftColumn}>
@@ -142,16 +143,14 @@ class Streams extends Component {
</RadioGroup>
</div>
<div className={styles.mainContent}>
<DataTable
className={styles.streamsTable}
rows={assets.ids.map((id) => assets.byId[id])}>
<TableHeader name="title">{lang.t('streams.article')}</TableHeader>
<TableHeader name="publication_date" cellFormatter={this.renderDate}>
{lang.t('streams.pubdate')}
</TableHeader>
<TableHeader name="closedAt" cellFormatter={this.renderStatus} className={styles.status}>
{lang.t('streams.status')}
</TableHeader>
<DataTable className={styles.streamsTable} rows={assetsIds} onClick={this.goToModeration}>
<TableHeader name="title" cellFormatter={this.renderTitle}>{lang.t('streams.article')}</TableHeader>
<TableHeader name="publication_date" cellFormatter={this.renderDate}>
{lang.t('streams.pubdate')}
</TableHeader>
<TableHeader name="closedAt" cellFormatter={this.renderStatus} className={styles.status}>
{lang.t('streams.status')}
</TableHeader>
</DataTable>
<Pager
totalPages={Math.ceil((assets.count || 0) / limit)}
@@ -169,6 +168,7 @@ const mapStateToProps = ({assets}) => {
assets: assets.toJS()
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchAssets: (...args) => {
@@ -0,0 +1,39 @@
import React from 'react';
import translations from '../../translations.json';
import I18n from 'coral-framework/modules/i18n/i18n';
const lang = new I18n(translations);
const StreamsTable = props => (
<table className={'mdl-data-table'}>
<thead>
<tr>
<th className="mdl-data-table__cell--non-numeric">
{lang.t('streams.article')}
</th>
<th className="mdl-data-table__cell--non-numeric">
{lang.t('streams.pubdate')}
</th>
<th className="mdl-data-table__cell--non-numeric">
{lang.t('streams.status')}
</th>
</tr>
</thead>
<tbody>
{props.rows.map((row, i)=> (
<tr key={i}>
<td className="mdl-data-table__cell--non-numeric">
{row.title}
</td>
<td className="mdl-data-table__cell--non-numeric">
{row.publication_date}
</td>
<td className="mdl-data-table__cell--non-numeric">
{lang.t('streams.status')}
</td>
</tr>
))}
</tbody>
</table>
);
export default StreamsTable;
@@ -0,0 +1,6 @@
query Assets {
assets {
id
title
}
}
@@ -1,6 +1,6 @@
#import "../fragments/commentView.graphql"
query ($asset_id: ID!) {
query ModQueue ($asset_id: ID!) {
all: comments(query: {
statuses: [ACCEPTED, REJECTED, PREMOD],
asset_id: $asset_id
@@ -25,10 +25,8 @@ query ($asset_id: ID!) {
}) {
...commentView
}
account: comments(query: {
statuses: [ACCEPTED],
asset_id: $asset_id
}) {
...commentView
assets: assets {
id
title
}
}
+14 -8
View File
@@ -1,20 +1,26 @@
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({
byId: Map(),
ids: List()
ids: List(),
assets: List()
});
export default (state = initialState, action) => {
export default function assets (state = initialState, action) {
switch (action.type) {
case FETCH_ASSETS_SUCCESS:
case actions.FETCH_ASSETS_SUCCESS:
return replaceAssets(action, state);
case UPDATE_ASSET_STATE_REQUEST:
return state.setIn(['byId', action.id, 'closedAt'], action.closedAt);
default: return state;
case actions.UPDATE_ASSET_STATE_REQUEST:
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 assets = fromJS(action.assets.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {}));
+2
View File
@@ -2,6 +2,7 @@ const _ = require('lodash');
const Comment = require('./comment');
const Action = require('./action');
const User = require('./user');
module.exports = (context) => {
@@ -9,6 +10,7 @@ module.exports = (context) => {
return _.merge(...[
Comment,
Action,
User,
].map((mutators) => {
// Each set of mutators is a function which takes the context.
+34
View File
@@ -0,0 +1,34 @@
const UsersService = require('../../services/users');
const setUserStatus = ({user}, {id, status}) => {
console.log('------as-d-asd-a-sads-a-sad-dsa-----');
console.log('user', user);
console.log('id', id);
console.log('status', status);
return UsersService.setStatus(id, status)
.then((user) => {
console.log('result', user);
return user;
});
};
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: () => {},
}
};
};
+3
View File
@@ -8,6 +8,9 @@ const RootMutation = {
deleteAction(_, {id}, {mutators: {Action}}) {
return Action.delete({id});
},
setUserStatus(_, {id, status}, {mutators: {User}}) {
return User.setUserStatus({id, status});
}
};
module.exports = RootMutation;
+12
View File
@@ -61,6 +61,9 @@ type User {
# returns all comments based on a query.
comments(query: CommentsQuery): [Comment]
# returns user status
status: USER_STATUS
}
type Comment {
@@ -177,6 +180,13 @@ enum COMMENT_STATUS {
PREMOD
}
enum USER_STATUS {
ACTIVE
BANNED
PENDING
APPROVED
}
type RootQuery {
# retrieves site wide settings and defaults.
@@ -216,6 +226,8 @@ type RootMutation {
# delete an action based on the action id.
deleteAction(id: ID!): Boolean
# sets user status
setUserStatus(id: ID!, status: USER_STATUS!): Boolean
}
schema {
+2 -1
View File
@@ -147,7 +147,8 @@ const USER_GRAPH_OPERATIONS = [
'mutation:createComment',
'mutation:createAction',
'mutation:deleteAction',
'mutation:editName'
'mutation:editName',
'mutation:setUserStatus'
];
/**