Merge branch 'master' into provide-valid-optimistic-status

This commit is contained in:
Kim Gardner
2017-08-01 11:59:34 -04:00
committed by GitHub
15 changed files with 165 additions and 76 deletions
+12 -6
View File
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import {Router, Route, IndexRedirect, Redirect} from 'react-router'; import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
import {history} from 'coral-framework/helpers/router'; import {history} from 'coral-framework/helpers/router';
import Configure from 'routes/Configure'; import Configure from 'routes/Configure';
@@ -15,7 +15,7 @@ const routes = (
<div> <div>
<Route exact path="/admin/install" component={Install}/> <Route exact path="/admin/install" component={Install}/>
<Route path='/admin' component={Layout}> <Route path='/admin' component={Layout}>
<IndexRedirect to='/admin/moderate/all' /> <IndexRedirect to='/admin/moderate' />
<Route path='configure' component={Configure} /> <Route path='configure' component={Configure} />
<Route path='stories' component={Stories} /> <Route path='stories' component={Stories} />
<Route path='dashboard' component={Dashboard} /> <Route path='dashboard' component={Dashboard} />
@@ -35,10 +35,15 @@ const routes = (
{/* Moderation Routes */} {/* Moderation Routes */}
<Route path='moderate' component={ModerationLayout}> <Route path='moderate' component={ModerationLayout}>
<IndexRoute components={Moderation} />
<Route path='all' components={Moderation}> <Route path='all' components={Moderation}>
<Route path=':id' components={Moderation} /> <Route path=':id' components={Moderation} />
</Route> </Route>
<Route path='accepted' components={Moderation}> <Route path='new' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path='approved' components={Moderation}>
<Route path=':id' components={Moderation} /> <Route path=':id' components={Moderation} />
</Route> </Route>
<Route path='premod' components={Moderation}> <Route path='premod' components={Moderation}>
@@ -47,11 +52,12 @@ const routes = (
<Route path='rejected' components={Moderation}> <Route path='rejected' components={Moderation}>
<Route path=':id' components={Moderation} /> <Route path=':id' components={Moderation} />
</Route> </Route>
<Route path='flagged' components={Moderation}> <Route path='reported' components={Moderation}>
<Route path=':id' components={Moderation} /> <Route path=':id' components={Moderation} />
</Route> </Route>
<Redirect from=':id' to='all/:id' />
<IndexRedirect to='all' /> <Route path=':id' components={Moderation} />
</Route> </Route>
</Route> </Route>
</div> </div>
@@ -60,7 +60,7 @@ class UserDetailComment extends React.Component {
</div> </div>
<div className={styles.story}> <div className={styles.story}>
Story: {comment.asset.title} Story: {comment.asset.title}
{<Link to={`/admin/moderate/all/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>} {<Link to={`/admin/moderate/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
</div> </div>
<CommentAnimatedEdit body={comment.body}> <CommentAnimatedEdit body={comment.body}>
<div className={styles.bodyContainer}> <div className={styles.bodyContainer}>
+26 -13
View File
@@ -1,7 +1,7 @@
import update from 'immutability-helper'; import update from 'immutability-helper';
import * as notification from 'coral-admin/src/services/notification'; import * as notification from 'coral-admin/src/services/notification';
const queues = ['all', 'premod', 'flagged', 'accepted', 'rejected']; const queues = ['all', 'premod', 'reported', 'approved', 'rejected', 'new'];
const limit = 10; const limit = 10;
const ascending = (a, b) => { const ascending = (a, b) => {
@@ -64,23 +64,35 @@ function addCommentToQueue(root, queue, comment, sort) {
return update(root, changes); return update(root, changes);
} }
/**
* getCommentQueues determines in which queues a comment should be placed.
*/
function getCommentQueues(comment) { function getCommentQueues(comment) {
const queues = ['all']; const queues = ['all'];
if (comment.status === 'ACCEPTED') { const isFlagged = comment.actions && comment.actions.some((a) => a.__typename === 'FlagAction');
queues.push('accepted');
} switch(comment.status) {
else if (comment.status === 'REJECTED') { case 'ACCEPTED':
queues.push('approved');
break;
case 'REJECTED':
queues.push('rejected'); queues.push('rejected');
} break;
else if (comment.status === 'PREMOD') { case 'PREMOD':
queues.push('premod'); queues.push('premod');
queues.push('new');
if (isFlagged) {
queues.push('reported');
}
break;
case 'NONE':
queues.push('new');
if (isFlagged) {
queues.push('reported');
}
break;
} }
if (
['NONE', 'PREMOD'].indexOf(comment.status) >= 0
&& comment.actions && comment.actions.some((a) => a.__typename === 'FlagAction')
) {
queues.push('flagged');
}
return queues; return queues;
} }
@@ -98,6 +110,7 @@ function getCommentQueues(comment) {
*/ */
export function handleCommentChange(root, comment, sort, notify) { export function handleCommentChange(root, comment, sort, notify) {
let next = root; let next = root;
const nextQueues = getCommentQueues(comment); const nextQueues = getCommentQueues(comment);
let notificationShown = false; let notificationShown = false;
@@ -100,7 +100,7 @@ class Comment extends React.Component {
<div className={styles.moderateArticle}> <div className={styles.moderateArticle}>
Story: {comment.asset.title} Story: {comment.asset.title}
{!props.currentAsset && {!props.currentAsset &&
<Link to={`/admin/moderate/all/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>} <Link to={`/admin/moderate/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
</div> </div>
<CommentAnimatedEdit body={comment.body}> <CommentAnimatedEdit body={comment.body}>
<div className={styles.itemBody}> <div className={styles.itemBody}>
@@ -58,8 +58,7 @@ export default class Moderation extends Component {
} }
getComments = () => { getComments = () => {
const {root, route} = this.props; const {root, activeTab} = this.props;
const activeTab = route.path === ':id' ? 'premod' : route.path;
return root[activeTab].nodes; return root[activeTab].nodes;
} }
@@ -101,25 +100,28 @@ export default class Moderation extends Component {
} }
render () { render () {
const {root, moderation, settings, viewUserDetail, hideUserDetail, activeTab, getModPath, premodEnabled, ...props} = this.props;
const {root, moderation, settings, viewUserDetail, hideUserDetail, activeTab, ...props} = this.props;
const assetId = this.props.params.id; const assetId = this.props.params.id;
const {asset} = root; const {asset} = root;
const comments = root[activeTab]; const comments = root[activeTab];
let activeTabCount; let activeTabCount;
switch(activeTab) { switch(activeTab) {
case 'all': case 'all':
activeTabCount = root.allCount; activeTabCount = root.allCount;
break; break;
case 'accepted': case 'new':
activeTabCount = root.acceptedCount; activeTabCount = root.newCount;
break;
case 'approved':
activeTabCount = root.approvedCount;
break; break;
case 'premod': case 'premod':
activeTabCount = root.premodCount; activeTabCount = root.premodCount;
break; break;
case 'flagged': case 'reported':
activeTabCount = root.flaggedCount; activeTabCount = root.reportedCount;
break; break;
case 'rejected': case 'rejected':
activeTabCount = root.rejectedCount; activeTabCount = root.rejectedCount;
@@ -137,12 +139,16 @@ export default class Moderation extends Component {
<ModerationMenu <ModerationMenu
asset={asset} asset={asset}
allCount={root.allCount} allCount={root.allCount}
acceptedCount={root.acceptedCount} newCount={root.newCount}
getModPath={getModPath}
approvedCount={root.approvedCount}
premodCount={root.premodCount} premodCount={root.premodCount}
rejectedCount={root.rejectedCount} rejectedCount={root.rejectedCount}
flaggedCount={root.flaggedCount} reportedCount={root.reportedCount}
selectSort={this.props.setSortOrder} selectSort={this.props.setSortOrder}
sort={this.props.moderation.sortOrder} sort={this.props.moderation.sortOrder}
premodEnabled={premodEnabled}
activeTab={activeTab}
/> />
<ModerationQueue <ModerationQueue
data={this.props.data} data={this.props.data}
@@ -4,52 +4,72 @@ import styles from './styles.css';
import {SelectField, Option} from 'react-mdl-selectfield'; import {SelectField, Option} from 'react-mdl-selectfield';
import {Icon} from 'coral-ui'; import {Icon} from 'coral-ui';
import {Link} from 'react-router'; import {Link} from 'react-router';
import cn from 'classnames';
import t from 'coral-framework/services/i18n'; import t from 'coral-framework/services/i18n';
const ModerationMenu = ( const ModerationMenu = ({
{asset, allCount, acceptedCount, premodCount, rejectedCount, flaggedCount, selectSort, sort} asset = {},
) => { allCount,
approvedCount,
function getPath (type) { premodCount,
return asset ? `/admin/moderate/${type}/${asset.id}` : `/admin/moderate/${type}`; newCount,
} rejectedCount,
reportedCount,
selectSort,
sort,
premodEnabled,
getModPath,
activeTab
}) => {
return ( return (
<div className="mdl-tabs"> <div className="mdl-tabs">
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}> <div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<div className={styles.tabBarPadding} /> <div className={styles.tabBarPadding} />
<div> <div>
{
premodEnabled ? (
<Link
to={getModPath('premod', asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'premod'})}
activeClassName={styles.active}>
<Icon name='access_time' className={styles.tabIcon} /> {t('modqueue.premod')} <CommentCount count={premodCount} />
</Link>
) : (
<Link
to={getModPath('new', asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'new'})}
activeClassName={styles.active}>
<Icon name='question_answer' className={styles.tabIcon} /> {t('modqueue.new')} <CommentCount count={newCount} />
</Link>
)
}
<Link <Link
to={getPath('all')} to={getModPath('reported', asset.id)}
className={`mdl-tabs__tab ${styles.tab}`} className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'reported'})}
activeClassName={styles.active}> activeClassName={styles.active}>
<Icon name='question_answer' className={styles.tabIcon} /> {t('modqueue.all')} <CommentCount count={allCount} /> <Icon name='flag' className={styles.tabIcon} /> {t('modqueue.reported')} <CommentCount count={reportedCount} />
</Link> </Link>
<Link <Link
to={getPath('premod')} to={getModPath('approved', asset.id)}
className={`mdl-tabs__tab ${styles.tab}`} className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'approved'})}
activeClassName={styles.active}> activeClassName={styles.active}>
<Icon name='access_time' className={styles.tabIcon} /> {t('modqueue.premod')} <CommentCount count={premodCount} /> <Icon name='check' className={styles.tabIcon} /> {t('modqueue.approved')} <CommentCount count={approvedCount} />
</Link> </Link>
<Link <Link
to={getPath('flagged')} to={getModPath('rejected', asset.id)}
className={`mdl-tabs__tab ${styles.tab}`} className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'rejected'})}
activeClassName={styles.active}>
<Icon name='flag' className={styles.tabIcon} /> {t('modqueue.flagged')} <CommentCount count={flaggedCount} />
</Link>
<Link
to={getPath('accepted')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
<Icon name='check' className={styles.tabIcon} /> {t('modqueue.approved')} <CommentCount count={acceptedCount} />
</Link>
<Link
to={getPath('rejected')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}> activeClassName={styles.active}>
<Icon name='close' className={styles.tabIcon} /> {t('modqueue.rejected')} <CommentCount count={rejectedCount} /> <Icon name='close' className={styles.tabIcon} /> {t('modqueue.rejected')} <CommentCount count={rejectedCount} />
</Link> </Link>
<Link
to={getModPath('all', asset.id)}
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === 'all'})}
activeClassName={styles.active}>
<Icon name='question_answer' className={styles.tabIcon} /> {t('modqueue.all')} <CommentCount count={allCount} />
</Link>
</div> </div>
<SelectField <SelectField
className={styles.selectField} className={styles.selectField}
@@ -68,7 +88,7 @@ ModerationMenu.propTypes = {
allCount: PropTypes.number.isRequired, allCount: PropTypes.number.isRequired,
premodCount: PropTypes.number.isRequired, premodCount: PropTypes.number.isRequired,
rejectedCount: PropTypes.number.isRequired, rejectedCount: PropTypes.number.isRequired,
flaggedCount: PropTypes.number.isRequired, reportedCount: PropTypes.number.isRequired,
asset: PropTypes.shape({ asset: PropTypes.shape({
id: PropTypes.string id: PropTypes.string
}) })
@@ -64,6 +64,7 @@ const StorySearch = (props) => {
? <Spinner /> ? <Spinner />
: assets.map((story, i) => { : assets.map((story, i) => {
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date(); const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
return <Story return <Story
key={i} key={i}
id={story.id} id={story.id}
@@ -8,6 +8,7 @@ import t from 'coral-framework/services/i18n';
import update from 'immutability-helper'; import update from 'immutability-helper';
import truncate from 'lodash/truncate'; import truncate from 'lodash/truncate';
import NotFoundAsset from '../components/NotFoundAsset'; import NotFoundAsset from '../components/NotFoundAsset';
import {isPremod, getModPath} from '../../../utils';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations'; import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {handleCommentChange} from '../../../graphql/utils'; import {handleCommentChange} from '../../../graphql/utils';
@@ -37,7 +38,18 @@ function prepareNotificationText(text) {
class ModerationContainer extends Component { class ModerationContainer extends Component {
subscriptions = []; subscriptions = [];
get activeTab() { return this.props.route.path; } get activeTab() {
const {root: {asset, settings}, router, route} = this.props;
// Grab premod from asset or from settings
const premod = !router.params.id ? settings.moderation : asset.settings.moderation;
const queue = isPremod(premod) ? 'premod' : 'new';
const activeTab = route.path && route.path !== ':id' ? route.path : queue;
return activeTab;
}
subscribeToUpdates(variables = this.props.data.variables) { subscribeToUpdates(variables = this.props.data.variables) {
const sub1 = this.props.data.subscribeToMore({ const sub1 = this.props.data.subscribeToMore({
@@ -153,13 +165,16 @@ class ModerationContainer extends Component {
case 'all': case 'all':
variables.statuses = null; variables.statuses = null;
break; break;
case 'accepted': case 'new':
variables.statuses = ['NONE', 'PREMOD'];
break;
case 'approved':
variables.statuses = ['ACCEPTED']; variables.statuses = ['ACCEPTED'];
break; break;
case 'premod': case 'premod':
variables.statuses = ['PREMOD']; variables.statuses = ['PREMOD'];
break; break;
case 'flagged': case 'reported':
variables.statuses = ['NONE', 'PREMOD']; variables.statuses = ['NONE', 'PREMOD'];
variables.action_type = 'FLAG'; variables.action_type = 'FLAG';
break; break;
@@ -184,7 +199,7 @@ class ModerationContainer extends Component {
}; };
render () { render () {
const {root, root: {asset}, data, params: {id: assetId}} = this.props; const {root, root: {asset, settings}, data, params: {id: assetId}} = this.props;
if (data.error) { if (data.error) {
return <div>Error</div>; return <div>Error</div>;
@@ -209,10 +224,12 @@ class ModerationContainer extends Component {
return <Moderation return <Moderation
{...this.props} {...this.props}
getModPath={getModPath}
loadMore={this.loadMore} loadMore={this.loadMore}
acceptComment={this.acceptComment} acceptComment={this.acceptComment}
rejectComment={this.rejectComment} rejectComment={this.rejectComment}
activeTab={this.activeTab} activeTab={this.activeTab}
premodEnabled={assetId ? isPremod(asset.settings.moderation) : isPremod(settings.moderation)}
/>; />;
} }
} }
@@ -304,7 +321,14 @@ const withModQueueQuery = withQuery(gql`
}) { }) {
...CoralAdmin_Moderation_CommentConnection ...CoralAdmin_Moderation_CommentConnection
} }
accepted: comments(query: { new: comments(query: {
statuses: [NONE, PREMOD],
asset_id: $asset_id,
sort: $sort
}) {
...CoralAdmin_Moderation_CommentConnection
}
approved: comments(query: {
statuses: [ACCEPTED], statuses: [ACCEPTED],
asset_id: $asset_id, asset_id: $asset_id,
sort: $sort sort: $sort
@@ -318,7 +342,7 @@ const withModQueueQuery = withQuery(gql`
}) { }) {
...CoralAdmin_Moderation_CommentConnection ...CoralAdmin_Moderation_CommentConnection
} }
flagged: comments(query: { reported: comments(query: {
action_type: FLAG, action_type: FLAG,
asset_id: $asset_id, asset_id: $asset_id,
statuses: [NONE, PREMOD], statuses: [NONE, PREMOD],
@@ -337,11 +361,18 @@ const withModQueueQuery = withQuery(gql`
id id
title title
url url
settings {
moderation
}
} }
allCount: commentCount(query: { allCount: commentCount(query: {
asset_id: $asset_id asset_id: $asset_id
}) })
acceptedCount: commentCount(query: { newCount: commentCount(query: {
statuses: [NONE, PREMOD],
asset_id: $asset_id
})
approvedCount: commentCount(query: {
statuses: [ACCEPTED], statuses: [ACCEPTED],
asset_id: $asset_id asset_id: $asset_id
}) })
@@ -353,13 +384,14 @@ const withModQueueQuery = withQuery(gql`
statuses: [REJECTED], statuses: [REJECTED],
asset_id: $asset_id asset_id: $asset_id
}) })
flaggedCount: commentCount(query: { reportedCount: commentCount(query: {
action_type: FLAG, action_type: FLAG,
asset_id: $asset_id, asset_id: $asset_id,
statuses: [NONE, PREMOD] statuses: [NONE, PREMOD]
}) })
settings { settings {
organizationName organizationName
moderation
} }
} }
${commentConnectionFragment} ${commentConnectionFragment}
@@ -380,7 +412,11 @@ const withQueueCountPolling = withQuery(gql`
allCount: commentCount(query: { allCount: commentCount(query: {
asset_id: $asset_id asset_id: $asset_id
}) })
acceptedCount: commentCount(query: { newCount: commentCount(query: {
statuses: [NONE, PREMOD],
asset_id: $asset_id
})
approvedCount: commentCount(query: {
statuses: [ACCEPTED], statuses: [ACCEPTED],
asset_id: $asset_id asset_id: $asset_id
}) })
@@ -392,7 +428,7 @@ const withQueueCountPolling = withQuery(gql`
statuses: [REJECTED], statuses: [REJECTED],
asset_id: $asset_id asset_id: $asset_id
}) })
flaggedCount: commentCount(query: { reportedCount: commentCount(query: {
action_type: FLAG, action_type: FLAG,
asset_id: $asset_id, asset_id: $asset_id,
statuses: [NONE, PREMOD] statuses: [NONE, PREMOD]
@@ -59,13 +59,13 @@ class StorySearchContainer extends React.Component {
goToStory = (id) => { goToStory = (id) => {
const {router} = this.props; const {router} = this.props;
router.push(`/admin/moderate/all/${id}`); router.push(`/admin/moderate/${id}`);
this.clearAndCloseSearch(); this.clearAndCloseSearch();
} }
goToModerateAll = () => { goToModerateAll = () => {
const {router} = this.props; const {router} = this.props;
router.push('/admin/moderate/all'); router.push('/admin/moderate');
this.clearAndCloseSearch(); this.clearAndCloseSearch();
} }
+4
View File
@@ -0,0 +1,4 @@
export const isPremod = (mod) => mod === 'PRE';
export const getModPath = (type = 'all', assetId) =>
assetId ? `/admin/moderate/${type}/${assetId}` : `/admin/moderate/${type}`;
+2 -2
View File
@@ -42,14 +42,14 @@ export default class Popup extends Component {
this.onUnload(); this.onUnload();
const interval = setInterval(() => { const interval = setInterval(() => {
if (this.ref.onload === null) { if (this.ref && this.ref.onload === null) {
this.setCallbacks(); this.setCallbacks();
clearInterval(interval); clearInterval(interval);
} }
}, 50); }, 50);
this.detectCloseInterval = setInterval(() => { this.detectCloseInterval = setInterval(() => {
if (this.ref.closed) { if (!this.ref || this.ref.closed) {
clearInterval(this.detectCloseInterval); clearInterval(this.detectCloseInterval);
this.onClose(); this.onClose();
} }
@@ -5,7 +5,7 @@ import t from 'coral-framework/services/i18n';
const ModerationLink = (props) => props.isAdmin ? ( const ModerationLink = (props) => props.isAdmin ? (
<div className={styles.moderationLink}> <div className={styles.moderationLink}>
<a href={`/admin/moderate/all/${props.assetId}`} target="_blank"> <a href={`/admin/moderate/${props.assetId}`} target="_blank">
{t('moderate_this_stream')} {t('moderate_this_stream')}
</a> </a>
</div> </div>
+2
View File
@@ -262,6 +262,7 @@ en:
dont_like_username: "Don't like username" dont_like_username: "Don't like username"
empty_queue: "No more comments to moderate! You're all caught up. Go have some ☕️" empty_queue: "No more comments to moderate! You're all caught up. Go have some ☕️"
flagged: flagged flagged: flagged
reported: reported
impersonating: Impersonating impersonating: Impersonating
less_detail: "Less detail" less_detail: "Less detail"
likes: likes likes: likes
@@ -269,6 +270,7 @@ en:
mod_faster: "Moderate faster with keyboard shortcuts" mod_faster: "Moderate faster with keyboard shortcuts"
moderate: "Moderate →" moderate: "Moderate →"
more_detail: "More detail" more_detail: "More detail"
new: New
newest_first: "Newest First" newest_first: "Newest First"
navigation: Navigation navigation: Navigation
next_comment: "Go to the next comment" next_comment: "Go to the next comment"
+1
View File
@@ -261,6 +261,7 @@ es:
mod_faster: "Moderar más rápido con atajos de teclado" mod_faster: "Moderar más rápido con atajos de teclado"
moderate: "Moderar →" moderate: "Moderar →"
more_detail: "Más detalles" more_detail: "Más detalles"
new: Nuevo
newest_first: "Primero el más nuevo" newest_first: "Primero el más nuevo"
navigation: Navegación navigation: Navegación
next_comment: "Ir al siguiente comentario" next_comment: "Ir al siguiente comentario"
@@ -15,7 +15,7 @@ export default class Tag extends React.Component {
} }
showTooltip = e => { showTooltip = (e) => {
e.preventDefault(); e.preventDefault();
this.setState({ this.setState({
tooltip: true tooltip: true