mirror of
https://github.com/wassname/talk.git
synced 2026-08-07 11:29:44 +08:00
Merge branch 'master' into font-size-docs
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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}));
|
||||
};
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
@@ -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}));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
@@ -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()));
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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}));
|
||||
};
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+2
-3
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user