+const ModerationMenu = ({asset, premodCount, rejectedCount, flaggedCount, selectSort, sort}) => {
+ const premodPath = asset ? `/admin/moderate/premod/${asset.id}` : '/admin/moderate/premod';
+ const rejectPath = asset ? `/admin/moderate/rejected/${asset.id}` : '/admin/moderate/rejected';
+ const flagPath = asset ? `/admin/moderate/flagged/${asset.id}` : '/admin/moderate/flagged';
+ return (
+
+
+
+
+
+ {lang.t('modqueue.premod')}
+
+
+ {lang.t('modqueue.rejected')}
+
+
+ {lang.t('modqueue.flagged')}
+
+
selectSort(sort)}>
+
+
+
- );
- }
-}
+
+ );
+};
+
+ModerationMenu.propTypes = {
+ premodCount: PropTypes.number.isRequired,
+ rejectedCount: PropTypes.number.isRequired,
+ flaggedCount: PropTypes.number.isRequired,
+ asset: PropTypes.shape({
+ id: PropTypes.string
+ })
+};
export default ModerationMenu;
diff --git a/client/coral-admin/src/containers/ModerationQueue/components/styles.css b/client/coral-admin/src/containers/ModerationQueue/components/styles.css
index 627cde5e4..9576045de 100644
--- a/client/coral-admin/src/containers/ModerationQueue/components/styles.css
+++ b/client/coral-admin/src/containers/ModerationQueue/components/styles.css
@@ -394,3 +394,23 @@ span {
cursor: pointer;
}
}
+
+.loadMoreContainer {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+};
+
+.loadMore {
+ width: 100%;
+ text-align: center;
+ color: #FFF;
+ max-width: 660px;
+ margin-bottom: 30px;
+ background-color: #2376D8;
+ cursor: pointer;
+}
+
+.loadMore:hover {
+ background-color: #4399FF;
+}
diff --git a/client/coral-admin/src/containers/Streams/Streams.css b/client/coral-admin/src/containers/Stories/Stories.css
similarity index 100%
rename from client/coral-admin/src/containers/Streams/Streams.css
rename to client/coral-admin/src/containers/Stories/Stories.css
diff --git a/client/coral-admin/src/containers/Streams/Streams.js b/client/coral-admin/src/containers/Stories/Stories.js
similarity index 97%
rename from client/coral-admin/src/containers/Streams/Streams.js
rename to client/coral-admin/src/containers/Stories/Stories.js
index 96130fcba..4d2ad086a 100644
--- a/client/coral-admin/src/containers/Streams/Streams.js
+++ b/client/coral-admin/src/containers/Stories/Stories.js
@@ -1,5 +1,5 @@
import React, {Component} from 'react';
-import styles from './Streams.css';
+import styles from './Stories.css';
import {connect} from 'react-redux';
import I18n from 'coral-framework/modules/i18n/i18n';
import {fetchAssets, updateAssetState} from '../../actions/assets';
@@ -12,7 +12,7 @@ import EmptyCard from 'coral-admin/src/components/EmptyCard';
const limit = 25;
-class Streams extends Component {
+class Stories extends Component {
state = {
search: '',
@@ -80,8 +80,8 @@ class Streams extends Component {
- {closed ? lang.t('streams.closed') : lang.t('streams.open')}
{!statusMenuOpen && }
+ {closed ? lang.t('streams.closed') : lang.t('streams.open')}
{
statusMenuOpen &&
@@ -182,6 +182,6 @@ const mapDispatchToProps = (dispatch) => {
};
};
-export default connect(mapStateToProps, mapDispatchToProps)(Streams);
+export default connect(mapStateToProps, mapDispatchToProps)(Stories);
const lang = new I18n(translations);
diff --git a/client/coral-admin/src/containers/Streams/Stories.js b/client/coral-admin/src/containers/Streams/Stories.js
new file mode 100644
index 000000000..4d2ad086a
--- /dev/null
+++ b/client/coral-admin/src/containers/Streams/Stories.js
@@ -0,0 +1,187 @@
+import React, {Component} from 'react';
+import styles from './Stories.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 {Link} from 'react-router';
+
+import {Pager, Icon} from 'coral-ui';
+import {DataTable, TableHeader, RadioGroup, Radio} from 'react-mdl';
+import EmptyCard from 'coral-admin/src/components/EmptyCard';
+
+const limit = 25;
+
+class Stories 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) => {
+ const search = e.target.value;
+ this.setState((prevState) => {
+ prevState.search = search;
+ clearTimeout(prevState.timer);
+ const fetchAssets = this.props.fetchAssets;
+ prevState.timer = setTimeout(() => {
+ fetchAssets(0, limit, search, 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;
+ });
+ }
+ }
+
+ renderTitle = (title, {id}) =>
{title}
+
+ renderStatus = (closedAt, {id}) => {
+ const closed = closedAt && new Date(closedAt).getTime() < Date.now();
+ const statusMenuOpen = this.state.statusMenus[id];
+ return
+
+ {!statusMenuOpen && }
+ {closed ? lang.t('streams.closed') : lang.t('streams.open')}
+
+ {
+ statusMenuOpen &&
+
+ {!closed ? lang.t('streams.closed') : lang.t('streams.open')}
+
+ }
+
;
+ }
+
+ 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;
+
+ const assetsIds = assets.ids.map((id) => assets.byId[id]);
+
+ return (
+
+
+
+
+
+
+
{lang.t('streams.filter-streams')}
+
{lang.t('streams.stream-status')}
+
+ {lang.t('streams.all')}
+ {lang.t('streams.open')}
+ {lang.t('streams.closed')}
+
+
{lang.t('streams.sort-by')}
+
+ {lang.t('streams.newest')}
+ {lang.t('streams.oldest')}
+
+
+ {
+ assetsIds.length
+ ?
+
+ {lang.t('streams.article')}
+
+ {lang.t('streams.pubdate')}
+
+
+ {lang.t('streams.status')}
+
+
+
+
+ :
{lang.t('streams.empty_result')}
+ }
+
+ );
+ }
+}
+
+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)(Stories);
+
+const lang = new I18n(translations);
diff --git a/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql b/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql
index 37335aeaa..c77fbc32b 100644
--- a/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql
+++ b/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql
@@ -4,6 +4,7 @@ fragment metrics on Asset {
url
author
created_at
+ commentCount
action_summaries {
type: __typename
actionCount
diff --git a/client/coral-admin/src/graphql/mutations/index.js b/client/coral-admin/src/graphql/mutations/index.js
index d74f4dea8..4a574cc93 100644
--- a/client/coral-admin/src/graphql/mutations/index.js
+++ b/client/coral-admin/src/graphql/mutations/index.js
@@ -51,7 +51,26 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
commentId,
status: 'ACCEPTED'
},
- refetchQueries: ['ModQueue']
+ updateQueries: {
+ ModQueue: (oldData) => {
+ const premod = oldData.premod.filter(c => c.id !== commentId);
+ const flagged = oldData.flagged.filter(c => c.id !== commentId);
+ const rejected = oldData.rejected.filter(c => c.id !== commentId);
+ const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
+ const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
+ const rejectedCount = rejected.length < oldData.rejected.length ? oldData.rejectedCount - 1 : oldData.rejectedCount;
+
+ return {
+ ...oldData,
+ premodCount,
+ flaggedCount,
+ rejectedCount,
+ premod,
+ flagged,
+ rejected,
+ };
+ }
+ }
});
},
rejectComment: ({commentId}) => {
@@ -60,7 +79,27 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
commentId,
status: 'REJECTED'
},
- refetchQueries: ['ModQueue']
+ updateQueries: {
+ ModQueue: (oldData) => {
+ const comment = oldData.premod.concat(oldData.flagged).filter(c => c.id === commentId)[0];
+ const rejected = [comment].concat(oldData.rejected);
+ const premod = oldData.premod.filter(c => c.id !== commentId);
+ const flagged = oldData.flagged.filter(c => c.id !== commentId);
+ const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
+ const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
+ const rejectedCount = oldData.rejectedCount + 1;
+
+ return {
+ ...oldData,
+ premodCount,
+ flaggedCount,
+ rejectedCount,
+ premod,
+ flagged,
+ rejected
+ };
+ }
+ }
});
}
})
diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js
index e911402b0..291ea6bc5 100644
--- a/client/coral-admin/src/graphql/queries/index.js
+++ b/client/coral-admin/src/graphql/queries/index.js
@@ -1,6 +1,7 @@
import {graphql} from 'react-apollo';
import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
+import MOD_QUEUE_LOAD_MORE from './loadMore.graphql';
import MOD_USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql';
import METRICS from './metricsQuery.graphql';
@@ -15,7 +16,8 @@ export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
},
props: ({ownProps: {params: {id = null}}, data}) => ({
data,
- modQueueResort: modQueueResort(id, data.fetchMore)
+ modQueueResort: modQueueResort(id, data.fetchMore),
+ loadMore: loadMore(data.fetchMore)
})
});
@@ -31,6 +33,40 @@ export const getMetrics = graphql(METRICS, {
}
});
+export const loadMore = (fetchMore) => ({limit, cursor, sort, tab, asset_id}) => {
+ let statuses;
+ switch(tab) {
+ case 'premod':
+ statuses = ['PREMOD'];
+ break;
+ case 'flagged':
+ statuses = ['NONE', 'PREMOD'];
+ break;
+ case 'rejected':
+ statuses = ['REJECTED'];
+ break;
+ }
+ return fetchMore({
+ query: MOD_QUEUE_LOAD_MORE,
+ variables: {
+ limit,
+ cursor,
+ sort,
+ statuses,
+ asset_id
+ },
+ updateQuery: (oldData, {fetchMoreResult:{data:{comments}}}) => {
+ return {
+ ...oldData,
+ [tab]: [
+ ...oldData[tab],
+ ...comments
+ ]
+ };
+ }
+ });
+};
+
export const modUserFlaggedQuery = graphql(MOD_USER_FLAGGED_QUERY);
export const modQueueResort = (id, fetchMore) => (sort) => {
diff --git a/client/coral-admin/src/graphql/queries/loadMore.graphql b/client/coral-admin/src/graphql/queries/loadMore.graphql
new file mode 100644
index 000000000..56966a804
--- /dev/null
+++ b/client/coral-admin/src/graphql/queries/loadMore.graphql
@@ -0,0 +1,13 @@
+#import "../fragments/commentView.graphql"
+
+query LoadMoreModQueue($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!]) {
+ comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort}) {
+ ...commentView
+ action_summaries {
+ count
+ ... on FlagActionSummary {
+ reason
+ }
+ }
+ }
+}
diff --git a/client/coral-admin/src/graphql/queries/metricsQuery.graphql b/client/coral-admin/src/graphql/queries/metricsQuery.graphql
index 42a9fb70e..f0dff8965 100644
--- a/client/coral-admin/src/graphql/queries/metricsQuery.graphql
+++ b/client/coral-admin/src/graphql/queries/metricsQuery.graphql
@@ -7,4 +7,7 @@ query Metrics ($from: Date!, $to: Date!) {
assetsByLike: assetMetrics(from: $from, to: $to, sort: LIKE) {
...metrics
}
+ assetsByActivity: assetMetrics(from: $from, to: $to, sort: ACTIVITY) {
+ ...metrics
+ }
}
diff --git a/client/coral-admin/src/reducers/auth.js b/client/coral-admin/src/reducers/auth.js
index b52efdb85..a7054ddfa 100644
--- a/client/coral-admin/src/reducers/auth.js
+++ b/client/coral-admin/src/reducers/auth.js
@@ -6,6 +6,7 @@ const initialState = Map({
user: null,
isAdmin: false,
loginError: null,
+ loginMaxExceeded: false,
passwordRequestSuccess: null
});
@@ -29,12 +30,18 @@ export default function auth (state = initialState, action) {
return initialState;
case actions.LOGIN_REQUEST:
return state.set('loginError', null);
+ case actions.LOGIN_SUCCESS:
+ return state.set('loginMaxExceeded', false).set('loginError', null);
case actions.LOGIN_FAILURE:
return state.set('loginError', action.message);
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
return state.set('passwordRequestSuccess', null);
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
return state.set('passwordRequestSuccess', 'If you have a registered account, a password reset link was sent to that email.');
+ case actions.LOGIN_MAXIMUM_EXCEEDED:
+ return state
+ .set('loginMaxExceeded', true)
+ .set('loginError', action.message);
default :
return state;
}
diff --git a/client/coral-admin/src/reducers/config.js b/client/coral-admin/src/reducers/config.js
new file mode 100644
index 000000000..a5d11d6eb
--- /dev/null
+++ b/client/coral-admin/src/reducers/config.js
@@ -0,0 +1,16 @@
+import {Map} from 'immutable';
+
+import * as actions from '../actions/config';
+
+const initialState = Map({
+ data: Map({})
+});
+
+export default function config (state = initialState, action) {
+ switch (action.type) {
+ case actions.CONFIG_UPDATED:
+ return state.set('data', Map(action.data));
+ default:
+ return state;
+ }
+}
diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js
index 837ad507d..3036d6271 100644
--- a/client/coral-admin/src/reducers/index.js
+++ b/client/coral-admin/src/reducers/index.js
@@ -4,6 +4,7 @@ import settings from './settings';
import community from './community';
import moderation from './moderation';
import install from './install';
+import config from './config';
export default {
auth,
@@ -11,5 +12,6 @@ export default {
settings,
community,
moderation,
- install
+ install,
+ config
};
diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js
index 09a0b39e7..cf270543c 100644
--- a/client/coral-admin/src/reducers/moderation.js
+++ b/client/coral-admin/src/reducers/moderation.js
@@ -19,6 +19,7 @@ export default function moderation (state = initialState, action) {
.merge({
user: Map(action.user),
commentId: action.commentId,
+ showRejectedNote: action.showRejectedNote,
banDialog: true
});
case actions.SET_ACTIVE_TAB:
diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json
index 321ace66b..a5b50b21c 100644
--- a/client/coral-admin/src/translations.json
+++ b/client/coral-admin/src/translations.json
@@ -1,7 +1,8 @@
{
"en": {
"errors": {
- "NOT_AUTHORIZED": "Your username or password is not recognized by our system."
+ "NOT_AUTHORIZED": "Your username or password is not recognized by our system.",
+ "LOGIN_MAXIMUM_EXCEEDED": "You have made too many unsuccessful password attempts. Please wait."
},
"community": {
"username_and_email": "Username and Email",
@@ -94,7 +95,7 @@
"moderate": "Moderate",
"configure": "Configure",
"community": "Community",
- "streams": "Streams",
+ "stories": "Stories",
"closed-comments-desc": "Write a message to be displayed when when your comment stream is closed and no longer accepting comments.",
"closed-comments-label": "Write a message...",
"hours": "Hours",
@@ -131,9 +132,12 @@
"write_message": "Write a message"
},
"dashboard": {
+ "next-update": "{0} minutes until next update.",
+ "auto-update": "Data automatically updates every five minutes or when you Reload.",
"no_flags": "There have been no flags in the last 5 minutes! Hooray!",
"no_likes": "There have been no likes in the last 5 minutes. All quiet.",
"flags": "Flags",
+ "no_activity": "There haven't been any comments anywhere in the last five minutes.",
"comment_count": "comments"
},
"streams": {
@@ -149,14 +153,15 @@
"sort-by": "Sort By",
"open": "Open",
"closed": "Closed",
- "article": "Article",
+ "article": "Story",
"pubdate": "Publication Date",
- "status": "Status"
+ "status": "Stream Status"
}
},
"es": {
"errors": {
- "NOT_AUTHORIZED": "Acción no autorizada."
+ "NOT_AUTHORIZED": "Acción no autorizada.",
+ "LOGIN_MAXIMUM_EXCEEDED": "Ha realizado demasiados intentos fallidos de contraseña. Por favor espera."
},
"community": {
"username_and_email": "Usuario y E-mail",
@@ -250,7 +255,7 @@
"moderate": "Moderar",
"configure": "Configurar",
"community": "Comunidad",
- "streams": "Streams",
+ "stories": "Artículos",
"closed-comments-desc": "Escribe un mensaje que será mostrado cuando los comentarios estén cerrados y no se acepten más comentarios.",
"closed-comments-label": "Escribe un mensaje...",
"never": "Nunca",
@@ -276,9 +281,12 @@
"yes_ban_user": "Si, Suspendan el usuario"
},
"dashbord": {
+ "next-update": "{0} minutos hasta la siguiente actualización.",
+ "auto-update": "Los datos se actualizan automaticamente cada 5 minutos o cuando Recargas.",
"no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!",
"no_likes": "A nadie le ha gustado algún comentario en los últimos 5 minutos. Todo tranquilo.",
"flags": "Marcados",
+ "no_activity": "No hubo comentarios en los ultimos 5 minutos",
"comment_count": "comentarios"
},
"streams": {
@@ -294,7 +302,7 @@
"sort-by": "",
"open": "",
"closed": "",
- "article": "",
+ "article": "artículo",
"pubdate": "",
"status": ""
}
diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js
index 0a3cdb78b..06352ec2f 100644
--- a/client/coral-embed-stream/src/Comment.js
+++ b/client/coral-embed-stream/src/Comment.js
@@ -157,31 +157,31 @@ class Comment extends React.Component {
-
-
-
-
- setActiveReplyBox(comment.id)}
- parentCommentId={parentId || comment.id}
- currentUserId={currentUser && currentUser.id}
- banned={false} />
-
-
-
-
-
-
-
+
+
+
+
+ setActiveReplyBox(comment.id)}
+ parentCommentId={parentId || comment.id}
+ currentUserId={currentUser && currentUser.id}
+ banned={false} />
+
+
+
+
+
+
+
diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js
index 1c52fb68c..ce6dc638a 100644
--- a/client/coral-embed-stream/src/Embed.js
+++ b/client/coral-embed-stream/src/Embed.js
@@ -221,6 +221,7 @@ class Embed extends Component {
comments={asset.comments} />