mirror of
https://github.com/wassname/talk.git
synced 2026-07-26 13:37:38 +08:00
Merge branch 'master' into story-139830877-bug-local-user
This commit is contained in:
@@ -4,22 +4,22 @@ import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-ro
|
||||
import Streams from 'containers/Streams/Streams';
|
||||
import Configure from 'containers/Configure/Configure';
|
||||
import LayoutContainer from 'containers/LayoutContainer';
|
||||
import CommentStream from 'containers/CommentStream/CommentStream';
|
||||
import InstallContainer from 'containers/Install/InstallContainer';
|
||||
import CommunityContainer from 'containers/Community/CommunityContainer';
|
||||
|
||||
import ModerationLayout from 'containers/ModerationQueue/ModerationLayout';
|
||||
import ModerationContainer from 'containers/ModerationQueue/ModerationContainer';
|
||||
import Dashboard from 'containers/Dashboard/Dashboard';
|
||||
|
||||
const routes = (
|
||||
<div>
|
||||
<Route exact path="/admin/install" component={InstallContainer}/>
|
||||
<Route path='/admin' component={LayoutContainer}>
|
||||
<IndexRoute component={ModerationContainer} />
|
||||
<Route path='embed' component={CommentStream} />
|
||||
<Route path='community' component={CommunityContainer} />
|
||||
<Route path='configure' component={Configure} />
|
||||
<Route path='streams' component={Streams} />
|
||||
<Route path='dashboard' component={Dashboard} />
|
||||
|
||||
{/* Moderation Routes */}
|
||||
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
import * as commentTypes from '../constants/comments';
|
||||
import * as actionTypes from '../constants/actions';
|
||||
|
||||
function addUsersCommentsActions (dispatch, {comments, users, actions}) {
|
||||
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});
|
||||
}
|
||||
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const fetchModerationQueueComments = () => {
|
||||
return dispatch => {
|
||||
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
|
||||
|
||||
return Promise.all([
|
||||
coralApi('/queue/comments/premod'),
|
||||
coralApi('/queue/users/flagged'),
|
||||
coralApi('/queue/comments/rejected'),
|
||||
coralApi('/queue/comments/flagged')
|
||||
])
|
||||
.then(([premodComments, pendingUsers, rejected, flagged]) => {
|
||||
|
||||
/* Combine seperate calls into a single object */
|
||||
flagged.comments.forEach(comment => comment.flagged = true);
|
||||
return {
|
||||
comments: [...premodComments.comments, ...rejected.comments, ...flagged.comments],
|
||||
users: [...premodComments.users, ...pendingUsers.users, ...rejected.users, ...flagged.users],
|
||||
actions: [...premodComments.actions, ...pendingUsers.actions, ...rejected.actions, ...flagged.actions]
|
||||
};
|
||||
})
|
||||
.then(addUsersCommentsActions.bind(this, dispatch));
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchPremodQueue = () => {
|
||||
return dispatch => {
|
||||
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
|
||||
|
||||
return coralApi('/queue/comments/premod')
|
||||
.then(addUsersCommentsActions.bind(this, dispatch));
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchPendingUsersQueue = () => {
|
||||
return dispatch => {
|
||||
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
|
||||
|
||||
return coralApi('/queue/users/flagged')
|
||||
.then(addUsersCommentsActions.bind(this, dispatch));
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchRejectedQueue = () => {
|
||||
return dispatch => {
|
||||
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
|
||||
|
||||
return coralApi('/queue/comments/rejected')
|
||||
.then(addUsersCommentsActions.bind(this, dispatch));
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchFlaggedQueue = () => {
|
||||
return dispatch => {
|
||||
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
|
||||
|
||||
return coralApi('/queue/comments/flagged')
|
||||
.then(results => {
|
||||
results.comments.forEach(comment => comment.flagged = true);
|
||||
return results;
|
||||
})
|
||||
.then(addUsersCommentsActions.bind(this, dispatch));
|
||||
};
|
||||
};
|
||||
|
||||
// Create a new comment
|
||||
export const createComment = (name, body) => {
|
||||
return (dispatch) => {
|
||||
const formData = {body, name};
|
||||
return coralApi('/comments', {method: 'POST', body: formData})
|
||||
.then(res => dispatch({type: commentTypes.COMMENT_CREATE_SUCCESS, comment: res}))
|
||||
.catch(error => dispatch({type: commentTypes.COMMENT_CREATE_FAILED, error}));
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Action disptacher related to comments
|
||||
*/
|
||||
|
||||
// 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: commentTypes.COMMENT_STATUS_UPDATE_REQUEST, id: comment.id, status});
|
||||
return coralApi(`/comments/${comment.id}/status`, {method: 'PUT', body: {status}})
|
||||
.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: commentTypes.COMMENT_FLAG, id});
|
||||
dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)});
|
||||
};
|
||||
@@ -22,17 +22,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.formField {
|
||||
.textField {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.formField label {
|
||||
.textField label {
|
||||
font-size: 1.08em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.formField input {
|
||||
.textField input {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
.heading {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.widgetTable {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.widgetTable thead th {
|
||||
border-bottom: 1px solid #f47e6b;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.widgetTable tbody tr {
|
||||
border-bottom: 1px solid lightgrey;
|
||||
}
|
||||
|
||||
.widgetTable tbody tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.widgetTable tbody td {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.lede {
|
||||
font-size: 0.9em;
|
||||
color: grey;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './FlagWidget.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const FlagWidget = ({assets}) => {
|
||||
|
||||
return (
|
||||
<table className={styles.widgetTable}>
|
||||
<thead className={styles.widgetHead}>
|
||||
<tr>
|
||||
<th></th>{/* empty on purpose */}
|
||||
<th>{lang.t('streams.article')}</th>
|
||||
<th>{lang.t('modqueue.flagged')}</th>
|
||||
<th>{lang.t('modqueue.likes')}</th>
|
||||
<th>{lang.t('dashboard.comment_count')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
assets.length
|
||||
? assets.map((asset, index) => {
|
||||
const flagCount = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary').actionCount;
|
||||
const likeCount = asset.action_summaries.find(s => s.__typename === 'LikeAssetActionSummary').actionCount;
|
||||
return (
|
||||
<tr key={asset.id}>
|
||||
<td>{index + 1}.</td>
|
||||
<td>
|
||||
<Link to={`/admin/moderate/flagged/${asset.id}`}>{asset.title}</Link>
|
||||
<p className={styles.lede}>{asset.author} - Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</td>
|
||||
<td>{likeCount}</td>
|
||||
<td>{flagCount}</td>
|
||||
<td>{asset.commentCount}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: <tr><td colSpan="3">{lang.t('dashboard.no_flags')}</td></tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
FlagWidget.propTypes = {
|
||||
assets: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
commentCount: PropTypes.number,
|
||||
action_summaries: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
__typename: PropTypes.string.isRequired,
|
||||
actionCount: PropTypes.number.isRequired,
|
||||
actionableItemCount: PropTypes.number.isRequired
|
||||
})
|
||||
)
|
||||
})
|
||||
).isRequired
|
||||
};
|
||||
|
||||
export default FlagWidget;
|
||||
@@ -13,21 +13,33 @@ export default ({handleLogout, restricted = false}) => (
|
||||
!restricted ?
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink className={styles.navLink} to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.moderate')}
|
||||
</IndexLink>
|
||||
<Link className={styles.navLink} to="/admin/streams"
|
||||
activeClassName={styles.active}>
|
||||
<IndexLink
|
||||
className={styles.navLink}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.moderate')}
|
||||
</IndexLink>
|
||||
<Link className={styles.navLink}
|
||||
to="/admin/streams"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.streams')}
|
||||
</Link>
|
||||
<Link className={styles.navLink} to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.community')}
|
||||
<Link className={styles.navLink}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.community')}
|
||||
</Link>
|
||||
<Link className={styles.navLink} to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.configure')}
|
||||
<Link
|
||||
className={styles.navLink}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.configure')}
|
||||
</Link>
|
||||
<Link
|
||||
className={styles.navLink}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.dashboard')}
|
||||
</Link>
|
||||
</Navigation>
|
||||
<div className={styles.rightPanel}>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.container {
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (--big-viewport) {
|
||||
.tab {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './CommentStream.css';
|
||||
import {Snackbar} from 'react-mdl';
|
||||
import {connect} from 'react-redux';
|
||||
import {createComment, flagComment} from 'actions/comments';
|
||||
import ModerationList from 'components/ModerationList';
|
||||
import CommentBox from 'components/CommentBox';
|
||||
|
||||
/**
|
||||
* Renders a comment stream using a ModerationList component
|
||||
* and adds a box for adding a new comment
|
||||
*/
|
||||
|
||||
class CommentStream extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {snackbar: false, snackbarMsg: ''};
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.onClickAction = this.onClickAction.bind(this);
|
||||
}
|
||||
|
||||
// Fetch the comments before mounting
|
||||
componentWillMount () {
|
||||
this.props.dispatch({type: 'COMMENT_STREAM_FETCH'});
|
||||
}
|
||||
|
||||
// Submit the new comment
|
||||
onSubmit (comment) {
|
||||
this.props.dispatch(createComment(comment.name, comment.body));
|
||||
}
|
||||
|
||||
// The only action for now is flagging
|
||||
onClickAction (action, id) {
|
||||
if (action === 'flag') {
|
||||
this.props.dispatch(flagComment(id));
|
||||
clearTimeout(this._snackTimeout);
|
||||
this.setState({snackbar: true, snackbarMsg: 'Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.'});
|
||||
this._snackTimeout = setTimeout(() => this.setState({snackbar: false}), 30000);
|
||||
}
|
||||
}
|
||||
|
||||
// Render the comment box along with the ModerationList
|
||||
render ({comments, users}, {snackbar, snackbarMsg}) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<CommentBox onSubmit={this.onSubmit} />
|
||||
<ModerationList isActive hideActive
|
||||
singleView={false}
|
||||
commentIds={comments.ids}
|
||||
comments={comments.byId}
|
||||
users={users.byId}
|
||||
onClickAction={this.onClickAction}
|
||||
actions={['flag']}
|
||||
loading={comments.loading} />
|
||||
<Snackbar active={snackbar}>{snackbarMsg}</Snackbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
comments: state.comments.toJS(),
|
||||
users: state.users.toJS()
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(CommentStream);
|
||||
@@ -0,0 +1,23 @@
|
||||
.Dashboard {
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
margin-top: 10px;
|
||||
flex: 1;
|
||||
box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.2);
|
||||
margin-right: 10px;
|
||||
padding: 15px;
|
||||
|
||||
}
|
||||
|
||||
.widget:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import {compose} from 'react-apollo';
|
||||
import {mostFlags} from 'coral-admin/src/graphql/queries';
|
||||
import {Spinner} from 'coral-ui';
|
||||
import styles from './Dashboard.css';
|
||||
import FlagWidget from '../../components/FlagWidget';
|
||||
|
||||
class Dashboard extends React.Component {
|
||||
render () {
|
||||
|
||||
const {data} = this.props;
|
||||
const {metrics: assets} = data;
|
||||
|
||||
if (data.loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
return <code><pre>{data.error}</pre></code>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.Dashboard}>
|
||||
<div className={styles.widget}>
|
||||
<h2 className={styles.heading}>Top Ten Articles with the most flagged comments</h2>
|
||||
<FlagWidget assets={assets} />
|
||||
</div>
|
||||
<div className={styles.widget}>
|
||||
<h2 className={styles.heading}>Top ten comments with the most likes</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
mostFlags
|
||||
)(Dashboard);
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {FormField, Button} from 'coral-ui';
|
||||
import {TextField, Button} from 'coral-ui';
|
||||
|
||||
const AddOrganizationName = props => {
|
||||
const {handleSettingsChange, handleSettingsSubmit, install} = props;
|
||||
@@ -12,8 +12,8 @@ const AddOrganizationName = props => {
|
||||
</p>
|
||||
<div className={styles.form}>
|
||||
<form onSubmit={handleSettingsSubmit}>
|
||||
<FormField
|
||||
className={styles.FormField}
|
||||
<TextField
|
||||
className={styles.TextField}
|
||||
id="organizationName"
|
||||
type="text"
|
||||
label='Organization name'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {FormField, Button, Spinner} from 'coral-ui';
|
||||
import {TextField, Button, Spinner} from 'coral-ui';
|
||||
|
||||
const InitialStep = props => {
|
||||
const {handleUserChange, handleUserSubmit, install} = props;
|
||||
@@ -8,8 +8,8 @@ const InitialStep = props => {
|
||||
<div className={styles.step}>
|
||||
<div className={styles.form}>
|
||||
<form onSubmit={handleUserSubmit}>
|
||||
<FormField
|
||||
className={styles.formField}
|
||||
<TextField
|
||||
className={styles.textField}
|
||||
id="email"
|
||||
type="email"
|
||||
label='Email address'
|
||||
@@ -19,8 +19,8 @@ const InitialStep = props => {
|
||||
noValidate
|
||||
/>
|
||||
|
||||
<FormField
|
||||
className={styles.formField}
|
||||
<TextField
|
||||
className={styles.textField}
|
||||
id="username"
|
||||
type="text"
|
||||
label='Username'
|
||||
@@ -29,8 +29,8 @@ const InitialStep = props => {
|
||||
errorMsg={install.errors.username}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
className={styles.formField}
|
||||
<TextField
|
||||
className={styles.textField}
|
||||
id="password"
|
||||
type="password"
|
||||
label='Password'
|
||||
@@ -39,8 +39,8 @@ const InitialStep = props => {
|
||||
errorMsg={install.errors.password}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
className={styles.formField}
|
||||
<TextField
|
||||
className={styles.textField}
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
label='Confirm Password'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {Button, Select, Option, FormField} from 'coral-ui';
|
||||
import {Button, Select, Option, TextField} from 'coral-ui';
|
||||
|
||||
const InviteTeamMembers = props => {
|
||||
const {nextStep} = props;
|
||||
@@ -14,19 +14,19 @@ const InviteTeamMembers = props => {
|
||||
<div className={styles.form}>
|
||||
<form>
|
||||
|
||||
<FormField
|
||||
className={styles.formField}
|
||||
<TextField
|
||||
className={styles.textField}
|
||||
id="email"
|
||||
type="email"
|
||||
label='Email address' required/>
|
||||
|
||||
<FormField
|
||||
className={styles.formField}
|
||||
<TextField
|
||||
className={styles.textField}
|
||||
id="username"
|
||||
type="text"
|
||||
label='Username' required/>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<div className={styles.textField}>
|
||||
<label htmlFor='role'>Assing a role</label>
|
||||
<Select id='role' label='Select Role'>
|
||||
<Option>Admin</Option>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.formField {
|
||||
.textField {
|
||||
text-align: left;
|
||||
|
||||
label {
|
||||
|
||||
@@ -9,12 +9,13 @@ const ModerationQueue = ({activeTab = 'premod', ...props}) => {
|
||||
<ul>
|
||||
{
|
||||
props.data[activeTab].map((comment, i) => {
|
||||
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
|
||||
return <Comment
|
||||
key={i}
|
||||
index={i}
|
||||
comment={comment}
|
||||
suspectWords={props.suspectWords}
|
||||
actions={actionsMap[comment.status]}
|
||||
actions={actionsMap[status]}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment}
|
||||
|
||||
@@ -318,6 +318,7 @@ span {
|
||||
}
|
||||
|
||||
.flagBox {
|
||||
max-width: 480px;
|
||||
border-top: 1px solid rgba(66, 66, 66, 0.12);
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
export const actionsMap = {
|
||||
PREMOD: ['REJECT', 'APPROVE', 'BAN'],
|
||||
FLAGGED: ['REJECT', 'APPROVE'],
|
||||
FLAGGED: ['REJECT', 'APPROVE', 'BAN'],
|
||||
REJECTED: ['APPROVE']
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import {graphql} from 'react-apollo';
|
||||
|
||||
import MOST_FLAGS from './mostFlags.graphql';
|
||||
import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
|
||||
|
||||
export const mostFlags = graphql(MOST_FLAGS, {
|
||||
options: () => {
|
||||
|
||||
// currently hard-coded per Greg's advice
|
||||
const fiveMinutesAgo = new Date();
|
||||
fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 305);
|
||||
return {
|
||||
variables: {
|
||||
sort: 'FLAG',
|
||||
from: fiveMinutesAgo.toISOString(),
|
||||
to: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
|
||||
options: ({params: {id = ''}}) => {
|
||||
return {
|
||||
|
||||
@@ -9,7 +9,8 @@ query ModQueue ($asset_id: ID!) {
|
||||
}
|
||||
flagged: comments(query: {
|
||||
action_type: FLAG,
|
||||
asset_id: $asset_id
|
||||
asset_id: $asset_id,
|
||||
statuses: [NONE, PREMOD]
|
||||
}) {
|
||||
...commentView
|
||||
action_summaries {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
query Metrics ($from: Date!, $to: Date!, $sort: ACTION_TYPE!) {
|
||||
metrics(from: $from, to: $to, sort: $sort) {
|
||||
id
|
||||
title
|
||||
url
|
||||
commentCount
|
||||
author
|
||||
created_at
|
||||
action_summaries {
|
||||
actionCount
|
||||
actionableItemCount
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
"loading": "Loading results"
|
||||
},
|
||||
"modqueue": {
|
||||
"likes": "likes",
|
||||
"all": "all",
|
||||
"premod": "pre-mod",
|
||||
"rejected": "rejected",
|
||||
@@ -50,6 +51,7 @@
|
||||
"configure": {
|
||||
"custom-css-url": "Custom CSS URL",
|
||||
"custom-css-url-desc": "URL of a CSS stylesheet that will override default Embed Stream styles. Can be internal or external.",
|
||||
"dashboard": "Dashboard",
|
||||
"enable-pre-moderation": "Enable pre-moderation",
|
||||
"enable-pre-moderation-text": "Moderators must approve any comment before it is published.",
|
||||
"require-email-verification": "Require Email Verification",
|
||||
@@ -106,6 +108,10 @@
|
||||
"email": "Another member of the community recently flagged your {0} for review. Because of its content your {0} was rejected. This means you can no longer comment, like, or flag content until you rewrite your {0}. Please e-mail moderator@newsorg.com if you have any questions or concerns.",
|
||||
"write_message": "Write a message"
|
||||
},
|
||||
"dashboard": {
|
||||
"no_flags": "There have been no flags in the last 5 minutes! Hooray!",
|
||||
"comment_count": "Comments"
|
||||
},
|
||||
"streams": {
|
||||
"search": "Search",
|
||||
"filter-streams": "Filter Streams",
|
||||
@@ -140,6 +146,7 @@
|
||||
"loading": "Cargando resultados"
|
||||
},
|
||||
"modqueue": {
|
||||
"likes": "gustos",
|
||||
"premod": "pre-mod",
|
||||
"rejected": "rechazado",
|
||||
"flagged": "marcado",
|
||||
@@ -161,6 +168,7 @@
|
||||
"configure": {
|
||||
"custom-css-url": "URL CSS a medida",
|
||||
"custom-css-url-desc": "URL de una hoja de estilo que va a sobrescribir los estilos por defecto de Embed Stream. Puede ser interna o externa.",
|
||||
"dashboard": "Panel",
|
||||
"enable-pre-moderation": "Habilitar pre-moderación",
|
||||
"enable-pre-moderation-text": "Los moderadores deben aprobar cada comentario antes de que sea publicado.",
|
||||
"require-email-verification": "Necesita confirmación de correo",
|
||||
@@ -207,6 +215,10 @@
|
||||
"cancel": "Cancelar",
|
||||
"yes_ban_user": "Si, Suspendan el usuario"
|
||||
},
|
||||
"dashbord": {
|
||||
"no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!",
|
||||
"comment_count": "Comentarios"
|
||||
},
|
||||
"streams": {
|
||||
"search": "",
|
||||
"filter-streams": "",
|
||||
|
||||
@@ -35,3 +35,7 @@ p {
|
||||
.wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from 'react';
|
||||
import {Button, Checkbox} from 'coral-ui';
|
||||
import {Button, Checkbox, TextField} from 'coral-ui';
|
||||
|
||||
import styles from './ConfigureCommentStream.css';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export default ({handleChange, handleApply, changed, ...props}) => (
|
||||
export default ({handleChange, handleApply, changed, updateQuestionBoxContent, ...props}) => (
|
||||
<form onSubmit={handleApply}>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.container}>
|
||||
@@ -32,25 +33,31 @@ export default ({handleChange, handleApply, changed, ...props}) => (
|
||||
title: lang.t('configureCommentStream.enablePremod'),
|
||||
description: lang.t('configureCommentStream.enablePremodDescription')
|
||||
}} />
|
||||
{/* To be implimented
|
||||
<ul>
|
||||
<li>
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
cStyle={changed ? 'green' : 'darkGrey'}
|
||||
name="premodLinks"
|
||||
onChange={handleChange}
|
||||
defaultChecked={props.premodLinks}
|
||||
info={{
|
||||
title: lang.t('configureCommentStream.enablePremodLinks'),
|
||||
description: lang.t('configureCommentStream.enablePremodDescription')
|
||||
}} />
|
||||
</li>
|
||||
</ul>
|
||||
*/}
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
cStyle={changed ? 'green' : 'darkGrey'}
|
||||
name="qboxenable"
|
||||
onChange={handleChange}
|
||||
defaultChecked={props.questionBoxEnable}
|
||||
info={{
|
||||
title: lang.t('configureCommentStream.enableQuestionBox'),
|
||||
description: lang.t('configureCommentStream.enableQuestionBoxDescription')
|
||||
}} />
|
||||
|
||||
<div className={`${props.questionBoxEnable ? null : styles.hidden}`} >
|
||||
<TextField
|
||||
id="qboxcontent"
|
||||
onChange={updateQuestionBoxContent}
|
||||
rows={3}
|
||||
value={props.questionBoxContent}
|
||||
label={lang.t('configureCommentStream.includeQuestionHere')}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
);
|
||||
|
||||
@@ -21,18 +21,23 @@ class ConfigureStreamContainer extends Component {
|
||||
this.toggleStatus = this.toggleStatus.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleApply = this.handleApply.bind(this);
|
||||
this.updateQuestionBoxContent = this.updateQuestionBoxContent.bind(this);
|
||||
}
|
||||
|
||||
handleApply (e) {
|
||||
e.preventDefault();
|
||||
const {elements} = e.target;
|
||||
const premod = elements.premod.checked;
|
||||
const questionBoxEnable = elements.qboxenable.checked;
|
||||
const questionBoxContent = elements.qboxcontent.value;
|
||||
|
||||
// const premodLinks = elements.premodLinks.checked;
|
||||
const {changed} = this.state;
|
||||
|
||||
const newConfig = {
|
||||
moderation: premod ? 'PRE' : 'POST'
|
||||
moderation: premod ? 'PRE' : 'POST',
|
||||
questionBoxEnable,
|
||||
questionBoxContent
|
||||
};
|
||||
|
||||
if (changed) {
|
||||
@@ -45,12 +50,20 @@ class ConfigureStreamContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleChange () {
|
||||
handleChange (e) {
|
||||
if (e.target && e.target.id === 'qboxenable') {
|
||||
this.props.asset.settings.questionBoxEnable = e.target.checked;
|
||||
}
|
||||
this.setState({
|
||||
changed: true
|
||||
});
|
||||
}
|
||||
|
||||
updateQuestionBoxContent(e) {
|
||||
this.props.asset.settings.questionBoxContent = e.target.value;
|
||||
this.handleChange(e);
|
||||
}
|
||||
|
||||
toggleStatus () {
|
||||
this.props.updateStatus(
|
||||
this.props.asset.closedAt === null ? 'closed' : 'open'
|
||||
@@ -66,6 +79,8 @@ class ConfigureStreamContainer extends Component {
|
||||
render () {
|
||||
const status = this.props.asset.closedAt === null ? 'open' : 'closed';
|
||||
const premod = this.props.asset.settings.moderation === 'PRE';
|
||||
const questionBoxEnable = this.props.asset.settings.questionBoxEnable;
|
||||
const questionBoxContent = this.props.asset.settings.questionBoxContent;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -75,6 +90,9 @@ class ConfigureStreamContainer extends Component {
|
||||
changed={this.state.changed}
|
||||
premodLinks={false}
|
||||
premod={premod}
|
||||
updateQuestionBoxContent={this.updateQuestionBoxContent}
|
||||
questionBoxEnable={questionBoxEnable}
|
||||
questionBoxContent={questionBoxContent}
|
||||
/>
|
||||
<hr />
|
||||
<h3>{status === 'open' ? 'Close' : 'Open'} Comment Stream</h3>
|
||||
@@ -94,7 +112,7 @@ const mapStateToProps = (state) => ({
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
updateStatus: status => dispatch(updateOpenStatus(status)),
|
||||
updateConfiguration: newConfig => dispatch(updateConfiguration(newConfig))
|
||||
updateConfiguration: newConfig => dispatch(updateConfiguration(newConfig)),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
|
||||
@@ -7,18 +7,24 @@
|
||||
"enablePremod": "Enable Premoderation",
|
||||
"enablePremodDescription": "Moderators must approve any comment before its published.",
|
||||
"enablePremodLinks": "Pre-Moderate Comments Containing Links",
|
||||
"enablePremodLinksDescription": "Moderators must approve any comment containing a link before its published."
|
||||
"enablePremodLinksDescription": "Moderators must approve any comment containing a link before its published.",
|
||||
"enableQuestionBox": "Ask readers a question",
|
||||
"enableQuestionBoxDescription": "This question will appear at the top of this comment stram. Ask readers about a certain issue in the article or pose discussion questions, etc.",
|
||||
"includeQuestionHere": "Write your question here."
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"configureCommentStream": {
|
||||
"apply": "Aplicar",
|
||||
"title": "Configurar los comentarios",
|
||||
"description": "Como Administrador puedes modificar las opciones de los comentarios en este artículo",
|
||||
"description": "Como Administrador/a puedes modificar las opciones de los comentarios en este artículo",
|
||||
"enablePremod": "Activar Pre Moderación",
|
||||
"enablePremodDescription": "Los Moderadores deben aprobar cualquier comentario antes de su publicación",
|
||||
"enablePremodDescription": "Los y las Moderadoras deben aprobar cualquier comentario antes de su publicación",
|
||||
"enablePremodLinks": "Pre-Moderar Commentarios que contienen Links",
|
||||
"enablePremodLinksDescription": "Los Moderadores deben probar cualquier comentario que contengan links antes de su publicación."
|
||||
"enablePremodLinksDescription": "Los y las Moderadoras deben probar cualquier comentario que contengan links antes de su publicación.",
|
||||
"enableQuestionBox": "Hacer una pregunta a los y las lectoras.",
|
||||
"enableQuestionBoxDescription": "Esta pregunta aparecera en la parte de arriba del hilo de comentarios.",
|
||||
"includeQuestionHere": "Escribir la pregunta aquí."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default function fetcher(query) {
|
||||
return fetch(`${window.location.host}/api/v1/graph/ql`, {
|
||||
return fetch(`${window.location.origin}/api/v1/graph/ql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
||||
@@ -17,6 +17,7 @@ import PubDate from 'coral-plugin-pubdate/PubDate';
|
||||
import {ReplyBox, ReplyButton} from 'coral-plugin-replies';
|
||||
import FlagComment from 'coral-plugin-flags/FlagComment';
|
||||
import LikeButton from 'coral-plugin-likes/LikeButton';
|
||||
import LoadMore from 'coral-embed-stream/src/LoadMore';
|
||||
|
||||
import styles from './Comment.css';
|
||||
|
||||
@@ -89,6 +90,7 @@ class Comment extends React.Component {
|
||||
showSignInDialog,
|
||||
postLike,
|
||||
postFlag,
|
||||
loadMore,
|
||||
setActiveReplyBox,
|
||||
activeReplyBox,
|
||||
deleteAction
|
||||
@@ -172,6 +174,17 @@ class Comment extends React.Component {
|
||||
comment={reply} />;
|
||||
})
|
||||
}
|
||||
{
|
||||
comment.replies &&
|
||||
<div className='coral-load-more-replies'>
|
||||
<LoadMore
|
||||
id={asset.id}
|
||||
comments={comment.replies}
|
||||
parentId={comment.id}
|
||||
moreComments={comment.replyCount > comment.replies.length}
|
||||
loadMore={loadMore}/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {Notification, notificationActions, authActions, assetActions, pym} from
|
||||
|
||||
import Stream from './Stream';
|
||||
import InfoBox from 'coral-plugin-infobox/InfoBox';
|
||||
import QuestionBox from 'coral-plugin-questionbox/QuestionBox';
|
||||
import {ModerationLink} from 'coral-plugin-moderation';
|
||||
import Count from 'coral-plugin-comment-count/CommentCount';
|
||||
import CommentBox from 'coral-plugin-commentbox/CommentBox';
|
||||
@@ -114,6 +115,10 @@ class Embed extends Component {
|
||||
content={asset.settings.infoBoxContent}
|
||||
enable={asset.settings.infoBoxEnable}
|
||||
/>
|
||||
<QuestionBox
|
||||
content={asset.settings.questionBoxContent}
|
||||
enable={asset.settings.questionBoxEnable}
|
||||
/>
|
||||
<RestrictedContent restricted={banned} restrictedComp={
|
||||
<SuspendedAccount
|
||||
canEditName={user && user.canEditName}
|
||||
@@ -151,6 +156,7 @@ class Embed extends Component {
|
||||
currentUser={user}
|
||||
postLike={this.props.postLike}
|
||||
postFlag={this.props.postFlag}
|
||||
loadMore={this.props.loadMore}
|
||||
deleteAction={this.props.deleteAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
comments={asset.comments} />
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-framework/translations.json';
|
||||
import {ADDTL_COMMENTS_ON_LOAD_MORE} from 'coral-framework/constants/comments';
|
||||
import {Button} from 'coral-ui';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const loadMoreComments = (assetId, comments, loadMore) => {
|
||||
const loadMoreComments = (assetId, comments, loadMore, parentId) => {
|
||||
|
||||
if (!comments.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursor = parentId
|
||||
? comments[1].created_at
|
||||
: comments[comments.length - 1].created_at;
|
||||
|
||||
loadMore({
|
||||
limit: 10,
|
||||
cursor: comments[comments.length - 1].created_at,
|
||||
limit: ADDTL_COMMENTS_ON_LOAD_MORE,
|
||||
cursor,
|
||||
assetId,
|
||||
sort: 'REVERSE_CHRONOLOGICAL'
|
||||
parent_id: parentId,
|
||||
sort: parentId ? 'CHRONOLOGICAL' : 'REVERSE_CHRONOLOGICAL'
|
||||
});
|
||||
};
|
||||
|
||||
const LoadMore = ({assetId, comments, loadMore, moreComments}) => (
|
||||
const LoadMore = ({assetId, comments, loadMore, moreComments, parentId}) => (
|
||||
moreComments
|
||||
? <Button
|
||||
className='coral-load-more'
|
||||
onClick={() => loadMoreComments(assetId, comments, loadMore)}>
|
||||
onClick={() => loadMoreComments(assetId, comments, loadMore, parentId)}>
|
||||
{
|
||||
lang.t('loadMore')
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ class Stream extends React.Component {
|
||||
addNotification,
|
||||
postFlag,
|
||||
postLike,
|
||||
loadMore,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
refetch
|
||||
@@ -59,6 +60,7 @@ class Stream extends React.Component {
|
||||
currentUser={currentUser}
|
||||
postLike={postLike}
|
||||
postFlag={postFlag}
|
||||
loadMore={loadMore}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
key={comment.id}
|
||||
|
||||
@@ -62,7 +62,7 @@ hr {
|
||||
.coral-plugin-infobox-info {
|
||||
top: 0;
|
||||
border: 0;
|
||||
background: rgb(105,105,105);
|
||||
background: rgb(35,118,216);
|
||||
color: white;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
@@ -72,6 +72,21 @@ hr {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Question Box Styles */
|
||||
.coral-plugin-questionbox-info {
|
||||
top: 0;
|
||||
border: 0;
|
||||
background: rgb(105,105,105);
|
||||
color: white;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
margin-bottom: 0px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
@@ -317,3 +332,13 @@ button.coral-load-more {
|
||||
button.coral-load-more:hover {
|
||||
background-color: #4399FF;
|
||||
}
|
||||
|
||||
.coral-load-more-replies {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.coral-load-more-replies button.coral-load-more {
|
||||
width: initial;
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const ADDTL_COMMENTS_ON_LOAD_MORE = 10;
|
||||
@@ -35,13 +35,28 @@ export const queryStream = graphql(STREAM_QUERY, {
|
||||
asset_id,
|
||||
sort
|
||||
},
|
||||
updateQuery: (oldData, {fetchMoreResult:{data:{new_top_level_comments}}}) => ({
|
||||
...oldData,
|
||||
asset: {
|
||||
...oldData.asset,
|
||||
comments: [...oldData.asset.comments, ...new_top_level_comments]
|
||||
updateQuery: (oldData, {fetchMoreResult:{data:{new_top_level_comments}}}) =>
|
||||
|
||||
// If loading more replies
|
||||
parent_id ? {
|
||||
...oldData,
|
||||
asset: {
|
||||
...oldData.asset,
|
||||
comments: oldData.asset.comments.map((comment) =>
|
||||
comment.id === parent_id
|
||||
? {...comment, replies: [...comment.replies, ...new_top_level_comments]}
|
||||
: comment)
|
||||
}
|
||||
}
|
||||
|
||||
// If loading more top-level comments
|
||||
: {
|
||||
...oldData,
|
||||
asset: {
|
||||
...oldData.asset,
|
||||
comments: [...oldData.asset.comments, ...new_top_level_comments]
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
query LoadMoreComments($limit: Int = 5, $cursor: Date, $parent_id: ID, $asset_id: ID, $sort: SORT_ORDER) {
|
||||
new_top_level_comments: comments(query: {limit: $limit, cursor: $cursor, parent_id: $parent_id, asset_id: $asset_id, sort: $sort}) {
|
||||
...commentView
|
||||
replyCount
|
||||
replies(limit: 3) {
|
||||
...commentView
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ query AssetQuery($asset_url: String!) {
|
||||
moderation
|
||||
infoBoxEnable
|
||||
infoBoxContent
|
||||
questionBoxEnable
|
||||
questionBoxContent
|
||||
closeTimeout
|
||||
closedMessage
|
||||
charCountEnable
|
||||
@@ -20,6 +22,7 @@ query AssetQuery($asset_url: String!) {
|
||||
commentCount
|
||||
comments(limit: 10) {
|
||||
...commentView
|
||||
replyCount
|
||||
replies(limit: 3) {
|
||||
...commentView
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
const packagename = 'coral-plugin-questionbox';
|
||||
|
||||
const QuestionBox = ({enable, content}) =>
|
||||
<div
|
||||
className={`${packagename}-info ${enable ? null : 'hidden'}` }>
|
||||
{content}
|
||||
</div>;
|
||||
|
||||
export default QuestionBox;
|
||||
@@ -50,7 +50,7 @@ class Stream extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize GraphQL queries or mutations with the `gql` tag
|
||||
// Initialize GraphQL queries or mutations with the gql tag
|
||||
const StreamQuery = gql`fragment commentView on Comment {
|
||||
id
|
||||
body
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import FormField from 'coral-ui/components/FormField';
|
||||
import TextField from 'coral-ui/components/TextField';
|
||||
import Alert from './Alert';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import {Dialog} from 'coral-ui';
|
||||
@@ -28,7 +28,7 @@ const CreateDisplayNameDialog = ({open, handleClose, offset, formData, handleSub
|
||||
<label htmlFor="username">{lang.t('createdisplay.yourusername')}</label>
|
||||
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
|
||||
<form id="saveDisplayName" onSubmit={handleSubmitDisplayName}>
|
||||
<FormField
|
||||
<TextField
|
||||
id="username"
|
||||
type="string"
|
||||
label={lang.t('createdisplay.username')}
|
||||
|
||||
@@ -26,7 +26,7 @@ class ForgotContent extends React.Component {
|
||||
<h1>{lang.t('signIn.recoverPassword')}</h1>
|
||||
</div>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className={styles.formField}>
|
||||
<div className={styles.textField}>
|
||||
<label htmlFor="email">{lang.t('signIn.email')}</label>
|
||||
<input
|
||||
ref={input => this.emailInput = input}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Alert from './Alert';
|
||||
import {Button, FormField, Spinner, Success} from 'coral-ui';
|
||||
import {Button, TextField, Spinner, Success} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
@@ -32,7 +32,7 @@ const SignInContent = ({
|
||||
auth.emailVerificationFailure
|
||||
? <form onSubmit={handleResendVerification}>
|
||||
<p>{lang.t('signIn.requestNewVerifyEmail')}</p>
|
||||
<FormField
|
||||
<TextField
|
||||
id="confirm-email"
|
||||
type="email"
|
||||
label={lang.t('signIn.email')}
|
||||
@@ -54,14 +54,14 @@ const SignInContent = ({
|
||||
</h1>
|
||||
</div>
|
||||
<form onSubmit={handleSignIn}>
|
||||
<FormField
|
||||
<TextField
|
||||
id="email"
|
||||
type="email"
|
||||
label={lang.t('signIn.email')}
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormField
|
||||
<TextField
|
||||
id="password"
|
||||
type="password"
|
||||
label={lang.t('signIn.password')}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Alert from './Alert';
|
||||
import {Button, FormField, Spinner, Success} from 'coral-ui';
|
||||
import {Button, TextField, Spinner, Success} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
@@ -79,7 +79,7 @@ class SignUpContent extends React.Component {
|
||||
</h1>
|
||||
</div>
|
||||
<form onSubmit={handleSignUp}>
|
||||
<FormField
|
||||
<TextField
|
||||
id="email"
|
||||
type="email"
|
||||
label={lang.t('signIn.email')}
|
||||
@@ -88,7 +88,7 @@ class SignUpContent extends React.Component {
|
||||
errorMsg={errors.email}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormField
|
||||
<TextField
|
||||
id="username"
|
||||
type="text"
|
||||
label={lang.t('signIn.username')}
|
||||
@@ -97,7 +97,7 @@ class SignUpContent extends React.Component {
|
||||
errorMsg={errors.username}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormField
|
||||
<TextField
|
||||
id="password"
|
||||
type="password"
|
||||
label={lang.t('signIn.password')}
|
||||
@@ -108,7 +108,7 @@ class SignUpContent extends React.Component {
|
||||
minLength="8"
|
||||
/>
|
||||
{ errors.password && <span className={styles.hint}> Password must be at least 8 characters. </span> }
|
||||
<FormField
|
||||
<TextField
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
label={lang.t('signIn.confirmPassword')}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
.formField {
|
||||
.textField {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.formField label {
|
||||
.textField label {
|
||||
font-size: 1.08em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.formField input {
|
||||
.textField input {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './FormField.css';
|
||||
import styles from './TextField.css';
|
||||
|
||||
const FormField = ({className, showErrors = false, errorMsg, label, ...props}) => (
|
||||
<div className={`${styles.formField} ${className ? className : ''}`}>
|
||||
const TextField = ({className, showErrors = false, errorMsg, label, ...props}) => (
|
||||
<div className={`${styles.textField} ${className ? className : ''}`}>
|
||||
<label htmlFor={props.id}>
|
||||
{label}
|
||||
</label>
|
||||
@@ -15,7 +15,7 @@ const FormField = ({className, showErrors = false, errorMsg, label, ...props}) =
|
||||
</div>
|
||||
);
|
||||
|
||||
FormField.propTypes = {
|
||||
TextField.propTypes = {
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
@@ -23,4 +23,4 @@ FormField.propTypes = {
|
||||
type: PropTypes.string
|
||||
};
|
||||
|
||||
export default FormField;
|
||||
export default TextField;
|
||||
@@ -13,7 +13,7 @@ export {default as Icon} from './components/Icon';
|
||||
export {default as List} from './components/List';
|
||||
export {default as Item} from './components/Item';
|
||||
export {default as Card} from './components/Card';
|
||||
export {default as FormField} from './components/FormField';
|
||||
export {default as TextField} from './components/TextField';
|
||||
export {default as Success} from './components/Success';
|
||||
export {default as Pager} from './components/Pager';
|
||||
export {default as Wizard} from './components/Wizard';
|
||||
|
||||
@@ -46,6 +46,15 @@ const findOrCreateAssetByURL = (context, asset_url) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getAssetsForMetrics = ({loaders: {Actions, Comments}}) => {
|
||||
return Actions.getByTypes({action_type: 'FLAG', item_type: 'COMMENT'})
|
||||
.then((actions) => { // ALL ACTIONS :O
|
||||
const ids = actions.map(({item_id}) => item_id);
|
||||
|
||||
return Comments.getByQuery({ids});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
@@ -59,6 +68,7 @@ module.exports = (context) => ({
|
||||
getByURL: (url) => findOrCreateAssetByURL(context, url),
|
||||
|
||||
getByID: new DataLoader((ids) => genAssetsByID(context, ids)),
|
||||
getForMetrics: () => getAssetsForMetrics(context),
|
||||
getAll: new util.SingletonResolver(() => AssetModel.find({}))
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ const getCountsByAssetID = (context, asset_ids) => {
|
||||
$in: asset_ids
|
||||
},
|
||||
status: {
|
||||
$in: [null, 'ACCEPTED']
|
||||
$in: ['NONE', 'ACCEPTED']
|
||||
},
|
||||
parent_id: null
|
||||
}
|
||||
@@ -51,7 +51,7 @@ const getCountsByParentID = (context, parent_ids) => {
|
||||
$in: parent_ids
|
||||
},
|
||||
status: {
|
||||
$in: [null, 'ACCEPTED']
|
||||
$in: ['NONE', 'ACCEPTED']
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -88,7 +88,7 @@ const getCommentsByQuery = ({user}, {ids, statuses, asset_id, parent_id, author_
|
||||
} else {
|
||||
comments = comments.where({
|
||||
status: {
|
||||
$in: [null, 'ACCEPTED']
|
||||
$in: ['NONE', 'ACCEPTED']
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ const _ = require('lodash');
|
||||
const Actions = require('./actions');
|
||||
const Assets = require('./assets');
|
||||
const Comments = require('./comments');
|
||||
const Metrics = require('./metrics');
|
||||
const Settings = require('./settings');
|
||||
const Users = require('./users');
|
||||
|
||||
@@ -18,6 +19,7 @@ module.exports = (context) => {
|
||||
Actions,
|
||||
Assets,
|
||||
Comments,
|
||||
Metrics,
|
||||
Settings,
|
||||
Users
|
||||
].map((loaders) => {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
const _ = require('lodash');
|
||||
const DataLoader = require('dataloader');
|
||||
const {objectCacheKeyFn} = require('./util');
|
||||
|
||||
const CommentModel = require('../../models/comment');
|
||||
const ActionModel = require('../../models/action');
|
||||
|
||||
const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
|
||||
|
||||
let commentMetrics = {};
|
||||
let assetMetrics = [];
|
||||
|
||||
return Metrics.getRecentActions.load({from, to})
|
||||
.then((actionSummaries) => {
|
||||
|
||||
commentMetrics = actionSummaries.reduce((acc, {item_id, action_type, count}) => {
|
||||
if (!(item_id in acc)) {
|
||||
acc[item_id] = [];
|
||||
}
|
||||
|
||||
acc[item_id].push({action_type, count});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Collect just the comment id's.
|
||||
let commentIDs = _.uniq(actionSummaries.map((as) => as.item_id));
|
||||
|
||||
// Find those comments.
|
||||
return Metrics.getSpecificComments.loadMany(commentIDs);
|
||||
})
|
||||
.then((comments) => {
|
||||
|
||||
let commentResults = _.groupBy(comments, 'asset_id');
|
||||
|
||||
assetMetrics = Object.keys(commentResults).map((asset_id) => {
|
||||
let ids = commentResults[asset_id].map((comment) => comment.id);
|
||||
let summaries = _.groupBy(_.flatten(ids.map((id) => commentMetrics[id])), 'action_type');
|
||||
|
||||
let action_summaries = Object.keys(summaries).map((action_type) => ({
|
||||
action_type,
|
||||
actionCount: summaries[action_type].reduce((acc, {count}) => acc + count, 0),
|
||||
actionableItemCount: summaries[action_type].length
|
||||
}));
|
||||
|
||||
return {action_summaries, id: asset_id};
|
||||
});
|
||||
|
||||
// Sort these metrics by the predefined sort order. This will ensure that
|
||||
// if the action summary does not exist on the object, that it is less
|
||||
// prefered over the one that does have it.
|
||||
assetMetrics.sort((a, b) => {
|
||||
let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sort));
|
||||
let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sort));
|
||||
|
||||
// If either a or b don't have this action type, then one of them will
|
||||
// automatically win.
|
||||
if (aActionSummary == null || bActionSummary == null) {
|
||||
if (bActionSummary != null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (aActionSummary != null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Both of them had an actionCount, hence we can determine that we could
|
||||
// compare the actual values directly.
|
||||
return bActionSummary.actionCount - aActionSummary.actionCount;
|
||||
});
|
||||
|
||||
// Only keep the top `limit`.
|
||||
assetMetrics = assetMetrics.slice(0, limit);
|
||||
|
||||
// Determine the assets that we need to return.
|
||||
return Assets.getByID.loadMany(assetMetrics.map((asset) => asset.id));
|
||||
})
|
||||
.then((assets) => {
|
||||
|
||||
// Join up the assets that are returned by their id.
|
||||
let groupedAssets = _.groupBy(assets, 'id');
|
||||
|
||||
// Return from the sorted asset metrics and return their assetes.
|
||||
return assetMetrics.map(({id, action_summaries}) => {
|
||||
if (id in groupedAssets) {
|
||||
let asset = groupedAssets[id][0];
|
||||
|
||||
// Add the action summaries to the asset.
|
||||
asset.action_summaries = action_summaries;
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
return null;
|
||||
}).filter((asset) => asset != null);
|
||||
});
|
||||
};
|
||||
|
||||
const getRecentActions = (context, {from, to}) => {
|
||||
return ActionModel.aggregate([
|
||||
|
||||
// Find all actions that were created in the time range.
|
||||
{$match: {
|
||||
item_type: 'COMMENTS',
|
||||
created_at: {
|
||||
$gt: from,
|
||||
$lt: to
|
||||
}
|
||||
}},
|
||||
|
||||
// Count all those items.
|
||||
{$group: {
|
||||
_id: {
|
||||
item_id: '$item_id',
|
||||
action_type: '$action_type'
|
||||
},
|
||||
count: {
|
||||
$sum: 1
|
||||
}
|
||||
}},
|
||||
|
||||
// Project the count to a better field.
|
||||
{$project: {
|
||||
item_id: '$_id.item_id',
|
||||
action_type: '$_id.action_type',
|
||||
count: '$count'
|
||||
}}
|
||||
]);
|
||||
};
|
||||
|
||||
const getSpecificComments = (context, ids) => {
|
||||
return CommentModel.find({
|
||||
id: {
|
||||
$in: ids
|
||||
}
|
||||
})
|
||||
.select({
|
||||
id: 1,
|
||||
asset_id: 1
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = (context) => ({
|
||||
Metrics: {
|
||||
getSpecificComments: new DataLoader((ids) => getSpecificComments(context, ids)),
|
||||
getRecentActions: new DataLoader(([{from, to}]) => getRecentActions(context, {from, to}).then((as) => [as]), {
|
||||
batch: false,
|
||||
cacheKeyFn: objectCacheKeyFn('from', 'to')
|
||||
}),
|
||||
get: ({from, to, sort, limit}) => getMetrics(context, {from, to, sort, limit})
|
||||
}
|
||||
});
|
||||
@@ -130,10 +130,20 @@ const objectCacheKeyFn = (...paths) => (obj) => {
|
||||
return paths.map((path) => obj[path]).join(':');
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps an object's paths to a string that can be used as a cache key.
|
||||
* @param {Array} paths paths on the object to be used to generate the cache
|
||||
* key
|
||||
*/
|
||||
const arrayCacheKeyFn = (arr) => {
|
||||
return arr.sort().join(':');
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
singleJoinBy,
|
||||
arrayJoinBy,
|
||||
objectCacheKeyFn,
|
||||
arrayCacheKeyFn,
|
||||
SingletonResolver,
|
||||
SharedCacheDataLoader
|
||||
};
|
||||
|
||||
@@ -45,10 +45,6 @@ const deleteAction = ({user}, {id}) => {
|
||||
};
|
||||
|
||||
module.exports = (context) => {
|
||||
|
||||
// TODO: refactor to something that'll return an error in the event an attempt
|
||||
// is made to mutate state while not logged in. There's got to be a better way
|
||||
// to do this.
|
||||
if (context.user && context.user.can('mutation:createAction', 'mutation:deleteAction')) {
|
||||
return {
|
||||
Action: {
|
||||
|
||||
+20
-10
@@ -11,10 +11,10 @@ const Wordlist = require('../../services/wordlist');
|
||||
* @param {String} body body of the comment
|
||||
* @param {String} asset_id asset for the comment
|
||||
* @param {String} parent_id optional parent of the comment
|
||||
* @param {String} [status=null] the status of the new comment
|
||||
* @param {String} [status='NONE'] the status of the new comment
|
||||
* @return {Promise} resolves to the created comment
|
||||
*/
|
||||
const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = null}, status = null) => {
|
||||
const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = null}, status = 'NONE') => {
|
||||
return CommentsService.publicCreate({
|
||||
body,
|
||||
asset_id,
|
||||
@@ -105,7 +105,7 @@ const resolveNewCommentStatus = (context, {asset_id, body}, wordlist = {}) => {
|
||||
if (charCountEnable && body.length > charCount) {
|
||||
return 'REJECTED';
|
||||
}
|
||||
return moderation === 'PRE' ? 'PREMOD' : null;
|
||||
return moderation === 'PRE' ? 'PREMOD' : 'NONE';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -169,16 +169,26 @@ const createPublicComment = (context, commentInput) => {
|
||||
* @param {String} status the new status of the comment
|
||||
*/
|
||||
|
||||
const setCommentStatus = ({comment}, {id, status}) => {
|
||||
return CommentsService.setStatus(id, status)
|
||||
.then(res => res);
|
||||
const setCommentStatus = ({loaders: {Comments}}, {id, status}) => {
|
||||
return CommentsService
|
||||
.setStatus(id, status)
|
||||
.then((comment) => {
|
||||
|
||||
// If the loaders are present, clear the caches for these values because we
|
||||
// just added a new comment, hence the counts should be updated.
|
||||
if (Comments && Comments.countByAssetID && Comments.countByParentID) {
|
||||
if (comment.parent_id != null) {
|
||||
Comments.countByParentID.clear(comment.parent_id);
|
||||
} else {
|
||||
Comments.countByAssetID.clear(comment.asset_id);
|
||||
}
|
||||
}
|
||||
|
||||
return comment;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = (context) => {
|
||||
|
||||
// TODO: refactor to something that'll return an error in the event an attempt
|
||||
// is made to mutate state while not logged in. There's got to be a better way
|
||||
// to do this.
|
||||
let mutators = {
|
||||
Comment: {
|
||||
create: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
|
||||
@@ -7,10 +7,6 @@ const setUserStatus = ({user}, {id, status}) => {
|
||||
};
|
||||
|
||||
module.exports = (context) => {
|
||||
|
||||
// TODO: refactor to something that'll return an error in the event an attempt
|
||||
// is made to mutate state while not logged in. There's got to be a better way
|
||||
// to do this.
|
||||
if (context.user && context.user.can('mutation:setUserStatus')) {
|
||||
return {
|
||||
User: {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
const AssetActionSummary = {
|
||||
__resolveType({action_type}) {
|
||||
switch (action_type) {
|
||||
case 'FLAG':
|
||||
return 'FlagAssetActionSummary';
|
||||
case 'LIKE':
|
||||
return 'LikeAssetActionSummary';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AssetActionSummary;
|
||||
@@ -1,5 +1,6 @@
|
||||
const ActionSummary = require('./action_summary');
|
||||
const Action = require('./action');
|
||||
const AssetActionSummary = require('./asset_action_summary');
|
||||
const Asset = require('./asset');
|
||||
const Comment = require('./comment');
|
||||
const Date = require('./date');
|
||||
@@ -17,6 +18,7 @@ const ValidationUserError = require('./validation_user_error');
|
||||
module.exports = {
|
||||
ActionSummary,
|
||||
Action,
|
||||
AssetActionSummary,
|
||||
Asset,
|
||||
Comment,
|
||||
Date,
|
||||
|
||||
@@ -39,6 +39,14 @@ const RootQuery = {
|
||||
return Comments.getByQuery(query);
|
||||
},
|
||||
|
||||
metrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics}}) {
|
||||
if (user == null || !user.hasRoles('ADMIN')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Metrics.get({from, to, sort, limit});
|
||||
},
|
||||
|
||||
// This returns the current user, ensure that if we aren't logged in, we
|
||||
// return null.
|
||||
me(_, args, {user}) {
|
||||
|
||||
+50
-3
@@ -67,6 +67,10 @@ type Tag {
|
||||
# The statuses that a comment may have.
|
||||
enum COMMENT_STATUS {
|
||||
|
||||
# The comment is not PREMOD, but was not applied a moderation status by a
|
||||
# moderator.
|
||||
NONE
|
||||
|
||||
# The comment has been accepted by a moderator.
|
||||
ACCEPTED
|
||||
|
||||
@@ -93,7 +97,7 @@ enum ACTION_TYPE {
|
||||
input CommentsQuery {
|
||||
|
||||
# current status of a comment.
|
||||
statuses: [COMMENT_STATUS]
|
||||
statuses: [COMMENT_STATUS!]
|
||||
|
||||
# asset that a comment is on.
|
||||
asset_id: ID
|
||||
@@ -152,7 +156,7 @@ type Comment {
|
||||
asset: Asset
|
||||
|
||||
# The current status of a comment.
|
||||
status: COMMENT_STATUS
|
||||
status: COMMENT_STATUS!
|
||||
|
||||
# The time when the comment was created
|
||||
created_at: Date!
|
||||
@@ -188,6 +192,36 @@ interface ActionSummary {
|
||||
current_user: Action
|
||||
}
|
||||
|
||||
# A summary of actions for a specific action type on an Asset.
|
||||
interface AssetActionSummary {
|
||||
|
||||
# Number of actions associated with actionable types on this this Asset.
|
||||
actionCount: Int
|
||||
|
||||
# Number of unique actionable types that are referenced by the actions.
|
||||
actionableItemCount: Int
|
||||
}
|
||||
|
||||
# A summary of counts related to all the Flags on an Asset.
|
||||
type FlagAssetActionSummary implements AssetActionSummary {
|
||||
|
||||
# Number of flags associated with actionable types on this this Asset.
|
||||
actionCount: Int
|
||||
|
||||
# Number of unique actionable types that are referenced by the flags.
|
||||
actionableItemCount: Int
|
||||
}
|
||||
|
||||
# A summary of counts related to all the Likes on an Asset.
|
||||
type LikeAssetActionSummary implements AssetActionSummary {
|
||||
|
||||
# Number of likes associated with actionable types on this this Asset.
|
||||
actionCount: Int
|
||||
|
||||
# Number of unique actionable types that are referenced by the likes.
|
||||
actionableItemCount: Int
|
||||
}
|
||||
|
||||
# LikeAction is used by users who "like" a specific entity.
|
||||
type LikeAction implements Action {
|
||||
|
||||
@@ -274,6 +308,8 @@ type Settings {
|
||||
|
||||
infoBoxEnable: Boolean
|
||||
infoBoxContent: String
|
||||
questionBoxEnable: Boolean
|
||||
questionBoxContent: String
|
||||
closeTimeout: Int
|
||||
closedMessage: String
|
||||
charCountEnable: Boolean
|
||||
@@ -313,8 +349,15 @@ type Asset {
|
||||
# The date that the asset was closed at.
|
||||
closedAt: Date
|
||||
|
||||
# Summary of all Actions against all entities associated with the Asset.
|
||||
# (likes, flags, etc.)
|
||||
action_summaries: [AssetActionSummary]
|
||||
|
||||
# The date that the asset was created.
|
||||
created_at: Date
|
||||
|
||||
# The author(s) of the asset.
|
||||
author: String
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -349,7 +392,7 @@ type ValidationUserError implements UserError {
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Queries
|
||||
## Queries;
|
||||
################################################################################
|
||||
|
||||
# Establishes the ordering of the content by their created_at time stamp.
|
||||
@@ -386,6 +429,10 @@ type RootQuery {
|
||||
|
||||
# The currently logged in user based on the request.
|
||||
me: User
|
||||
|
||||
# Metrics related to user actions are saturated into the assets returned. The
|
||||
# sort will affect if it will allow
|
||||
metrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Asset]
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
||||
+6
-2
@@ -6,7 +6,7 @@ const STATUSES = [
|
||||
'ACCEPTED',
|
||||
'REJECTED',
|
||||
'PREMOD',
|
||||
null
|
||||
'NONE'
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -66,7 +66,11 @@ const CommentSchema = new Schema({
|
||||
asset_id: String,
|
||||
author_id: String,
|
||||
status_history: [StatusSchema],
|
||||
status: {type: String, default: null},
|
||||
status: {
|
||||
type: String,
|
||||
enum: STATUSES,
|
||||
default: 'NONE'
|
||||
},
|
||||
tags: [TagSchema],
|
||||
parent_id: String
|
||||
}, {
|
||||
|
||||
@@ -32,6 +32,14 @@ const SettingSchema = new Schema({
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
questionBoxEnable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
questionBoxContent: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
organizationName: {
|
||||
type: String
|
||||
},
|
||||
|
||||
@@ -52,7 +52,7 @@ router.get('/', (req, res, next) => {
|
||||
if (user_id) {
|
||||
query = CommentsService.findByUserId(user_id, authorization.has(req.user, 'ADMIN'));
|
||||
} else if (status) {
|
||||
query = assetIDWrap(CommentsService.findByStatus(status === 'NEW' ? null : status));
|
||||
query = assetIDWrap(CommentsService.findByStatus(status === 'NEW' ? 'NONE' : status));
|
||||
} else if (action_type) {
|
||||
query = CommentsService
|
||||
.findIdsByActionType(action_type)
|
||||
|
||||
@@ -11,6 +11,7 @@ const STATUSES = [
|
||||
'ACCEPTED',
|
||||
'REJECTED',
|
||||
'PREMOD',
|
||||
'NONE',
|
||||
];
|
||||
|
||||
module.exports = class CommentsService {
|
||||
@@ -31,7 +32,7 @@ module.exports = class CommentsService {
|
||||
body,
|
||||
asset_id,
|
||||
parent_id,
|
||||
status = null,
|
||||
status = 'NONE',
|
||||
author_id
|
||||
} = comment;
|
||||
|
||||
@@ -146,7 +147,7 @@ module.exports = class CommentsService {
|
||||
* @param {String} status status of the comment to search for
|
||||
* @return {Promise} resovles to comment array
|
||||
*/
|
||||
static findByStatus(status = null) {
|
||||
static findByStatus(status = 'NONE') {
|
||||
return CommentModel.find({status});
|
||||
}
|
||||
|
||||
@@ -155,7 +156,7 @@ module.exports = class CommentsService {
|
||||
* @param {String} asset_id
|
||||
* @return {Promise}
|
||||
*/
|
||||
static moderationQueue(status = null, asset_id = null) {
|
||||
static moderationQueue(status = 'NONE', asset_id = null) {
|
||||
|
||||
// Fetch the comments with statuses.
|
||||
let comments = CommentModel.find({status});
|
||||
@@ -272,7 +273,7 @@ module.exports = class CommentsService {
|
||||
return Promise.reject(new Error(`status ${status} is not supported`));
|
||||
}
|
||||
|
||||
return CommentModel.update({id}, {
|
||||
return CommentModel.findOneAndUpdate({id}, {
|
||||
$set: {status}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ describe('services.CommentsService', () => {
|
||||
|
||||
expect(c2).to.not.be.null;
|
||||
expect(c2.id).to.be.uuid;
|
||||
expect(c2.status).to.be.null;
|
||||
expect(c2.status).to.be.equal('NONE');
|
||||
|
||||
expect(c3).to.not.be.null;
|
||||
expect(c3.id).to.be.uuid;
|
||||
@@ -225,7 +225,7 @@ describe('services.CommentsService', () => {
|
||||
|
||||
return CommentsService.findById(comment_id)
|
||||
.then((c) => {
|
||||
expect(c.status).to.be.null;
|
||||
expect(c.status).to.be.equal('NONE');
|
||||
|
||||
return CommentsService.pushStatus(comment_id, 'REJECTED', '123');
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user