Merge branch 'master' into font-size-docs

This commit is contained in:
Riley Davis
2016-12-22 10:40:30 -07:00
committed by GitHub
63 changed files with 1379 additions and 331 deletions
+2
View File
@@ -4,6 +4,7 @@ import {Router, Route, IndexRoute, browserHistory} from 'react-router';
import ModerationQueue from 'containers/ModerationQueue/ModerationQueue';
import CommentStream from 'containers/CommentStream/CommentStream';
import Configure from 'containers/Configure/Configure';
import Streams from 'containers/Streams/Streams';
import CommunityContainer from 'containers/Community/CommunityContainer';
import LayoutContainer from 'containers/LayoutContainer';
@@ -13,6 +14,7 @@ const routes = (
<Route path='embed' component={CommentStream} />
<Route path='community' component={CommunityContainer} />
<Route path='configure' component={Configure} />
<Route path='streams' component={Streams} />
</Route>
);
+36
View File
@@ -0,0 +1,36 @@
import {
FETCH_ASSETS_REQUEST,
FETCH_ASSETS_SUCCESS,
FETCH_ASSETS_FAILURE,
UPDATE_ASSET_STATE_REQUEST,
UPDATE_ASSET_STATE_SUCCESS,
UPDATE_ASSET_STATE_FAILURE
} from '../constants/assets';
import coralApi from '../../../coral-framework/helpers/response';
/**
* Action disptacher related to assets
*/
// Fetch a page of assets
// Get comments to fill each of the three lists on the mod queue
export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch) => {
dispatch({type: FETCH_ASSETS_REQUEST});
return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`)
.then(({result, count}) =>
dispatch({type: FETCH_ASSETS_SUCCESS,
assets: result,
count
}))
.catch(error => dispatch({type: FETCH_ASSETS_FAILURE, error}));
};
// Update an asset state
// Get comments to fill each of the three lists on the mod queue
export const updateAssetState = (id, closedAt) => (dispatch) => {
dispatch({type: UPDATE_ASSET_STATE_REQUEST});
return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
.then(() =>
dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
.catch(error => dispatch({type: UPDATE_ASSET_STATE_FAILURE, error}));
};
+53 -14
View File
@@ -1,30 +1,69 @@
import coralApi from '../../../coral-framework/helpers/response';
import * as commentActions from '../constants/comments';
// Get comments to fill each of the three lists on the mod queue
export const fetchModerationQueueComments = () => {
return dispatch => {
dispatch({type: commentActions.COMMENTS_MODERATION_QUEUE_FETCH});
return Promise.all([
coralApi('/queue/comments/pending'),
coralApi('/comments?status=rejected'),
coralApi('/comments?action_type=flag')
])
.then(([pending, rejected, flagged]) => {
/* Combine seperate calls into a single object */
flagged.comments.forEach(comment => comment.flagged = true);
return {
comments: [...pending.comments, ...rejected.comments, ...flagged.comments],
users: [...pending.users, ...rejected.users, ...flagged.users],
actions: [...pending.actions, ...rejected.actions, ...flagged.actions]
};
})
.then(({comments, users}) => {
/* Post comments and users to redux store. Actions will be posted when they are needed. */
dispatch({type: commentActions.USERS_MODERATION_QUEUE_FETCH_SUCCESS, users});
dispatch({type: commentActions.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS, comments});
});
};
};
// Create a new comment
export const createComment = (name, body) => {
return dispatch => {
const comment = {body, name};
return coralApi('/comments', {method: 'POST', comment})
.then(res => dispatch({type: commentActions.COMMENT_CREATE_SUCCESS, comment: res}))
.catch(error => dispatch({type: commentActions.COMMENT_CREATE_FAILED, error}));
};
};
/**
* Action disptacher related to comments
*/
export const updateStatus = (status, id) => (dispatch, getState) => {
dispatch({type: 'COMMENT_STATUS_UPDATE', id, status});
dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)});
// Update a comment. Now to update a comment we need to send back the whole object
export const updateStatus = (status, comment) => {
return dispatch => {
dispatch({type: commentActions.COMMENT_STATUS_UPDATE, id: comment.id, status});
return coralApi(`/comments/${comment.id}/status`, {method: 'PUT', body: {status}})
.then(res => dispatch({type: commentActions.COMMENT_UPDATE_SUCCESS, res}))
.catch(error => dispatch({type: commentActions.COMMENT_UPDATE_FAILED, error}));
};
};
export const flagComment = id => (dispatch, getState) => {
dispatch({type: 'COMMENT_FLAG', id});
dispatch({type: commentActions.COMMENT_FLAG, id});
dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)});
};
export const createComment = (name, body) => dispatch => {
dispatch({type: 'COMMENT_CREATE', name, body});
};
// Dialog Actions
export const showBanUserDialog = (userId, userName, commentId) => {
return dispatch => {
dispatch({type: 'SHOW_BANUSER_DIALOG', userId, userName, commentId});
};
return {type: commentActions.SHOW_BANUSER_DIALOG, userId, userName, commentId};
};
export const hideBanUserDialog = (showDialog) => {
return dispatch => {
dispatch({type: 'HIDE_BANUSER_DIALOG', showDialog});
};
return {type: commentActions.HIDE_BANUSER_DIALOG, showDialog};
};
+8 -7
View File
@@ -1,14 +1,15 @@
import coralApi from '../../../coral-framework/helpers/response';
import * as actions from '../constants/user';
/**
* Action disptacher related to users
*/
//
// export const banUser = (status, author_id) => (dispatch) => {
// dispatch({type: 'USER_STATUS_UPDATE', author_id, status});
// };
export const banUser = (status, userId, commentId) => {
// change status of a user
export const userStatusUpdate = (status, userId, commentId) => {
return dispatch => {
dispatch({type: 'USER_BAN', status, userId, commentId});
dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH'});
dispatch({type: actions.UPDATE_STATUS_REQUEST});
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
.then(res => dispatch({type: actions.UPDATE_STATUS_SUCCESS, res}))
.catch(error => dispatch({type: actions.UPDATE_STATUS_FAILURE, error}));
};
};
+8 -6
View File
@@ -30,7 +30,7 @@ export default props => {
<div>
{links ?
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={styles.actions}>
<div className={`actions ${styles.actions}`}>
{props.actions.map((action, i) => getActionButton(action, i, props))}
</div>
</div>
@@ -63,21 +63,23 @@ const getActionButton = (action, i, props) => {
if (action === 'ban') {
return (
<Button
disabled={banned ? 'disabled' : ''}
className='ban'
cStyle='black'
disabled={banned ? 'disabled' : ''}
onClick={() => props.onClickShowBanDialog(author.id, author.displayName, comment.id)}
key={i} >
key={i}
>
{lang.t('comment.ban_user')}
</Button>
);
}
return (
<FabButton
className={styles.actionButton}
icon={props.actionsMap[action].icon}
className={`${action} ${styles.actionButton}`}
cStyle={action}
icon={props.actionsMap[action].icon}
key={i}
onClick={() => props.onClickAction(props.actionsMap[action].status, comment.id)}
onClick={() => props.onClickAction(props.actionsMap[action].status, comment)}
/>
);
};
@@ -126,7 +126,10 @@ export default class CommentList extends React.Component {
const {active} = this.state;
return (
<ul className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}>
<ul
className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}
id='commentList'
>
{commentIds.map((commentId, index) => {
const comment = comments[commentId];
const author = users[comment.author_id];
@@ -16,6 +16,8 @@ export default ({handleLogout}) => (
activeClassName={styles.active}>{lang.t('configure.community')}</Link>
<Link className={styles.navLink} to="/admin/configure"
activeClassName={styles.active}>{lang.t('configure.configure')}</Link>
<Link className={styles.navLink} to="/admin/streams"
activeClassName={styles.active}>{lang.t('configure.streams')}</Link>
</Navigation>
<div className={styles.rightPanel}>
<ul>
@@ -0,0 +1,6 @@
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';
+10 -1
View File
@@ -1,3 +1,12 @@
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
export const USER_BAN_SUCESS = 'USER_BAN_SUCESS';
export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS';
export const COMMENTS_MODERATION_QUEUE_FETCH = 'COMMENTS_MODERATION_QUEUE_FETCH';
export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS = 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS';
export const COMMENT_CREATE_SUCCESS = 'COMMENT_CREATE_SUCCESS';
export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED';
export const COMMENT_STREAM_FETCH_SUCCESS = 'COMMENT_STREAM_FETCH_SUCCESS';
export const COMMENT_UPDATE_SUCCESS = 'COMMENT_UPDATE_SUCCESS';
export const COMMENT_UPDATE_FAILED = 'COMMENT_UPDATE_FAILED';
export const COMMENT_STATUS_UPDATE = 'COMMENT_STATUS_UPDATE';
export const COMMENT_FLAG = 'COMMENT_FLAG';
+3
View File
@@ -0,0 +1,3 @@
export const UPDATE_STATUS_REQUEST = 'UPDATE_STATUS_REQUEST';
export const UPDATE_STATUS_SUCCESS = 'UPDATE_STATUS_SUCCESS';
export const UPDATE_STATUS_FAILURE = 'UPDATE_STATUS_FAILURE';
@@ -7,7 +7,7 @@ import styles from './Community.css';
import Table from './Table';
import Loading from './Loading';
import NoResults from './NoResults';
import Pager from './Pager';
import Pager from 'coral-ui/components/Pager';
const lang = new I18n(translations);
@@ -71,6 +71,7 @@ const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => {
const CommentSettings = ({fetchingSettings, updateSettings, settingsError, settings, errors}) => {
if (fetchingSettings) {
/* maybe a spinner here at some point */
return <p>Loading settings...</p>;
}
@@ -1,14 +1,18 @@
import React from 'react';
import {connect} from 'react-redux';
import {Icon} from 'react-mdl';
import key from 'keymaster';
import ModerationKeysModal from 'components/ModerationKeysModal';
import CommentList from 'components/CommentList';
import BanUserDialog from 'components/BanUserDialog';
import {updateStatus, showBanUserDialog, hideBanUserDialog} from 'actions/comments';
import {banUser} from 'actions/users';
import {
updateStatus,
showBanUserDialog,
hideBanUserDialog,
fetchModerationQueueComments
} from 'actions/comments';
import {userStatusUpdate} from 'actions/users';
import styles from './ModerationQueue.css';
import I18n from 'coral-framework/modules/i18n/i18n';
@@ -32,7 +36,7 @@ class ModerationQueue extends React.Component {
// Fetch comments and bind singleView key before render
componentWillMount () {
this.props.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH'});
this.props.dispatch(fetchModerationQueueComments());
key('s', () => this.setState({singleView: !this.state.singleView}));
key('shift+/', () => this.setState({modalOpen: true}));
key('esc', () => this.setState({modalOpen: false}));
@@ -55,10 +59,10 @@ class ModerationQueue extends React.Component {
}
// Dispatch the update status action
onCommentAction (action, id) {
onCommentAction (action, comment) {
// If not banning then change the status to approved or flagged as action = status
this.props.dispatch(updateStatus(action, id));
this.props.dispatch(updateStatus(action, comment));
}
showBanUserDialog (userId, userName, commentId) {
@@ -70,11 +74,10 @@ class ModerationQueue extends React.Component {
}
banUser (userId, commentId) {
this.props.dispatch(banUser('banned', userId, commentId));
}
showShortcuts = () => {
this.setState({modalOpen: true});
this.props.dispatch(userStatusUpdate('banned', userId, commentId))
.then(() => {
this.props.dispatch(fetchModerationQueueComments());
});
}
onTabClick (activeTab) {
@@ -100,11 +103,6 @@ class ModerationQueue extends React.Component {
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.rejected')}</a>
<a href='#flagged' onClick={() => this.onTabClick('flagged')}
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.flagged')}</a>
<a href='#shortcuts' onClick={this.showShortcuts}
className={`mdl-tabs__tab ${styles.tab} ${styles.showShortcuts}`}>
<Icon name='keyboard' />
<span>{lang.t('modqueue.showshortcuts')}</span>
</a>
</div>
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='pending'>
<CommentList
@@ -113,7 +111,7 @@ class ModerationQueue extends React.Component {
commentIds={premodIds}
comments={comments.byId}
users={users.byId}
onClickAction={(action, commentId) => this.onCommentAction(action, commentId)}
onClickAction={(action, comment) => this.onCommentAction(action, comment)}
onClickShowBanDialog={(userId, userName, commentId) => this.showBanUserDialog(userId, userName, commentId)}
actions={['reject', 'approve', 'ban']}
loading={comments.loading} />
@@ -122,7 +120,7 @@ class ModerationQueue extends React.Component {
handleClose={() => this.hideBanUserDialog()}
onClickBanUser={(userId, commentId) => this.banUser(userId, commentId)}
user={comments.banUser}/>
</div>
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
<CommentList
isActive={activeTab === 'rejected'}
@@ -130,29 +128,26 @@ class ModerationQueue extends React.Component {
commentIds={rejectedIds}
comments={comments.byId}
users={users.byId}
onClickAction={(action, id) => this.onCommentAction(action, id)}
onClickAction={(action, comment) => this.onCommentAction(action, comment)}
actions={['approve']}
loading={comments.loading} />
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='flagged'>
<CommentList
isActive={activeTab === 'flagged'}
isActive={activeTab === 'rejected'}
singleView={singleView}
commentIds={flaggedIds}
comments={comments.byId}
users={users.byId}
onClickAction={(action, id) => this.onCommentAction(action, id)}
onClickAction={(action, comment) => this.onCommentAction(action, comment)}
actions={['reject', 'approve']}
loading={comments.loading} />
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='shortcuts'>
<ModerationKeysModal open={modalOpen}
onClose={() => this.setState({modalOpen: false})} />
</div>
<ModerationKeysModal open={modalOpen}
onClose={() => this.setState({modalOpen: false})} />
</div>
</div>
);
}
}
@@ -0,0 +1,83 @@
.container {
padding: 10px;
display: flex;
}
.leftColumn {
width: 200px;
}
.mainContent {
width: calc(90% - 200px);
}
.searchIcon {
vertical-align: middle;
font-size: 18px;
color: #ccc;
}
.searchBox {
padding: 3px;
border: 1px solid #ccc;
border-radius: 3px;
width: 90%;
display: flex;
}
.searchBoxInput {
border: none;
flex: 1;
font-size: 14px;
}
.searchBoxInput:focus {
outline: none;
}
.optionHeader {
font-size: 16px;
font-weight: 900;
margin-top: 15px;
}
.optionDetail {
font-size: 16px;
margin-top: 3px;
}
.streamsTable {
width: 100%;
}
.radio {
display: block;
}
.statusMenu {
border-radius: 3px;
width: 10em;
text-align: center;
float: right;
border: 1px solid #ccc;
color: #fff;
cursor: pointer;
}
.statusMenuOpen {
padding: 10px;
background-color: #4caf50;
}
.statusMenuIcon {
float: right;
}
.statusMenuClosed {
padding: 10px;
background-color: #000;
}
.hidden {
display: none;
}
@@ -0,0 +1,180 @@
import React, {Component} from 'react';
import styles from './Streams.css';
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';
const limit = 25;
class Streams extends Component {
state = {
search: '',
sort: 'desc',
filter: 'all',
statusMenus: {},
timer: null,
page: 0
}
componentDidMount () {
this.props.fetchAssets(0, limit, '', this.state.sortBy);
}
onSettingChange = (setting) => (e) => {
let options = this.state;
this.setState({[setting]: e.target.value});
options[setting] = e.target.value;
this.props.fetchAssets(0, limit, options.search, options.sort, options.filter);
}
onSearchChange = (e) => {
this.setState((prevState) => {
prevState.search = e.target.value;
clearTimeout(prevState.timer);
const fetchAssets = this.props.fetchAssets;
prevState.timer = setTimeout(() => {
fetchAssets(0, limit, e.target.value, this.state.sort, this.state.filter);
}, 350);
return prevState;
});
}
renderDate = (date) => {
const d = new Date(date);
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
}
onStatusClick = (closeStream, id, statusMenuOpen) => () => {
if (statusMenuOpen) {
this.setState(prev => {
prev.statusMenus[id] = false;
return prev;
});
this.props.updateAssetState(id, closeStream ? Date.now() : null)
.then(() => {
const {search, sort, filter, page} = this.state;
this.props.fetchAssets(page, limit, search, sort, filter);
});
} else {
this.setState(prev => {
prev.statusMenus[id] = true;
return prev;
});
}
}
renderStatus = (closedAt, {id}) => {
const closed = closedAt && new Date(closedAt).getTime() < Date.now();
const statusMenuOpen = this.state.statusMenus[id];
return <div className={styles.statusMenu}>
<div
className={closed ? styles.statusMenuClosed : styles.statusMenuOpen}
onClick={this.onStatusClick(closed, id, statusMenuOpen)}>
{closed ? lang.t('streams.closed') : lang.t('streams.open')}
{!statusMenuOpen && <Icon className={styles.statusMenuIcon} name='keyboard_arrow_down'/>}
</div>
{
statusMenuOpen &&
<div
className={!closed ? styles.statusMenuClosed : styles.statusMenuOpen}
onClick={this.onStatusClick(!closed, id, statusMenuOpen)}>
{!closed ? lang.t('streams.closed') : lang.t('streams.open')}
</div>
}
</div>;
}
onPageClick = (page) => {
this.setState({page});
const {search, sort, filter} = this.state;
this.props.fetchAssets((page - 1) * limit, limit, search, sort, filter);
}
render () {
const {search, sort, filter} = this.state;
const {assets} = this.props;
return <div className={styles.container}>
<div className={styles.leftColumn}>
<div className={styles.searchBox}>
<Icon name='search' className={styles.searchIcon}/>
<input
type='text'
value={search}
className={styles.searchBoxInput}
onChange={this.onSearchChange}
placeholder={lang.t('streams.search')}/>
</div>
<div className={styles.optionHeader}>{lang.t('streams.filter-streams')}</div>
<div className={styles.optionDetail}>{lang.t('streams.stream-status')}</div>
<RadioGroup
name='status filter'
value={filter}
childContainer='div'
onChange={this.onSettingChange('filter')}>
<Radio value='all'>{lang.t('streams.all')}</Radio>
<Radio value='open'>{lang.t('streams.open')}</Radio>
<Radio value='closed'>{lang.t('streams.closed')}</Radio>
</RadioGroup>
<div className={styles.optionHeader}>{lang.t('streams.sort-by')}</div>
<RadioGroup
name='sort by'
value={sort}
childContainer='div'
onChange={this.onSettingChange('sort')}>
<Radio value='desc'>{lang.t('streams.newest')}</Radio>
<Radio value='asc'>{lang.t('streams.oldest')}</Radio>
</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 numeric name="publication_date" cellFormatter={this.renderDate}>
{lang.t('streams.pubdate')}
</TableHeader>
<TableHeader numeric name="closedAt" cellFormatter={this.renderStatus}>
{lang.t('streams.status')}
</TableHeader>
</DataTable>
<Pager
totalPages={Math.ceil((assets.count || 0) / limit)}
page={this.state.page}
onNewPageHandler={this.onPageClick}
/>
</div>
</div>;
}
}
const mapStateToProps = ({assets}) => {
return {
assets: assets.toJS()
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchAssets: (...args) => {
dispatch(fetchAssets.apply(this, args));
},
updateAssetState: (...args) => dispatch(updateAssetState.apply(this, args))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Streams);
const lang = new I18n(translations);
+24
View File
@@ -0,0 +1,24 @@
import {Map, List, fromJS} from 'immutable';
import {FETCH_ASSETS_SUCCESS, UPDATE_ASSET_STATE_REQUEST} from '../constants/assets';
const initialState = Map({
byId: Map(),
ids: List()
});
export default (state = initialState, action) => {
switch (action.type) {
case FETCH_ASSETS_SUCCESS:
return replaceAssets(action, state);
case UPDATE_ASSET_STATE_REQUEST:
return state.setIn(['byId', action.id, 'closedAt'], action.closedAt);
default: return state;
}
};
const replaceAssets = (action, state) => {
const assets = fromJS(action.assets.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {}));
return state.set('byId', assets)
.set('count', action.count)
.set('ids', List(assets.keys()));
};
+10 -8
View File
@@ -1,4 +1,5 @@
import * as actions from '../constants/comments';
import * as userActions from '../constants/user';
import {Map, List, fromJS} from 'immutable';
/**
@@ -23,16 +24,17 @@ const initialState = Map({
// Handle the comment actions
export default (state = initialState, action) => {
switch (action.type) {
case 'COMMENTS_MODERATION_QUEUE_FETCH': return state.set('loading', true);
case 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceComments(action, state);
case 'COMMENTS_MODERATION_QUEUE_FAILED': return state.set('loading', false);
case 'COMMENT_STATUS_UPDATE': return updateStatus(state, action);
case 'COMMENT_FLAG': return flag(state, action);
case 'COMMENT_CREATE_SUCCESS': return addComment(state, action);
case 'COMMENT_STREAM_FETCH_SUCCESS': return replaceComments(action, state);
case actions.COMMENTS_MODERATION_QUEUE_FETCH: 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: 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_SUCESS: 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;
}
};
+3 -1
View File
@@ -4,6 +4,7 @@ import settings from 'reducers/settings';
import community from 'reducers/community';
import users from 'reducers/users';
import auth from 'reducers/auth';
import assets from 'reducers/assets';
// Combine all reducers into a main one
export default combineReducers({
@@ -11,5 +12,6 @@ export default combineReducers({
comments,
community,
auth,
users
users,
assets
});
+1 -2
View File
@@ -2,7 +2,6 @@
import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import mainReducer from 'reducers';
import talkAdapter from 'services/talk-adapter';
/**
* Create the store by merging the app reducers with
@@ -14,5 +13,5 @@ import talkAdapter from 'services/talk-adapter';
export default createStore(
mainReducer,
window.devToolsExtension && window.devToolsExtension(),
applyMiddleware(thunk, talkAdapter)
applyMiddleware(thunk)
);
@@ -1,96 +0,0 @@
import coralApi from '../../../coral-framework/helpers/response';
/**
* The adapter is a redux middleware that interecepts the actions that need
* to interface with the backend, do the job and return the results.
* The idea is that if we expose the required actions to handle to devs, the
* moderation app can be platform agnostic. This same client could work not only
* for the coral but also for wordpress comments, disqus and many more.
*/
// Intercept redux actions and act over the ones we are interested
export default store => next => action => {
switch (action.type) {
case 'COMMENTS_MODERATION_QUEUE_FETCH':
fetchModerationQueueComments(store);
break;
case 'COMMENT_UPDATE':
updateComment(store, action.comment);
break;
case 'COMMENT_CREATE':
createComment(store, action.name, action.body);
break;
case 'USER_BAN':
userStatusUpdate(store, action.status, action.userId, action.commentId);
break;
}
next(action);
};
// Get comments to fill each of the three lists on the mod queue
const fetchModerationQueueComments = store =>
Promise.all([
coralApi('/queue/comments/pending'),
coralApi('/comments?status=rejected'),
coralApi('/comments?action_type=flag')
])
.then(([pending, rejected, flagged]) => {
/* Combine seperate calls into a single object */
let all = {};
all.comments = pending.comments
.concat(rejected.comments)
.concat(flagged.comments.map(comment => {
comment.flagged = true;
return comment;
}));
all.users = pending.users
.concat(rejected.users)
.concat(flagged.users);
all.actions = pending.actions
.concat(rejected.actions)
.concat(flagged.actions);
return all;
})
.then(all => {
/* Post comments and users to redux store. Actions will be posted when they are needed. */
store.dispatch({type: 'USERS_MODERATION_QUEUE_FETCH_SUCCESS',
users: all.users});
store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS',
comments: all.comments});
});
// .catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error}));
// Update a comment. Now to update a comment we need to send back the whole object
const updateComment = (store, comment) => {
coralApi(`/comments/${comment.get('id')}/status`, {method: 'PUT', body: {status: comment.get('status')}})
.then(res => store.dispatch({type: 'COMMENT_UPDATE_SUCCESS', res}))
.catch(error => store.dispatch({type: 'COMMENT_UPDATE_FAILED', error}));
};
// Create a new comment
const createComment = (store, name, comment) => {
const body = {
status: 'Untouched',
body: comment,
name: name,
createdAt: Date.now()
};
return coralApi('/comments', {method: 'POST', body})
.then(res => store.dispatch({type: 'COMMENT_CREATE_SUCCESS', comment: res}))
.catch(error => store.dispatch({type: 'COMMENT_CREATE_FAILED', error}));
};
// Ban a user
const userStatusUpdate = (store, status, userId, commentId) => {
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
.then(res => store.dispatch({type: 'USER_BAN_SUCESS', res}))
.catch(error => store.dispatch({type: 'USER_BAN_FAILED', error}));
};
+33
View File
@@ -56,6 +56,7 @@
"moderate": "Moderate",
"configure": "Configure",
"community": "Community",
"streams": "Streams",
"closed-comments-desc": "Write a message for closed threads",
"closed-comments-label": "Write a message...",
"hours": "Hours",
@@ -73,6 +74,22 @@
"note": "Note: Banning this user will also place this comment in the Rejected queue.",
"cancel": "Cancel",
"yes_ban_user": "Yes, Ban User"
},
"streams": {
"search": "Search",
"filter-streams": "Filter Streams",
"stream-status": "Stream Status",
"all": "All",
"open": "Open",
"closed": "Closed",
"newest": "Newest",
"oldest": "Oldest",
"sort-by": "Sort By",
"open": "Open",
"closed": "Closed",
"article": "Article",
"pubdate": "Publication Date",
"status": "Status"
}
},
"es": {
@@ -139,6 +156,22 @@
"note": "Nota: Suspender este usuario también va a colocar este comentario en la cola de Rechazados.",
"cancel": "Cancelar",
"yes_ban_user": "Si, Suspendan el usuario"
},
"streams": {
"search": "",
"filter-streams": "",
"stream-status": "",
"all": "",
"open": "",
"closed": "",
"newest": "",
"oldest": "",
"sort-by": "",
"open": "",
"closed": "",
"article": "",
"pubdate": "",
"status": ""
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ import React from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
const name = 'coral-plugin-flags';
const name = 'coral-plugin-likes';
const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, currentUser, banned}) => {
const liked = like && like.current_user;
@@ -9,10 +9,11 @@ import ForgotContent from './ForgotContent';
const SignDialog = ({open, view, handleClose, offset, ...props}) => (
<Dialog
className={styles.dialog}
id="signInDialog"
open={open}
style={{
position: 'relative',
top: offset !== 0 && offset
top: offset !== 0 && offset
}}>
<span className={styles.close} onClick={handleClose}>×</span>
{view === 'SIGNIN' && <SignInContent {...props} />}
+3 -1
View File
@@ -9,7 +9,9 @@ const UserBox = ({className, user, logout, ...props}) => (
className={`${styles.userBox} ${className ? className : ''}`}
{...props}
>
{lang.t('signIn.loggedInAs')} <a>{user.displayName}</a>. {lang.t('signIn.notYou')} <a onClick={logout}>{lang.t('signIn.logout')}</a>
{lang.t('signIn.loggedInAs')}
<a>{user.displayName}</a>. {lang.t('signIn.notYou')}
<a onClick={logout} id='logout'>{lang.t('signIn.logout')}</a>
</div>
);
@@ -113,6 +113,7 @@ input.error{
font-weight: bold;
cursor: pointer;
margin: 0px;
margin-left: 4px;
padding-bottom: 2px;
border-bottom: solid 1px black;
}
@@ -12,7 +12,7 @@ const Pager = ({totalPages, page, onNewPageHandler}) => (
<div className="pager">
<ul>
{
(totalPages > page) ?
(totalPages > page && totalPages > 1) ?
<li
className={`mdl-button mdl-js-button ${styles.li}`}
onClick={() => onNewPageHandler(page - 1)}>
@@ -23,7 +23,7 @@ const Pager = ({totalPages, page, onNewPageHandler}) => (
}
{Rows(page, totalPages, onNewPageHandler)}
{
(page < totalPages) ?
(page < totalPages && totalPages > 1) ?
<li
className={`mdl-button mdl-js-button ${styles.li}`}
onClick={() => onNewPageHandler(page + 1)}>
@@ -42,4 +42,3 @@ Pager.propTypes = {
};
export default Pager;