Merge branch 'master' into csrf

This commit is contained in:
gaba
2016-12-22 14:23:01 -08:00
56 changed files with 1802 additions and 7840 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}));
};
+15 -13
View File
@@ -1,10 +1,11 @@
import coralApi from '../../../coral-framework/helpers/response';
import * as commentActions from '../constants/comments';
import * as commentTypes from '../constants/comments';
import * as actionTypes from '../constants/actions';
// 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});
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
return Promise.all([
coralApi('/queue/comments/pending'),
coralApi('/comments?status=rejected'),
@@ -20,11 +21,12 @@ export const fetchModerationQueueComments = () => {
actions: [...pending.actions, ...rejected.actions, ...flagged.actions]
};
})
.then(({comments, users}) => {
.then(({comments, users, actions}) => {
/* 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});
dispatch({type: commentTypes.USERS_MODERATION_QUEUE_FETCH_SUCCESS, users});
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS, comments});
dispatch({type: actionTypes.ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS, actions});
});
};
@@ -35,8 +37,8 @@ export const createComment = (name, body, _csrf) => {
return dispatch => {
const comment = {body, name, _csrf};
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}));
.then(res => dispatch({type: commentTypes.COMMENT_CREATE_SUCCESS, comment: res}))
.catch(error => dispatch({type: commentTypes.COMMENT_CREATE_FAILED, error}));
};
};
@@ -47,23 +49,23 @@ export const createComment = (name, body, _csrf) => {
// 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});
dispatch({type: commentTypes.COMMENT_STATUS_UPDATE_REQUEST, 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}));
.then(res => dispatch({type: commentTypes.COMMENT_STATUS_UPDATE_SUCCESS, res}))
.catch(error => dispatch({type: commentTypes.COMMENT_STATUS_UPDATE_FAILURE, error}));
};
};
export const flagComment = id => (dispatch, getState) => {
dispatch({type: commentActions.COMMENT_FLAG, id});
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: commentActions.SHOW_BANUSER_DIALOG, userId, userName, commentId};
return {type: commentTypes.SHOW_BANUSER_DIALOG, userId, userName, commentId};
};
export const hideBanUserDialog = (showDialog) => {
return {type: commentActions.HIDE_BANUSER_DIALOG, showDialog};
return {type: commentTypes.HIDE_BANUSER_DIALOG, showDialog};
};
@@ -10,6 +10,8 @@ export const SAVE_SETTINGS_LOADING = 'SAVE_SETTINGS_LOADING';
export const SAVE_SETTINGS_SUCCESS = 'SAVE_SETTINGS_SUCCESS';
export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
export const WORDLIST_UPDATED = 'WORDLIST_UPDATED';
export const fetchSettings = () => dispatch => {
dispatch({type: SETTINGS_LOADING});
coralApi('/settings')
@@ -21,10 +23,16 @@ export const fetchSettings = () => dispatch => {
});
};
// for updating top-level settings
export const updateSettings = settings => {
return {type: SETTINGS_UPDATED, settings};
};
// this is a nested property, so it needs a special action.
export const updateWordlist = (listName, list) => {
return {type: WORDLIST_UPDATED, listName, list};
};
export const saveSettingsToServer = () => (dispatch, getState) => {
let settings = getState().settings.toJS().settings;
if (settings.charCount) {
+5 -2
View File
@@ -8,6 +8,7 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
import {Icon} from 'react-mdl';
import Highlighter from 'react-highlight-words';
import {FabButton, Button} from 'coral-ui';
const linkify = new Linkify();
@@ -31,7 +32,7 @@ export default props => {
{links ?
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={`actions ${styles.actions}`}>
{props.actions.map((action, i) => getActionButton(action, i, props))}
{props.modActions.map((action, i) => getActionButton(action, i, props))}
</div>
</div>
<div>
@@ -42,7 +43,9 @@ export default props => {
<div className={styles.itemBody}>
<span className={styles.body}>
<Linkify component='span' properties={{style: linkStyles}}>
{comment.body}
<Highlighter
searchWords={props.suspectWords}
textToHighlight={comment.body} />
</Linkify>
</span>
</div>
@@ -1,12 +1,11 @@
import React from 'react';
import React, {PropTypes} from 'react';
import styles from './CommentList.css';
import key from 'keymaster';
import Hammer from 'hammerjs';
import Comment from 'components/Comment';
// Each action has different meaning and configuration
const actions = {
const modActions = {
'reject': {status: 'rejected', icon: 'close', key: 'r'},
'approve': {status: 'accepted', icon: 'done', key: 't'},
'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'},
@@ -15,6 +14,23 @@ const actions = {
// Renders a comment list and allow performing actions
export default class CommentList extends React.Component {
static propTypes = {
isActive: PropTypes.bool,
singleView: PropTypes.bool,
commentIds: PropTypes.arrayOf(PropTypes.string).isRequired,
comments: PropTypes.object.isRequired,
users: PropTypes.object.isRequired,
onClickAction: PropTypes.func,
modActions: PropTypes.arrayOf(PropTypes.string),
loading: PropTypes.bool,
// list of actions (flags, etc) associated with the comments
actions: PropTypes.shape({
ids: PropTypes.arrayOf(PropTypes.string)
}),
suspectWords: PropTypes.arrayOf(PropTypes.string)
}
constructor (props) {
super(props);
@@ -44,22 +60,22 @@ export default class CommentList extends React.Component {
// Add swipe to approve or reject
bindGestures () {
const {actions} = this.props;
const {modActions} = this.props;
this._hammer = new Hammer(this.base);
this._hammer.get('swipe').set({direction: Hammer.DIRECTION_HORIZONTAL});
if (actions.indexOf('reject') !== -1) {
if (modActions.indexOf('reject') !== -1) {
this._hammer.on('swipeleft', () => this.props.singleView && this.actionKeyHandler('Rejected'));
}
if (actions.indexOf('approve') !== -1) {
if (modActions.indexOf('approve') !== -1) {
this._hammer.on('swiperight', () => this.props.singleView && this.actionKeyHandler('Approved'));
}
}
// Add key handlers. Each action has one and added j/k for moving around
bindKeyHandlers () {
this.props.actions.filter(action => actions[action].key).forEach(action => {
key(actions[action].key, 'commentList', () => this.props.isActive && this.actionKeyHandler(actions[action].status));
this.props.modActions.filter(action => modActions[action].key).forEach(action => {
key(modActions[action].key, 'commentList', () => this.props.isActive && this.actionKeyHandler(modActions[action].status));
});
key('j', 'commentList', () => this.props.isActive && this.moveKeyHandler('down'));
key('k', 'commentList', () => this.props.isActive && this.moveKeyHandler('up'));
@@ -122,7 +138,7 @@ export default class CommentList extends React.Component {
}
render () {
const {singleView, commentIds, comments, users, hideActive, key} = this.props;
const {singleView, commentIds, comments, users, hideActive, key, suspectWords} = this.props;
const {active} = this.state;
return (
@@ -133,14 +149,16 @@ export default class CommentList extends React.Component {
{commentIds.map((commentId, index) => {
const comment = comments[commentId];
const author = users[comment.author_id];
return <Comment comment={comment}
return <Comment
suspectWords={suspectWords}
comment={comment}
author={author}
key={index}
index={index}
onClickAction={this.onClickAction}
onClickShowBanDialog={this.onClickShowBanDialog}
actions={this.props.actions}
actionsMap={actions}
modActions={this.props.modActions}
actionsMap={modActions}
isActive={commentId === active}
hideActive={hideActive} />;
})}
@@ -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 @@
export const ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS = 'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS';
@@ -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';
+4 -5
View File
@@ -1,13 +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_REQUEST = 'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST';
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_STATUS_UPDATE_SUCCESS = 'COMMENT_STATUS_UPDATE_SUCCESS';
export const COMMENT_STATUS_UPDATE_FAILURE = 'COMMENT_STATUS_UPDATE_FAILURE';
export const COMMENT_STATUS_UPDATE_REQUEST = 'COMMENT_STATUS_UPDATE_REQUEST';
export const COMMENT_FLAG = 'COMMENT_FLAG';
@@ -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);
@@ -69,110 +69,115 @@ const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => {
}
};
const CommentSettings = ({fetchingSettings, updateSettings, settingsError, settings, errors}) => {
const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError, settings, errors}) => {
if (fetchingSettings) {
/* maybe a spinner here at some point */
return <p>Loading settings...</p>;
}
return <List>
<ListItem className={`${styles.configSetting} ${settings.moderation === 'pre' ? styles.enabledSetting : styles.disabledSetting}`}>
<ListItemAction>
<Checkbox
onChange={updateModeration(updateSettings, settings.moderation)}
checked={settings.moderation === 'pre'} />
</ListItemAction>
<ListItemContent>
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
<p className={settings.moderation === 'pre' ? '' : styles.disabledSettingText}>
{lang.t('configure.enable-pre-moderation-text')}
</p>
</ListItemContent>
</ListItem>
<ListItem className={`${styles.configSetting} ${settings.charCountEnable ? styles.enabledSetting : styles.disabledSetting}`}>
<ListItemAction>
<Checkbox
onChange={updateCharCountEnable(updateSettings, settings.charCountEnable)}
checked={settings.charCountEnable} />
</ListItemAction>
<ListItemContent>
<div className={styles.settingsHeader}>{lang.t('configure.comment-count-header')}</div>
<p className={settings.charCountEnable ? '' : styles.disabledSettingText}>
<span>{lang.t('configure.comment-count-text-pre')}</span>
<input type='text'
className={`${styles.charCountTexfield} ${settings.charCountEnable && styles.charCountTexfieldEnabled}`}
htmlFor='charCount'
onChange={updateCharCount(updateSettings, settingsError)}
value={settings.charCount}/>
<span>{lang.t('configure.comment-count-text-post')}</span>
{
errors.charCount &&
<span className={styles.settingsError}>
<br/>
<Icon name="error_outline"/>
{lang.t('configure.comment-count-error')}
</span>
}
</p>
</ListItemContent>
</ListItem>
<ListItem threeLine className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? styles.enabledSetting : styles.disabledSetting}`}>
<ListItemAction>
<Checkbox
onChange={updateInfoBoxEnable(updateSettings, settings.infoBoxEnable)}
checked={settings.infoBoxEnable} />
</ListItemAction>
<ListItemContent>
{lang.t('configure.include-comment-stream')}
<p>
{lang.t('configure.include-comment-stream-desc')}
</p>
</ListItemContent>
</ListItem>
<ListItem className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? null : styles.hidden}`} >
<ListItemContent>
<Textfield
onChange={updateInfoBoxContent(updateSettings)}
value={settings.infoBoxContent}
label={lang.t('configure.include-text')}
rows={3}/>
</ListItemContent>
</ListItem>
<ListItem className={styles.configSettingInfoBox}>
<ListItemContent>
{lang.t('configure.close-after')}
<br />
<Textfield
type='number'
pattern='[0-9]+'
style={{width: 50}}
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout)}
value={getTimeoutAmount(settings.closedTimeout)}
label={lang.t('configure.closed-comments-label')} />
<div className={styles.configTimeoutSelect}>
<SelectField
label="comments closed time window"
value={getTimeoutMeasure(settings.closedTimeout)}
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout, true)}>
<Option value={'hours'}>{lang.t('configure.hours')}</Option>
<Option value={'days'}>{lang.t('configure.days')}</Option>
<Option value={'weeks'}>{lang.t('configure.weeks')}</Option>
</SelectField>
</div>
</ListItemContent>
</ListItem>
<ListItem className={styles.configSettingInfoBox}>
<ListItemContent>
{lang.t('configure.closed-comments-desc')}
<Textfield
onChange={updateClosedMessage(updateSettings)}
value={settings.closedMessage}
label={lang.t('configure.closed-comments-label')}
rows={3}/>
</ListItemContent>
</ListItem>
</List>;
return (
<div>
<h3>{title}</h3>
<List>
<ListItem className={`${styles.configSetting} ${settings.moderation === 'pre' ? styles.enabledSetting : styles.disabledSetting}`}>
<ListItemAction>
<Checkbox
onChange={updateModeration(updateSettings, settings.moderation)}
checked={settings.moderation === 'pre'} />
</ListItemAction>
<ListItemContent>
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
<p className={settings.moderation === 'pre' ? '' : styles.disabledSettingText}>
{lang.t('configure.enable-pre-moderation-text')}
</p>
</ListItemContent>
</ListItem>
<ListItem className={`${styles.configSetting} ${settings.charCountEnable ? styles.enabledSetting : styles.disabledSetting}`}>
<ListItemAction>
<Checkbox
onChange={updateCharCountEnable(updateSettings, settings.charCountEnable)}
checked={settings.charCountEnable} />
</ListItemAction>
<ListItemContent>
<div className={styles.settingsHeader}>{lang.t('configure.comment-count-header')}</div>
<p className={settings.charCountEnable ? '' : styles.disabledSettingText}>
<span>{lang.t('configure.comment-count-text-pre')}</span>
<input type='text'
className={`${styles.charCountTexfield} ${settings.charCountEnable && styles.charCountTexfieldEnabled}`}
htmlFor='charCount'
onChange={updateCharCount(updateSettings, settingsError)}
value={settings.charCount}/>
<span>{lang.t('configure.comment-count-text-post')}</span>
{
errors.charCount &&
<span className={styles.settingsError}>
<br/>
<Icon name="error_outline"/>
{lang.t('configure.comment-count-error')}
</span>
}
</p>
</ListItemContent>
</ListItem>
<ListItem threeLine className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? styles.enabledSetting : styles.disabledSetting}`}>
<ListItemAction>
<Checkbox
onChange={updateInfoBoxEnable(updateSettings, settings.infoBoxEnable)}
checked={settings.infoBoxEnable} />
</ListItemAction>
<ListItemContent>
{lang.t('configure.include-comment-stream')}
<p>
{lang.t('configure.include-comment-stream-desc')}
</p>
</ListItemContent>
</ListItem>
<ListItem className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? null : styles.hidden}`} >
<ListItemContent>
<Textfield
onChange={updateInfoBoxContent(updateSettings)}
value={settings.infoBoxContent}
label={lang.t('configure.include-text')}
rows={3}/>
</ListItemContent>
</ListItem>
<ListItem className={styles.configSettingInfoBox}>
<ListItemContent>
{lang.t('configure.close-after')}
<br />
<Textfield
type='number'
pattern='[0-9]+'
style={{width: 50}}
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout)}
value={getTimeoutAmount(settings.closedTimeout)}
label={lang.t('configure.closed-comments-label')} />
<div className={styles.configTimeoutSelect}>
<SelectField
label="comments closed time window"
value={getTimeoutMeasure(settings.closedTimeout)}
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout, true)}>
<Option value={'hours'}>{lang.t('configure.hours')}</Option>
<Option value={'days'}>{lang.t('configure.days')}</Option>
<Option value={'weeks'}>{lang.t('configure.weeks')}</Option>
</SelectField>
</div>
</ListItemContent>
</ListItem>
<ListItem className={styles.configSettingInfoBox}>
<ListItemContent>
{lang.t('configure.closed-comments-desc')}
<Textfield
onChange={updateClosedMessage(updateSettings)}
value={settings.closedMessage}
label={lang.t('configure.closed-comments-label')}
rows={3}/>
</ListItemContent>
</ListItem>
</List>
</div>
);
};
export default CommentSettings;
@@ -112,12 +112,12 @@
letter-spacing: 0.03em;
}
#bannedWordlist {
#bannedWordlist, #suspectWordlist {
width: 100%;
padding: 10px;
}
.bannedWordHeader {
.wordlistHeader {
font-weight: bold;
font-size:18px;
margin-bottom:3px;
@@ -1,6 +1,11 @@
import React from 'react';
import {connect} from 'react-redux';
import {fetchSettings, updateSettings, saveSettingsToServer} from '../../actions/settings';
import {
fetchSettings,
updateSettings,
saveSettingsToServer,
updateWordlist,
} from '../../actions/settings';
import {
List,
ListItem,
@@ -14,6 +19,7 @@ import translations from '../../translations.json';
import EmbedLink from './EmbedLink';
import CommentSettings from './CommentSettings';
import Wordlist from './Wordlist';
import has from 'lodash/has';
class Configure extends React.Component {
constructor (props) {
@@ -21,7 +27,6 @@ class Configure extends React.Component {
this.state = {
activeSection: 'comments',
wordlist: [],
changed: false,
errors: {}
};
@@ -31,15 +36,6 @@ class Configure extends React.Component {
this.props.dispatch(fetchSettings());
}
componentWillUpdate = (newProps) => {
if ((!this.props.settings
|| !this.props.settings.wordlist)
&& newProps.settings.wordlist
&& newProps.settings.wordlist.length !== 0 ) {
this.setState({wordlist: newProps.settings.wordlist.join(', ')});
}
}
saveSettings = () => {
this.props.dispatch(saveSettingsToServer());
this.setState({changed: false});
@@ -49,15 +45,9 @@ class Configure extends React.Component {
this.setState({activeSection});
}
onChangeWordlist = (event) => {
event.preventDefault();
const newlist = event.target.value;
this.setState({wordlist: newlist.toLowerCase(), changed: true});
this.props.dispatch(updateSettings({
wordlist: newlist.toLowerCase()
.split(',')
.map((word) => word.trim())
}));
onChangeWordlist = (listName, list) => {
this.setState({changed: true});
this.props.dispatch(updateWordlist(listName, list));
}
onSettingUpdate = (setting) => {
@@ -74,46 +64,46 @@ class Configure extends React.Component {
});
}
getSection = (section) => {
getSection (section) {
const pageTitle = this.getPageTitle(section);
switch(section){
case 'comments':
return <CommentSettings
title={pageTitle}
fetchingSettings={this.props.fetchingSettings}
settings={this.props.settings}
updateSettings={this.onSettingUpdate}
errors={this.state.errors}
settingsError={this.onSettingError}/>;
case 'embed':
return <EmbedLink/>;
return <EmbedLink title={pageTitle} />;
case 'wordlist':
return <Wordlist
wordlist={this.state.wordlist}
onChangeWordlist={this.onChangeWordlist}/>;
return has(this, 'props.settings.wordlist')
? <Wordlist
bannedWords={this.props.settings.wordlist.banned}
suspectWords={this.props.settings.wordlist.suspect}
onChangeWordlist={this.onChangeWordlist} />
: <p>loading wordlists</p>;
}
}
getPageTitle = (section) => {
getPageTitle (section) {
switch(section) {
case 'comments':
return lang.t('configure.comment-settings');
case 'embed':
return lang.t('configure.embed-comment-stream');
case 'wordlist':
return lang.t('configure.wordlist');
default:
return '';
}
}
render () {
let pageTitle = this.getPageTitle(this.state.activeSection);
const section = this.getSection(this.state.activeSection);
const showSave = Object.keys(this.state.errors).reduce(
(bool, error) => this.state.errors[error] ? false : bool, this.state.changed);
if (this.props.fetchingSettings) {
pageTitle += ' - Loading...';
}
return (
<div className={styles.container}>
<div className={styles.leftColumn}>
@@ -151,7 +141,6 @@ class Configure extends React.Component {
</div>
<div className={styles.mainContent}>
<h1>{pageTitle}</h1>
{ this.props.saveFetchingError }
{ this.props.fetchSettingsError }
{ section }
@@ -31,16 +31,21 @@ class EmbedLink extends Component {
render () {
const embedText = `<div id='coralStreamEmbed'></div><script type='text/javascript' src='${window.location.protocol}//pym.nprapps.org/pym.v1.min.js'></script><script>var pymParent = new pym.Parent('coralStreamEmbed', '${window.location.protocol}//${window.location.host}/embed/stream', {title: 'Comments'});</script>`;
return <List>
<ListItem className={styles.configSettingEmbed}>
<p>{lang.t('configure.copy-and-paste')}</p>
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
<Button raised colored className={styles.copyButton} onClick={this.copyToClipBoard}>
{lang.t('embedlink.copy')}
</Button>
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
</ListItem>
</List>;
return (
<div>
<h3>{this.props.title}</h3>
<List>
<ListItem className={styles.configSettingEmbed}>
<p>{lang.t('configure.copy-and-paste')}</p>
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
<Button raised colored className={styles.copyButton} onClick={this.copyToClipBoard}>
{lang.t('embedlink.copy')}
</Button>
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
</ListItem>
</List>
</div>
);
}
}
@@ -1,21 +1,38 @@
import React from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import styles from './Configure.css';
import {
Card
} from 'react-mdl';
import TagsInput from 'react-tagsinput';
const Wordlist = ({wordlist, onChangeWordlist}) => <Card id={styles.bannedWordlist} shadow={2}>
<p className={styles.bannedWordHeader}>{lang.t('configure.banned-word-header')}</p>
<p className={styles.bannedWordText}>{lang.t('configure.banned-word-text')}</p>
<textarea
rows={5}
type='text'
className={styles.bannedWordInput}
onChange={onChangeWordlist}
value={wordlist}/>
</Card>;
import styles from './Configure.css';
import {Card} from 'react-mdl';
const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
<div>
<h3>{lang.t('configure.banned-words-title')}</h3>
<Card id={styles.bannedWordlist} shadow={2}>
<p className={styles.wordlistHeader}>{lang.t('configure.banned-word-header')}</p>
<p className={styles.wordlistDesc}>{lang.t('configure.banned-word-text')}</p>
<TagsInput
value={bannedWords}
inputProps={{placeholder: 'word or phrase'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeWordlist('banned', tags)} />
</Card>
<h3>{lang.t('configure.suspect-words-title')}</h3>
<Card id={styles.suspectWordlist} shadow={2}>
<p className={styles.wordlistHeader}>{lang.t('configure.suspect-word-header')}</p>
<p className={styles.wordlistDesc}>{lang.t('configure.suspect-word-text')}</p>
<TagsInput
value={suspectWords}
inputProps={{placeholder: 'word or phrase'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeWordlist('suspect', tags)} />
</Card>
</div>
);
export default Wordlist;
@@ -13,6 +13,7 @@ import {
fetchModerationQueueComments
} from 'actions/comments';
import {userStatusUpdate} from 'actions/users';
import {fetchSettings} from 'actions/settings';
import styles from './ModerationQueue.css';
import I18n from 'coral-framework/modules/i18n/i18n';
@@ -36,6 +37,7 @@ class ModerationQueue extends React.Component {
// Fetch comments and bind singleView key before render
componentWillMount () {
this.props.dispatch(fetchSettings());
this.props.dispatch(fetchModerationQueueComments());
key('s', () => this.setState({singleView: !this.state.singleView}));
key('shift+/', () => this.setState({modalOpen: true}));
@@ -86,7 +88,7 @@ class ModerationQueue extends React.Component {
// Render the tabbed lists moderation queues
render () {
const {comments, users} = this.props;
const {comments, users, settings} = this.props;
const {activeTab, singleView, modalOpen} = this.state;
const premodIds = comments.ids.filter(id => comments.byId[id].status === 'premod');
@@ -106,6 +108,7 @@ class ModerationQueue extends React.Component {
</div>
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='pending'>
<CommentList
suspectWords={settings.settings.wordlist.suspect}
isActive={activeTab === 'pending'}
singleView={singleView}
commentIds={premodIds}
@@ -113,7 +116,7 @@ class ModerationQueue extends React.Component {
users={users.byId}
onClickAction={(action, comment) => this.onCommentAction(action, comment)}
onClickShowBanDialog={(userId, userName, commentId) => this.showBanUserDialog(userId, userName, commentId)}
actions={['reject', 'approve', 'ban']}
modActions={['reject', 'approve', 'ban']}
loading={comments.loading} />
<BanUserDialog
open={comments.showBanUserDialog}
@@ -123,24 +126,26 @@ class ModerationQueue extends React.Component {
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
<CommentList
suspectWords={settings.settings.wordlist.suspect}
isActive={activeTab === 'rejected'}
singleView={singleView}
commentIds={rejectedIds}
comments={comments.byId}
users={users.byId}
onClickAction={(action, comment) => this.onCommentAction(action, comment)}
actions={['approve']}
modActions={['approve']}
loading={comments.loading} />
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='flagged'>
<CommentList
isActive={activeTab === 'rejected'}
suspectWords={settings.settings.wordlist.suspect}
singleView={singleView}
commentIds={flaggedIds}
comments={comments.byId}
users={users.byId}
onClickAction={(action, comment) => this.onCommentAction(action, comment)}
actions={['reject', 'approve']}
modActions={['reject', 'approve']}
loading={comments.loading} />
</div>
<ModerationKeysModal open={modalOpen}
@@ -152,6 +157,8 @@ class ModerationQueue extends React.Component {
}
const mapStateToProps = state => ({
actions: state.actions.toJS(),
settings: state.settings.toJS(),
comments: state.comments.toJS(),
users: state.users.toJS()
});
@@ -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, Set} from 'immutable';
import * as types from '../constants/actions';
const initialState = Map({
ids: Set()
});
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) => {
const ids = action.actions.map(action => action.item_id);
const map = action.actions.reduce((memo, action) => {
memo[action.item_id] = action;
return memo;
}, {});
const newIds = state.get('ids').concat(ids);
return state.merge(map).set('ids', newIds);
};
+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()));
};
+3 -2
View File
@@ -24,15 +24,16 @@ const initialState = Map({
// Handle the comment actions
export default (state = initialState, action) => {
switch (action.type) {
case actions.COMMENTS_MODERATION_QUEUE_FETCH: return state.set('loading', true);
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: return updateStatus(state, action);
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;
}
+4
View File
@@ -4,6 +4,8 @@ import settings from 'reducers/settings';
import community from 'reducers/community';
import users from 'reducers/users';
import auth from 'reducers/auth';
import actions from 'reducers/actions';
import assets from 'reducers/assets';
// Combine all reducers into a main one
export default combineReducers({
@@ -11,5 +13,7 @@ export default combineReducers({
comments,
community,
auth,
actions,
assets,
users
});
+14 -2
View File
@@ -1,8 +1,13 @@
import {Map} from 'immutable';
import {Map, List} from 'immutable';
import * as types from '../actions/settings';
const initialState = Map({
settings: Map(),
settings: Map({
wordlist: Map({
banned: List(),
suspect: List()
})
}),
saveSettingsError: null,
fetchSettingsError: null,
fetchingSettings: false
@@ -18,16 +23,23 @@ export default (state = initialState, 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;
}
};
// 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.wordlist);
};
const saveComplete = (state, action) => {
const s = state.set('fetchingSettings', false).set('saveSettingsError', null);
const settings = s.get('settings').merge(action.settings);
+44 -3
View File
@@ -49,13 +49,18 @@
"comment-settings": "Comment Settings",
"embed-comment-stream": "Embed Comment Stream",
"banned-word-header": "Write the bannned words list",
"banned-word-text": "Comments which contain these words or phrases, not separated by commas and not case sensitive, will be automatically removed from the comment stream.",
"wordlist": "Banned words list",
"suspect-word-header": "Write the suspect words list",
"banned-word-text": "Comments which contain these words or phrases (not case-sensitive) will be automatically removed from the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
"suspect-word-text": "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
"wordlist": "Banned & Suspect Words",
"banned-words-title": "Banned words list",
"suspect-words-title": "Suspect words list",
"save-changes": "Save Changes",
"copy-and-paste": "Copy and paste code below into your CMS to embed your comment box in your articles",
"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 +78,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": {
@@ -113,9 +134,13 @@
"include-text": "Incluir tu texto aqui.",
"comment-settings": "Configuración de Comentarios",
"embed-comment-stream": "Colocar Hilo de Comentarios",
"wordlist": "Lista de palabras no permitidas",
"wordlist": "Palabras Suspendidas y Suspechosas",
"banned-word-header": "Escribir las palabras no permitidas",
"suspect-word-header": "Write the suspect words list",
"banned-word-text": "Comentarios que contengan estas palabras o frases, no separadas por comas y en mayusculas o minusuculas, serán automaticamente separadas de los comentarios publicados.",
"suspect-word-text": "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
"banned-words-title": "Banned words list",
"suspect-words-title": "Suspect words list",
"save-changes": "Guardar Cambios",
"copy-and-paste": "Copiar y pegar el código de más abajo en tu CMS para colocar la caja de comentarios en tus articulos",
"moderate": "Moderar",
@@ -139,6 +164,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": ""
}
}
}
+6 -5
View File
@@ -114,7 +114,6 @@ hr {
.coral-plugin-commentbox-char-count {
color: #ccc;
text-align: right;
font-size: 12px;
}
.coral-plugin-commentbox-char-max {
@@ -230,7 +229,7 @@ hr {
.coral-plugin-flags-popup-header {
font-weight: bolder;
font-size: 16px;
font-size: 1.33rem;
margin-bottom: 10px;
}
@@ -240,7 +239,8 @@ hr {
.coral-plugin-flags-popup-radio-label {
margin:5px;
font-size: 14px;
font-weight: 700;
font-size: .9rem;
}
.coral-plugin-flags-popup-counter {
@@ -254,8 +254,9 @@ hr {
margin-top: 10px;
}
.coral-plugin-flags-other-text {
.coral-plugin-flags-reason-text {
margin-left: 20px;
margin-top: 5px;
width: 75%;
}
@@ -290,7 +291,7 @@ hr {
.close-comments-alert {
background-color: #d65344;
color: white;
font-size: 16px;
font-size: 1.33rem;
padding: 5px;
}
+3 -1
View File
@@ -208,7 +208,9 @@ export function postItem (item, type, id) {
*
* @params
* id - the id of the item on which the action is taking place
* action - the name of the action
* action - the action object.
* Must include an 'action_type' string.
* May optionally include a `metadata` object with arbitrary action data.
* user - the user performing the action
* host - the coral host
*
+1 -1
View File
@@ -18,7 +18,7 @@ const getPopupMenu = [
{val: 'other', text: lang.t('other')}
],
button: lang.t('continue'),
sets: 'detail'
sets: 'reason'
};
},
() => {
+22 -26
View File
@@ -10,10 +10,9 @@ class FlagButton extends Component {
state = {
showMenu: false,
showOther: false,
itemType: '',
detail: '',
otherText: '',
reason: '',
note: '',
step: 0,
posted: false
}
@@ -30,7 +29,7 @@ class FlagButton extends Component {
onPopupContinue = () => {
const {postAction, addItem, updateItem, flag, id, author_id} = this.props;
const {itemType, field, detail, step, otherText, posted} = this.state;
const {itemType, field, reason, step, note, posted} = this.state;
// Proceed to the next step or close the menu if we've reached the end
if (step + 1 >= this.props.getPopupMenu.length) {
@@ -39,11 +38,10 @@ class FlagButton extends Component {
this.setState({step: step + 1});
}
// If itemType and detail are both set, post the action
if (itemType && detail && !posted) {
// If itemType and reason are both set, post the action
if (itemType && reason && !posted) {
// Set the text from the "other" field if it exists.
const updatedDetail = otherText || detail;
let item_id;
switch(itemType) {
case 'comments':
@@ -55,8 +53,11 @@ class FlagButton extends Component {
}
const action = {
action_type: 'flag',
field,
detail: updatedDetail
metadata: {
field,
reason,
note
}
};
postAction(item_id, itemType, action)
.then((action) => {
@@ -70,11 +71,6 @@ class FlagButton extends Component {
onPopupOptionClick = (sets) => (e) => {
// If the "other" option is clicked, show the other textbox
if(sets === 'detail' && e.target.value === 'other') {
this.setState({showOther: true});
}
// If flagging a user, indicate that this is referencing the username rather than the bio
if(sets === 'itemType' && e.target.value === 'user') {
this.setState({field: 'username'});
@@ -92,8 +88,8 @@ class FlagButton extends Component {
this.setState({[sets]: e.target.value});
}
onOtherTextChange = (e) => {
this.setState({otherText: e.target.value});
onNoteTextChange = (e) => {
this.setState({note: e.target.value});
}
handleClickOutside () {
@@ -142,16 +138,16 @@ class FlagButton extends Component {
)
}
{
this.state.showOther && <div>
<input
className={`${name}-other-text`}
type="text"
id="otherText"
onChange={this.onOtherTextChange}
value={this.state.otherText}/>
<label htmlFor={'otherText'} className={`${name}-popup-radio-label screen-reader-text`}>
lang.t('flag-reason')
</label><br/>
this.state.reason && <div>
<label htmlFor={'note'} className={`${name}-popup-radio-label`}>
{lang.t('flag-reason')}
</label><br/>
<textarea
className={`${name}-reason-text`}
id="note"
rows={4}
onChange={this.onNoteTextChange}
value={this.state.note}/>
</div>
}
</form>
+3 -2
View File
@@ -22,12 +22,13 @@ const getPopupMenu = [
[
{val: 'I don\'t agree with this comment', text: lang.t('no-agree-comment')},
{val: 'This comment is offensive', text: lang.t('comment-offensive')},
{val: 'This comment reveals personally identifiable infomration', text: lang.t('personal-info')},
{val: 'This looks like an ad/marketing', text: lang.t('marketing')},
{val: 'other', text: lang.t('other')}
]
: [
{val: 'This username is offensive', text: lang.t('username-offensive')},
{val: 'I don\'t like this username', text: lang.t('no-like-username')},
{val: 'This user is impersonating', text: lang.t('user-impersonating')},
{val: 'This looks like an ad/marketing', text: lang.t('marketing')},
{val: 'other', text: lang.t('other')}
];
@@ -35,7 +36,7 @@ const getPopupMenu = [
header: lang.t('step-2-header'),
options,
button: lang.t('continue'),
sets: 'detail'
sets: 'reason'
};
},
() => {
+4 -2
View File
@@ -19,8 +19,9 @@
"bio-offensive": "This bio is offensive",
"no-like-bio": "I don't like this bio",
"marketing": "This looks like an ad/marketing",
"user-impersonating": "This user is impersonating",
"thank-you": "We value your safety and feedback. A moderator will review your flag.",
"flag-reason": "Reason for flag",
"flag-reason": "Reason for flag (Optional)",
"other": "Other"
},
"es": {
@@ -42,9 +43,10 @@
"no-like-username": "No me gusta ese nombre de usuario",
"bio-offensive": "Esta bio es ofensiva",
"no-like-bio": "No me gusta esta bio",
"user-impersonating": "Este usario suplanta a alguien",
"marketing": "Esto parece una publicidad/marketing",
"thank-you": "Nos interesa tu protección y comentarios. Un moderador va a mirar tu marca.",
"flag-reason": "Razón por la que marcar",
"flag-reason": "Razón por la que marcar (Opcional)",
"other": "Otro"
}
}
@@ -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;