Reducers refactor, working Moderate Queue

This commit is contained in:
Belen Curcio
2017-02-09 07:00:39 -03:00
parent dc35425afd
commit e234fa7e0b
11 changed files with 325 additions and 206 deletions
@@ -29,7 +29,6 @@ class ModerationContainer extends Component {
componentWillMount() {
const {toggleModal, singleView} = this.props;
this.props.fetchModerationQueueComments();
this.props.fetchSettings();
key('s', () => singleView());
@@ -54,7 +53,7 @@ class ModerationContainer extends Component {
}
render () {
const {data, moderation} = this.props;
const {data, moderation, settings} = this.props;
if (data.loading) {
return <div><Spinner/></div>;
@@ -66,27 +65,16 @@ class ModerationContainer extends Component {
<ModerationQueue
activeTab={moderation.activeTab}
data={data}
onTabClick={this.onTabClick}
onClose={this.onClose}
{...this.props}
{...moderation}
suspectWords={settings.wordlist.suspect}
/>
</div>
);
}
}
// ModerationContainer.contextTypes = {
// router: React.PropTypes.func.isRequired
// };
const mapStateToProps = state => ({
moderation: state.moderation.toJS(),
comments: state.comments.toJS(),
settings: state.settings.toJS(),
users: state.users.toJS(),
actions: state.actions.toJS()
settings: state.settings.toJS()
});
const mapDispatchToProps = dispatch => ({
@@ -1,22 +1,29 @@
import React from 'react';
import React, {PropTypes} from 'react';
// import Comment from '../../components/Comment';
import Comment from './components/Comment';
export default (props) => {
const ModerationQueue = props => {
return (
<div>
{/* <Comment*/}
{/* comment={comment}*/}
{/* key={i}*/}
{/* author={comment.user}*/}
{/* />*/}
<ul>
{
props.data[props.activeTab].map((comment, i) =>
<div key={i}>
{comment.body}
</div>
)
props.data[props.activeTab].map((comment, i) => {
console.log(comment);
return <Comment
key={i}
index={i}
suspectWords={props.suspectWords}
{...comment}
/>;
})
}
</ul>
</div>
);
};
ModerationQueue.propTypes = {
data: PropTypes.object.isRequired
};
export default ModerationQueue;
@@ -0,0 +1,76 @@
import React, {PropTypes} from 'react';
import timeago from 'timeago.js';
import Linkify from 'react-linkify';
import Highlighter from 'react-highlight-words';
import styles from './styles.css';
import {Icon} from 'coral-ui';
// import ActionButton from '../components/ActionButton';
import I18n from 'coral-framework/modules/i18n/i18n';
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 : ''}`}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<span>{props.user.name}</span>
<span className={styles.created}>
{timeago().format(props.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
</span>
{props.flagged ? <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}`}>
{/* {props.modActions.map(*/}
{/* (action, i) =>*/}
{/* <ActionButton*/}
{/* option={action}*/}
{/* key={i}*/}
{/* type='COMMENTS'*/}
{/* comment={comment}*/}
{/* user={author}*/}
{/* menuOptionsMap={props.menuOptionsMap}*/}
{/* onClickAction={props.onClickAction}*/}
{/* onClickShowBanDialog={props.onClickShowBanDialog}/>*/}
{/* )}*/}
</div>
{props.user.banned === 'banned' ?
<span className={styles.banned}>
<Icon name='error_outline'/>
{lang.t('comment.banned_user')}
</span>
: null}
</div>
</div>
<div className={styles.itemBody}>
<span className={styles.body}>
<Linkify component='span' properties={{style: linkStyles}}>
<Highlighter searchWords={props.suspectWords} textToHighlight={props.body}/>
</Linkify>
</span>
</div>
</li>
);
};
const linkStyles = {
backgroundColor: 'rgb(255, 219, 135)',
padding: '1px 2px'
};
const linkify = new Linkify();
const lang = new I18n(translations);
Comment.propTypes = {
user: PropTypes.object.isRequired
};
export default Comment;
@@ -0,0 +1,184 @@
@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;
&: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;
}
.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: 20px;
flex: 1;
font-size: 0.88em;
color: black;
}
.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;
}
.banButton {
width: 114px;
letter-spacing: 1px;
i {
vertical-align: middle;
margin-right: 10px;
font-size: 14px;
}
}
@@ -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,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')));
};
-6
View File
@@ -1,19 +1,13 @@
import auth from './auth';
import users from './users';
import assets from './assets';
import actions from './actions';
import comments from './comments';
import settings from './settings';
import community from './community';
import moderation from './moderation';
export default {
auth,
users,
assets,
actions,
settings,
comments,
community,
moderation
};
+42 -39
View File
@@ -1,5 +1,5 @@
import {Map, List} from 'immutable';
import * as types from '../actions/settings';
import * as actions from '../actions/settings';
const initialState = Map({
settings: Map({
@@ -13,43 +13,46 @@ const initialState = Map({
fetchingSettings: false
});
// Handle the comment actions
export default (state = initialState, action) => {
export default function settings (state = initialState, action) {
switch (action.type) {
case types.SETTINGS_LOADING: return state.set('fetchingSettings', true).set('fetchSettingsError', null);
case types.SETTINGS_RECEIVED: return updateSettings(state, action);
case types.SETTINGS_FETCH_ERROR: return settingsFetchFailed(state, action);
case types.SETTINGS_UPDATED: return updateSettings(state, action);
case types.SAVE_SETTINGS_LOADING: return state.set('fetchingSettings', true).set('saveSettingsError', null);
case types.SAVE_SETTINGS_SUCCESS: return saveComplete(state, action);
case types.SAVE_SETTINGS_FAILED: return settingsSaveFailed(state, action);
case types.WORDLIST_UPDATED: return updateWordlist(state, action);
default: return state;
case actions.SETTINGS_LOADING:
return state
.set('fetchingSettings', true)
.set('fetchSettingsError', null);
case actions.SETTINGS_RECEIVED:
return state.merge({
fetchingSettings: null,
fetchSettingsError: null,
...action.settings
});
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);
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 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);
};
}
-28
View File
@@ -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));
};
@@ -2,7 +2,6 @@ import ApolloClient, {addTypename} from 'apollo-client';
import getNetworkInterface from './transport';
export const client = new ApolloClient({
connectToDevTools: true,
queryTransformer: addTypename,
dataIdFromObject: (result) => {
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle