mirror of
https://github.com/wassname/talk.git
synced 2026-07-09 19:26:31 +08:00
Merge branch 'master' into performance-enhancements
This commit is contained in:
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
||||
import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
|
||||
|
||||
import Configure from 'routes/Configure';
|
||||
import Dashboard from 'routes/Dashboard';
|
||||
import Install from 'routes/Install';
|
||||
import Stories from 'routes/Stories';
|
||||
import Community from 'routes/Community/containers/Community';
|
||||
@@ -18,7 +17,6 @@ const routes = (
|
||||
<IndexRedirect to='/admin/moderate' />
|
||||
<Route path='configure' component={Configure} />
|
||||
<Route path='stories' component={Stories} />
|
||||
<Route path='dashboard' component={Dashboard} />
|
||||
|
||||
{/* Community Routes */}
|
||||
|
||||
|
||||
@@ -12,20 +12,14 @@ const CoralDrawer = ({handleLogout, auth = {}}) => (
|
||||
{ auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
className={cn('talk-admin-nav-dashboard', styles.navLink)}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.dashboard')}
|
||||
</IndexLink>
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<Link
|
||||
<IndexLink
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
</Link>
|
||||
</IndexLink>
|
||||
)
|
||||
}
|
||||
<Link
|
||||
|
||||
@@ -23,23 +23,16 @@ const CoralHeader = ({
|
||||
{
|
||||
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
id='dashboardNav'
|
||||
className={cn('talk-admin-nav-dashboard', styles.navLink)}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.dashboard')}
|
||||
</IndexLink>
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<Link
|
||||
<IndexLink
|
||||
id='moderateNav'
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
{(root.premodCount !== 0 || root.reportedCount !== 0) && <Indicator />}
|
||||
</Link>
|
||||
</IndexLink>
|
||||
)
|
||||
}
|
||||
<Link
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// this is initialized here because
|
||||
// currently you have to reload the dashboard to get new stats
|
||||
// cleaner updates are planned in the future.
|
||||
const DASHBOARD_WINDOW_MINUTES = 5;
|
||||
let then = new Date();
|
||||
then.setMinutes(then.getMinutes() - DASHBOARD_WINDOW_MINUTES);
|
||||
|
||||
const initialState = {
|
||||
windowStart: then.toISOString(),
|
||||
windowEnd: new Date().toISOString(),
|
||||
};
|
||||
|
||||
export default function dashboard (state = initialState, _action) {
|
||||
return state;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import auth from './auth';
|
||||
import stories from './stories';
|
||||
import dashboard from './dashboard';
|
||||
import configure from './configure';
|
||||
import community from './community';
|
||||
import moderation from './moderation';
|
||||
@@ -13,7 +12,6 @@ import userDetail from './userDetail';
|
||||
export default {
|
||||
auth,
|
||||
banUserDialog,
|
||||
dashboard,
|
||||
configure,
|
||||
suspendUserDialog,
|
||||
userDetail,
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Widget.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ActivityWidget = ({assets}) => {
|
||||
return (
|
||||
<div className={styles.widget}>
|
||||
<h2 className={styles.heading}>{t('dashboard.most_conversations')}</h2>
|
||||
<div className={styles.widgetHead}>
|
||||
<p>{t('streams.article')}</p>
|
||||
<p>{t('dashboard.comment_count')}</p>
|
||||
</div>
|
||||
<div className={styles.widgetTable}>
|
||||
{
|
||||
assets.length
|
||||
? assets.map((asset) => {
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{asset.commentCount}</p>
|
||||
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</a>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{t('dashboard.no_activity')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ActivityWidget.propTypes = {
|
||||
assets: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
commentCount: PropTypes.number,
|
||||
author: PropTypes.string,
|
||||
created_at: PropTypes.string
|
||||
})).isRequired
|
||||
};
|
||||
|
||||
export default ActivityWidget;
|
||||
@@ -1,93 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Dashboard.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
const refreshIntervalSeconds = 60 * 5;
|
||||
|
||||
// TODO: refactor out storage code into redux.
|
||||
|
||||
class CountdownTimer extends React.Component {
|
||||
|
||||
static contextTypes = {
|
||||
storage: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
handleTimeout: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
const {storage} = context;
|
||||
try {
|
||||
if (storage && storage.getItem('coral:dashboardNote') === null) {
|
||||
storage.setItem('coral:dashboardNote', 'show');
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
// above will fail in Private Mode in some browsers.
|
||||
}
|
||||
|
||||
this.state = {
|
||||
secondsUntilRefresh: refreshIntervalSeconds,
|
||||
dashboardNote: (storage && storage.getItem('coral:dashboardNote')) || 'show'
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.interval = setInterval(() => { // the countdown timer
|
||||
let nextCount = this.state.secondsUntilRefresh - 1;
|
||||
if (nextCount < 0) {
|
||||
nextCount = refreshIntervalSeconds;
|
||||
return this.props.handleTimeout();
|
||||
}
|
||||
this.setState({secondsUntilRefresh: nextCount});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.clearInterval(this.interval);
|
||||
}
|
||||
|
||||
formatTime = () => {
|
||||
const minutes = Math.floor(this.state.secondsUntilRefresh / 60);
|
||||
let seconds = (this.state.secondsUntilRefresh % 60).toString();
|
||||
if (seconds.length < 2) {
|
||||
seconds = `0${seconds}`;
|
||||
}
|
||||
|
||||
return `${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
dismissNote = () => {
|
||||
const {storage} = this.context;
|
||||
try {
|
||||
if (storage) {
|
||||
storage.setItem('coral:dashboardNote', 'hide');
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
// when setItem fails in Safari Private mode
|
||||
this.setState({dashboardNote: 'hide'});
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {storage} = this.context;
|
||||
const hideReloadNote = (storage && storage.getItem('coral:dashboardNote') === 'hide') ||
|
||||
this.state.dashboardNote === 'hide'; // for Safari Incognito
|
||||
return (
|
||||
<p
|
||||
style={{display: hideReloadNote ? 'none' : 'block'}}
|
||||
className={styles.autoUpdate}
|
||||
onClick={this.dismissNote}>
|
||||
<b>×</b>
|
||||
<Icon name='timer' /> <strong>{t('dashboard.next_update', this.formatTime())}</strong> {t('dashboard.auto_update')}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CountdownTimer;
|
||||
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
|
||||
*/
|
||||
|
||||
.Dashboard {
|
||||
display: flex;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.autoUpdate {
|
||||
background-color: #d5d5d5;
|
||||
padding: 3px 10px 10px 10px;
|
||||
margin-bottom: 0;
|
||||
|
||||
i {
|
||||
position: relative;
|
||||
top: 7px;
|
||||
}
|
||||
|
||||
b {
|
||||
float: right;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
background-color: #c0c0c0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
top: 4px;
|
||||
position: relative;
|
||||
line-height: 1.7em;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import FlagWidget from './FlagWidget';
|
||||
import ActivityWidget from './ActivityWidget';
|
||||
import CountdownTimer from './CountdownTimer';
|
||||
import styles from './Dashboard.css';
|
||||
|
||||
export default ({root: {assetsByActivity, assetsByFlag}, reloadData}) => (
|
||||
<div>
|
||||
<CountdownTimer handleTimeout={reloadData} />
|
||||
<div className={styles.Dashboard}>
|
||||
<FlagWidget assets={assetsByFlag} />
|
||||
<ActivityWidget assets={assetsByActivity} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1,54 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Widget.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const FlagWidget = ({assets}) => {
|
||||
|
||||
return (
|
||||
<div className={styles.widget}>
|
||||
<h2 className={styles.heading}>{t('dashboard.most_flags')}</h2>
|
||||
<div className={styles.widgetHead}>
|
||||
<p>{t('streams.article')}</p>
|
||||
<p>{t('dashboard.flags')}</p>
|
||||
</div>
|
||||
<div className={styles.widgetTable}>
|
||||
{
|
||||
assets.length
|
||||
? assets.map((asset) => {
|
||||
let flagSummary = null;
|
||||
if (asset.action_summaries) {
|
||||
flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/reported/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
|
||||
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</a>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{t('dashboard.no_flags')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FlagWidget.propTypes = {
|
||||
assets: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
action_summaries: PropTypes.array,
|
||||
author: PropTypes.string,
|
||||
created_at: PropTypes.string
|
||||
})).isRequired
|
||||
};
|
||||
|
||||
export default FlagWidget;
|
||||
@@ -1,49 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Widget.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const LikeWidget = ({assets}) => {
|
||||
|
||||
return (
|
||||
<div className={styles.widget}>
|
||||
<h2 className={styles.heading}>Articles with the most likes</h2>
|
||||
<div className={styles.widgetHead}>
|
||||
<p>{t('streams.article')}</p>
|
||||
<p>{t('modqueue.likes')}</p>
|
||||
</div>
|
||||
<div className={styles.widgetTable}>
|
||||
{
|
||||
assets.length
|
||||
? assets.map((asset) => {
|
||||
const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary');
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
|
||||
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</a>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{t('dashboard.no_likes')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LikeWidget.propTypes = {
|
||||
assets: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
action_summaries: PropTypes.array,
|
||||
author: PropTypes.string,
|
||||
created_at: PropTypes.string
|
||||
})).isRequired
|
||||
};
|
||||
|
||||
export default LikeWidget;
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import ModerationQueue from 'coral-admin/src/containers/ModerationQueue/ModerationQueue';
|
||||
import styles from './Widget.css';
|
||||
import BanUserDialog from 'coral-admin/src/components/BanUserDialog';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const MostLikedCommentsWidget = (props) => {
|
||||
const {
|
||||
comments,
|
||||
moderation,
|
||||
settings,
|
||||
handleBanUser,
|
||||
showBanUserDialog,
|
||||
hideBanUserDialog,
|
||||
acceptComment,
|
||||
rejectComment
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.widget}>
|
||||
<h2 className={styles.heading}>{t('most_liked_comments')}</h2>
|
||||
<ModerationQueue
|
||||
comments={comments}
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
showBanUserDialog={showBanUserDialog}
|
||||
acceptComment={acceptComment}
|
||||
rejectComment={rejectComment} />
|
||||
<BanUserDialog
|
||||
open={moderation.banDialog}
|
||||
user={moderation.user}
|
||||
handleClose={hideBanUserDialog}
|
||||
handleBanUser={handleBanUser} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MostLikedCommentsWidget;
|
||||
@@ -1,110 +0,0 @@
|
||||
/**
|
||||
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--row-height: 60px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
margin: 10px 5px 5px 5px;
|
||||
box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.2);
|
||||
flex: 1;
|
||||
background-color: white;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.widget * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 0;
|
||||
padding-left: 10px;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: #2c2c2c;
|
||||
}
|
||||
|
||||
.widgetTable {
|
||||
height: calc(var(--row-height) * 10);
|
||||
}
|
||||
|
||||
.widgetTable + div:after {
|
||||
content: '';
|
||||
clear: both;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.widgetHead p {
|
||||
color: #2c2c2c;
|
||||
font-weight: 500;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
text-transform: capitalize;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.widgetHead p:last-child {
|
||||
float: right;
|
||||
margin-right: 100px;
|
||||
}
|
||||
|
||||
.rowLinkify {
|
||||
border-bottom: 1px solid lightgrey;
|
||||
color: #555;
|
||||
height: var(--row-height);
|
||||
padding: 10px;
|
||||
transition: background-color 200ms;
|
||||
}
|
||||
|
||||
.rowLinkify:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.rowLinkify:hover {
|
||||
background-color: #f8f8f8;
|
||||
pointer: default;
|
||||
}
|
||||
|
||||
.linkToAsset {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.linkToModerate {
|
||||
background-color: #BDBDBD;
|
||||
padding: 10px 14px;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
float: right;
|
||||
margin-left: 15px;
|
||||
transition: background-color 200ms;
|
||||
}
|
||||
|
||||
.linkToModerate:hover {
|
||||
background-color: #9E9E9E;
|
||||
}
|
||||
|
||||
.lede {
|
||||
font-size: 0.9em;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.assetTitle {
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
font-size: 1.2em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.widgetCount {
|
||||
color: #555;
|
||||
font-size: 1.3em;
|
||||
font-weight: 400;
|
||||
float: right;
|
||||
margin-top: 7px;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import Dashboard from '../components/Dashboard';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
|
||||
import {Spinner} from 'coral-ui';
|
||||
|
||||
class DashboardContainer extends React.Component {
|
||||
|
||||
reloadData = () => {
|
||||
this.props.data.refetch();
|
||||
}
|
||||
|
||||
render () {
|
||||
if (this.props.data.loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
return <Dashboard {...this.props} reloadData={this.reloadData} />;
|
||||
}
|
||||
}
|
||||
|
||||
export const witDashboardQuery = withQuery(gql`
|
||||
query CoralAdmin_Dashboard($from: Date!, $to: Date!) {
|
||||
assetsByFlag: assetMetrics(from: $from, to: $to, sortBy: FLAG) {
|
||||
...CoralAdmin_Metrics
|
||||
}
|
||||
assetsByActivity: assetMetrics(from: $from, to: $to, sortBy: ACTIVITY) {
|
||||
...CoralAdmin_Metrics
|
||||
}
|
||||
}
|
||||
fragment CoralAdmin_Metrics on Asset {
|
||||
id
|
||||
title
|
||||
url
|
||||
author
|
||||
created_at
|
||||
commentCount
|
||||
action_summaries {
|
||||
actionCount
|
||||
actionableItemCount
|
||||
}
|
||||
}
|
||||
`, {
|
||||
options: ({windowStart, windowEnd}) => {
|
||||
return {
|
||||
variables: {
|
||||
from: windowStart,
|
||||
to: windowEnd,
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
windowStart: state.dashboard.windowStart,
|
||||
windowEnd: state.dashboard.windowEnd,
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps),
|
||||
witDashboardQuery,
|
||||
)(DashboardContainer);
|
||||
@@ -1 +0,0 @@
|
||||
export {default} from './containers/Dashboard.js';
|
||||
@@ -8,13 +8,6 @@ permalink: /moderator-features/
|
||||
The Admin is your moderators will moderate your comments, and your Admins will
|
||||
configure and manage the different parts of Talk.
|
||||
|
||||
### Dashboard
|
||||
|
||||
The Dashboard provides real-time information to moderators so they know at a
|
||||
glance where they should direct their attention. It shows what articles are
|
||||
receiving the most reports and where the most active conversations are
|
||||
happening.
|
||||
|
||||
### Moderate
|
||||
|
||||
This is the tab where Moderators will spend the majority of their time. They can
|
||||
|
||||
@@ -79,11 +79,6 @@ const findOrCreateAssetByURL = async (context, asset_url) => {
|
||||
return asset;
|
||||
};
|
||||
|
||||
const getAssetsForMetrics = async ({loaders: {Comments}}) => {
|
||||
return Comments.getByQuery({action_type: 'FLAG'})
|
||||
.then((connection) => connection.nodes);
|
||||
};
|
||||
|
||||
const findByUrl = async (context, asset_url) => {
|
||||
|
||||
// Verify that the asset_url is parsable.
|
||||
@@ -111,7 +106,6 @@ module.exports = (context) => ({
|
||||
findByUrl: (url) => findByUrl(context, url),
|
||||
getByQuery: (query) => getAssetsByQuery(context, query),
|
||||
getByID: new DataLoader((ids) => genAssetsByID(context, ids)),
|
||||
getForMetrics: () => getAssetsForMetrics(context),
|
||||
getAll: new util.SingletonResolver(() => AssetModel.find({}))
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ const debug = require('debug')('talk:graph:loaders');
|
||||
const Actions = require('./actions');
|
||||
const Assets = require('./assets');
|
||||
const Comments = require('./comments');
|
||||
const Metrics = require('./metrics');
|
||||
const Settings = require('./settings');
|
||||
const Tags = require('./tags');
|
||||
const Users = require('./users');
|
||||
@@ -17,7 +16,6 @@ let loaders = [
|
||||
Actions,
|
||||
Assets,
|
||||
Comments,
|
||||
Metrics,
|
||||
Settings,
|
||||
Tags,
|
||||
Users,
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const DataLoader = require('dataloader');
|
||||
const {objectCacheKeyFn} = require('./util');
|
||||
|
||||
const ActionModel = require('../../models/action');
|
||||
const CommentModel = require('../../models/comment');
|
||||
|
||||
/**
|
||||
* Returns the assets which have had comments made within the last time period.
|
||||
*/
|
||||
const getAssetActivityMetrics = ({loaders: {Assets}}, {from, to, limit}) => {
|
||||
let assetMetrics = [];
|
||||
|
||||
return CommentModel.aggregate([
|
||||
{$match: {
|
||||
parent_id: null,
|
||||
created_at: {
|
||||
$gt: from,
|
||||
$lt: to
|
||||
}
|
||||
}},
|
||||
{$group: {
|
||||
_id: '$asset_id',
|
||||
commentCount: {
|
||||
$sum: 1
|
||||
}
|
||||
}},
|
||||
{$project: {
|
||||
_id: false,
|
||||
asset_id: '$_id',
|
||||
commentCount: '$commentCount'
|
||||
}},
|
||||
{$sort: {
|
||||
commentCount: -1
|
||||
}},
|
||||
{$limit: limit}
|
||||
])
|
||||
.then((results) => {
|
||||
assetMetrics = results;
|
||||
|
||||
return Assets.getByID.loadMany(results.map((result) => result.asset_id));
|
||||
})
|
||||
.then((assets) => assets.map((asset, i) => {
|
||||
|
||||
// We're leveraging the fact that the comments returned by the aggregation
|
||||
// query are in the request order that we just made, it's what the
|
||||
// Assets.getByID loader does.
|
||||
asset.commentCount = assetMetrics[i].commentCount;
|
||||
|
||||
return asset;
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a list of assets with action metadata included on the models.
|
||||
*/
|
||||
const getAssetMetrics = async ({loaders: {Metrics, Assets, Comments}}, {from, to, sortBy, limit}) => {
|
||||
|
||||
// Get the recent actions.
|
||||
let actionSummaries = await Metrics.getRecentActions.load({from, to});
|
||||
|
||||
let commentMetrics = actionSummaries.reduce((acc, {item_id, action_type, count}) => {
|
||||
if (action_type !== sortBy) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
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 = Object.keys(commentMetrics);
|
||||
|
||||
// Find those comments.
|
||||
let comments = await Comments.get.loadMany(commentIDs);
|
||||
|
||||
let commentResults = _.groupBy(comments, 'asset_id');
|
||||
|
||||
let 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};
|
||||
})
|
||||
|
||||
.filter((asset) => {
|
||||
let contextActionSummary = asset.action_summaries.find((({action_type}) => action_type === sortBy));
|
||||
if (contextActionSummary === null || contextActionSummary.actionCount === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
|
||||
// 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.
|
||||
.sort((a, b) => {
|
||||
let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sortBy));
|
||||
let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sortBy));
|
||||
|
||||
// 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.
|
||||
let assets = await Assets.getByID.loadMany(assetMetrics.map((asset) => asset.id));
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a list of comments that are retrieved based on most activity within
|
||||
* the indicated time range.
|
||||
*/
|
||||
const getCommentMetrics = async ({loaders: {Metrics, Comments}}, {from, to, sortBy, limit}) => {
|
||||
|
||||
let commentActionSummaries = {};
|
||||
|
||||
let actionSummaries = await Metrics.getRecentActions.load({from, to});
|
||||
|
||||
actionSummaries.sort((a, b) => {
|
||||
let aActionSummary = a.action_type === sortBy ? a : null;
|
||||
let bActionSummary = b.action_type === sortBy ? b : null;
|
||||
|
||||
// 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.count - aActionSummary.count;
|
||||
});
|
||||
|
||||
commentActionSummaries = _.groupBy(actionSummaries, 'item_id');
|
||||
|
||||
// Grab the comment id's for comment where they have at least one of the
|
||||
// actions being sorted by.
|
||||
let commentIDs = Object.keys(commentActionSummaries).filter((item_id) => {
|
||||
let contextActionSummary = commentActionSummaries[item_id].find(({action_type}) => action_type === sortBy);
|
||||
if (contextActionSummary == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Only keep the top `limit`.
|
||||
commentIDs = commentIDs.slice(0, limit);
|
||||
|
||||
// If there are no comment's to get, then just continue with an empty
|
||||
// array.
|
||||
if (commentIDs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Find those comments, this is the final stage, so let's get all the
|
||||
// fields.
|
||||
let comments = await Comments.get.loadMany(commentIDs);
|
||||
|
||||
return comments.map((comment) => {
|
||||
|
||||
// Add in the action summaries genrerated.
|
||||
comment.action_summaries = commentActionSummaries[comment.id];
|
||||
|
||||
return comment;
|
||||
});
|
||||
};
|
||||
|
||||
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'
|
||||
}}
|
||||
]);
|
||||
};
|
||||
|
||||
module.exports = (context) => ({
|
||||
Metrics: {
|
||||
getRecentActions: new DataLoader(([{from, to}]) => getRecentActions(context, {from, to}).then((as) => [as]), {
|
||||
batch: false,
|
||||
cacheKeyFn: objectCacheKeyFn('from', 'to')
|
||||
}),
|
||||
Assets: {
|
||||
get: ({from, to, sortBy, limit}) => getAssetMetrics(context, {from, to, sortBy, limit}),
|
||||
getActivity: ({from, to, limit}) => getAssetActivityMetrics(context, {from, to, limit}),
|
||||
},
|
||||
Comments: {
|
||||
get: ({from, to, sortBy, limit}) => getCommentMetrics(context, {from, to, sortBy, limit}),
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
const {
|
||||
SEARCH_ASSETS,
|
||||
SEARCH_OTHERS_COMMENTS,
|
||||
SEARCH_COMMENT_METRICS,
|
||||
SEARCH_OTHER_USERS
|
||||
} = require('../../perms/constants');
|
||||
|
||||
@@ -58,27 +57,6 @@ const RootQuery = {
|
||||
return Users.getCountByQuery(query);
|
||||
},
|
||||
|
||||
assetMetrics(_, query, {user, loaders: {Metrics: {Assets}}}) {
|
||||
if (user == null || !user.can(SEARCH_ASSETS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {sortBy} = query;
|
||||
if (sortBy === 'ACTIVITY') {
|
||||
return Assets.getActivity(query);
|
||||
}
|
||||
|
||||
return Assets.get(query);
|
||||
},
|
||||
|
||||
commentMetrics(_, query, {user, loaders: {Metrics: {Comments}}}) {
|
||||
if (user == null || !user.can(SEARCH_COMMENT_METRICS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Comments.get(query);
|
||||
},
|
||||
|
||||
// This returns the current user, ensure that if we aren't logged in, we
|
||||
// return null.
|
||||
me(_, args, {user}) {
|
||||
|
||||
@@ -5,7 +5,6 @@ const {
|
||||
SEARCH_OTHER_USERS,
|
||||
SEARCH_OTHERS_COMMENTS,
|
||||
UPDATE_USER_ROLES,
|
||||
SEARCH_COMMENT_METRICS,
|
||||
VIEW_SUSPENSION_INFO,
|
||||
LIST_OWN_TOKENS
|
||||
} = require('../../perms/constants');
|
||||
@@ -79,7 +78,7 @@ const User = {
|
||||
|
||||
// Extract the reliability from the user metadata if they have permission.
|
||||
reliable(user, _, {user: requestingUser}) {
|
||||
if (requestingUser && requestingUser.can(SEARCH_COMMENT_METRICS)) {
|
||||
if (requestingUser && requestingUser.can(SEARCH_ACTIONS)) {
|
||||
return KarmaService.model(user);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -818,19 +818,6 @@ enum USER_STATUS {
|
||||
APPROVED
|
||||
}
|
||||
|
||||
# Metrics for the assets.
|
||||
enum ASSET_METRICS_SORT {
|
||||
|
||||
# Represents a FlagAction.
|
||||
FLAG
|
||||
|
||||
# Represents a don't agree action.
|
||||
DONTAGREE
|
||||
|
||||
# Represents activity.
|
||||
ACTIVITY
|
||||
}
|
||||
|
||||
type RootQuery {
|
||||
|
||||
# Site wide settings and defaults.
|
||||
@@ -865,14 +852,6 @@ type RootQuery {
|
||||
|
||||
# a single User by id
|
||||
user(id: ID!): User
|
||||
|
||||
# Asset metrics related to user actions are saturated into the assets
|
||||
# returned. Parameters `from` and `to` are related to the action created_at field.
|
||||
assetMetrics(from: Date!, to: Date!, sortBy: ASSET_METRICS_SORT!, limit: Int = 10): [Asset!]
|
||||
|
||||
# Comment metrics related to user actions are saturated into the comments
|
||||
# returned. Parameters `from` and `to` are related to the action created_at field.
|
||||
commentMetrics(from: Date!, to: Date!, sortBy: ACTION_TYPE!, limit: Int = 10): [Comment!]
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
||||
@@ -98,7 +98,6 @@ da:
|
||||
copy_and_paste: "Kopier og indsæt kode nedenfor i dit CMS system for at indlejre din kommentarboks i dine artiker."
|
||||
custom_css_url: "Brugerdefineret CSS URL"
|
||||
custom_css_url_desc: "URL til et CSS stylesheet der tilsidesætter den alimdelige strøms stilart. Kan være internt eller externt"
|
||||
dashboard: "Instrumentbræt"
|
||||
days: "Dage"
|
||||
description: "Som administrator kan du tilpasse indstillingerne til kommentarstrømmen for denne historie:"
|
||||
domain_list_text: "Indtast de domæner du gerne vil tillade til Talk. f.eks. Dit lokale udviklings miljø (fx. localhost:3000 staging.domain.com domain.com)."
|
||||
@@ -153,16 +152,6 @@ da:
|
||||
username: "Brugernavn"
|
||||
write_your_username: "Rediger dit brugernavn"
|
||||
your_username: "Dit brugernavn vises på hver kommentar du sender."
|
||||
dashboard:
|
||||
auto_update: "Data opdateres automatisk hvert femte minut, eller når du genindlæser."
|
||||
comment_count: "kommentarer"
|
||||
flags: "Flags"
|
||||
most_flags: "Historier med de fleste flags"
|
||||
most_conversations: "Historier med de fleste samtaler"
|
||||
next_update: "{0} minutter indtil næste opdatering."
|
||||
no_activity: "Der har ikke været nogen kommentarer i løbet af de sidste fem minutter."
|
||||
no_flags: "Der har ikke været nogle flag i de sidste 5 minutter! Hurra!"
|
||||
no_likes: "Der har ikke været nogen der syntes godt om noget i de sidste 5 minutter. Helt stille"
|
||||
done: "Færdig"
|
||||
edit_comment:
|
||||
body_input_label: "Rediger denne kommentar"
|
||||
|
||||
@@ -100,7 +100,6 @@ en:
|
||||
copy_and_paste: "Copy and paste code below into your CMS to embed your comment box in your articles"
|
||||
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
|
||||
days: Days
|
||||
description: "As an admin, you can customize the settings for the comment stream for this story:"
|
||||
domain_list_text: "Enter the domains you would like to permit for Talk e.g. your local staging and production environments (ex. localhost:3000 staging.domain.com domain.com)."
|
||||
@@ -155,16 +154,6 @@ en:
|
||||
username: Username
|
||||
write_your_username: "Edit your username"
|
||||
your_username: "Your username appears on every comment you post."
|
||||
dashboard:
|
||||
auto_update: "Data automatically updates every five minutes or when you Reload."
|
||||
comment_count: comments
|
||||
flags: Flags
|
||||
most_flags: "Stories with the most flags"
|
||||
most_conversations: "Stories with the most conversations"
|
||||
next_update: "{0} minutes until next update."
|
||||
no_activity: "There haven't been any comments anywhere in the last five minutes."
|
||||
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."
|
||||
done: Done
|
||||
edit_comment:
|
||||
body_input_label: "Edit this comment"
|
||||
|
||||
@@ -98,7 +98,6 @@ es:
|
||||
copy_and_paste: "Copiar y pegar el código de más abajo en tu CMS para colocar la caja de comentarios en tus artículos"
|
||||
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 del hilo de comentarios. Puede ser interna o externa."
|
||||
dashboard: Panel
|
||||
days: Días
|
||||
description: "Como Administrador/a puedes modificar la configuración de los comentarios en este artículo"
|
||||
domain_list_text: "Agrega dominios permitidos a Talk, por ejemplo tu localhost, staging y ambientes de producción (ej. localhost:3000, staging.domain.com, domain.com)."
|
||||
@@ -153,16 +152,6 @@ es:
|
||||
username: "Nombre"
|
||||
write_your_username: "Edita tu nombre"
|
||||
your_username: "Tu nombre aparece en cada comentario que publiques."
|
||||
dashboard:
|
||||
auto_update: "Los datos se actualizan automáticamente cada cinco minutos o cuando refresca el navegador."
|
||||
comment_count: "comentarios"
|
||||
flags: "Reportes"
|
||||
most_flags: "Artículos con la mayor cantidad de reportes"
|
||||
most_conversations: "Artículos con las mayores conversaciones"
|
||||
next_update: "{0} minutos hasta la próxima actualización."
|
||||
no_activity: "¡No han habido comentarios en ningún lado en los últimos 5 minutos."
|
||||
no_flags: "¡No ha habido ningún reporte en los últimos 5 minutos! Bravo!"
|
||||
no_likes: "¡No ha habido ningún 'me gusta' en los últimos 5 minutos. Todo tranquilo."
|
||||
done: "Hecho"
|
||||
edit_comment:
|
||||
body_input_label: "Editar este comentario"
|
||||
|
||||
@@ -79,7 +79,6 @@ fr:
|
||||
copy_and_paste: "Copiez et collez le code ci-dessous dans votre CMS pour intégrer votre boîte de commentaires dans vos articles."
|
||||
custom_css_url: "URL CSS personnalisée"
|
||||
custom_css_url_desc: "URL d'une feuille de style CSS qui remplacera les styles par défaut d'intégration des commentaires. Peut être interne ou externe."
|
||||
dashboard: Tableau de bord
|
||||
days: Journées
|
||||
description: "En tant qu'administrateur, vous pouvez personnaliser les paramètres du fil de commentaires pour cet élément."
|
||||
domain_list_text: "Entrez les domaines que vous souhaitez autoriser pour Talk, par exemple vos environnements locaux de production et de production (ex : localhost: 3000 staging.domain.com domain.com)."
|
||||
@@ -121,16 +120,6 @@ fr:
|
||||
weeks: Semaines
|
||||
wordlist: "Mots interdits"
|
||||
continue: Continuer
|
||||
dashboard:
|
||||
auto_update: "Les données sont automatiquement mises à jour toutes les cinq minutes ou lorsque vous rechargez."
|
||||
comment_count: commentaires
|
||||
flags: signalements
|
||||
most_flags: "Articles avec le plus de signalements"
|
||||
most_conversations: "Articles avec le plus de conversations"
|
||||
next_update: "{0} minutes jusqu'à la prochaine mise à jour."
|
||||
no_activity: "Il n'y a eu aucun commentaire nulle part dans les cinq dernières minutes."
|
||||
no_flags: "Il n'y a pas eu aucun signalement au cours des 5 dernières minutes! Hooray !"
|
||||
no_likes: "Il n'y a pas eu de \"J'aime\" au cours des 5 dernières minutes. Tout est calme."
|
||||
done: Terminé
|
||||
edit_comment:
|
||||
body_input_label: "Modifier ce commentaire"
|
||||
|
||||
@@ -98,7 +98,6 @@ pt_BR:
|
||||
copy_and_paste: "Copie e cole o código abaixo em seu CMS para incorporar sua caixa de comentários em seus artigos."
|
||||
custom_css_url: "URL para CSS customizado"
|
||||
custom_css_url_desc: "URL de uma folha de estilo CSS para substituir os estilos padrão dos comentários embutidos. Pode ser interno ou externo."
|
||||
dashboard: Painel de controle
|
||||
days: Dias
|
||||
description: "Como administrador, você pode personalizar as configurações da lista de comentários para esta história:"
|
||||
domain_list_text: "Insira os domínios que você gostaria de permitir para o Talk, como seus ambientes de desenvolvimento, teste ou produção (ej. localhost:3000 staging.domain.com domain.com)."
|
||||
@@ -153,16 +152,6 @@ pt_BR:
|
||||
username: Nome de usuário
|
||||
write_your_username: "Edite seu nome de usuário"
|
||||
your_username: "Nosso nome de usuário aparece em todos os comentários que você publica."
|
||||
dashboard:
|
||||
auto_update: "Atualizado autimaticamente a cada minutos ou após recarregamento."
|
||||
comment_count: comentários
|
||||
flags: Marcações
|
||||
most_flags: "Conversas com mais comentários marcados"
|
||||
most_conversations: "Conversas com mais comentários"
|
||||
next_update: "{0} minutos até a próxima atualização."
|
||||
no_activity: "Não houve nenhum comentário nos últimos cinco minutos."
|
||||
no_flags: "Não houve nenhum comentário marcado em nenhum lugar nos últimos 5 minutos. Hip Hip Uha!"
|
||||
no_likes: "Não houve reações nos últimos 5 minutos."
|
||||
done: Feito
|
||||
edit_comment:
|
||||
body_input_label: "Edite este comentário"
|
||||
|
||||
@@ -26,7 +26,6 @@ module.exports = {
|
||||
SEARCH_ACTIONS: 'SEARCH_ACTIONS',
|
||||
SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS: 'SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS',
|
||||
SEARCH_OTHERS_COMMENTS: 'SEARCH_OTHERS_COMMENTS',
|
||||
SEARCH_COMMENT_METRICS: 'SEARCH_COMMENT_METRICS',
|
||||
LIST_OWN_TOKENS: 'LIST_OWN_TOKENS',
|
||||
SEARCH_COMMENT_STATUS_HISTORY: 'SEARCH_COMMENT_STATUS_HISTORY',
|
||||
VIEW_SUSPENSION_INFO: 'VIEW_SUSPENSION_INFO',
|
||||
|
||||
@@ -13,8 +13,6 @@ module.exports = (user, perm) => {
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SEARCH_OTHERS_COMMENTS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SEARCH_COMMENT_METRICS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.LIST_OWN_TOKENS:
|
||||
return check(user, ['ADMIN']);
|
||||
case types.SEARCH_COMMENT_STATUS_HISTORY:
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const UserModel = require('../../../../models/user');
|
||||
const AssetModel = require('../../../../models/asset');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const ActionModel = require('../../../../models/action');
|
||||
const CommentModel = require('../../../../models/comment');
|
||||
|
||||
const {expect} = require('chai');
|
||||
|
||||
describe('graph.loaders.Metrics', () => {
|
||||
beforeEach(() => SettingsService.init());
|
||||
|
||||
describe('#Comments', () => {
|
||||
const query = `
|
||||
query CommentMetrics($from: Date!, $to: Date!) {
|
||||
flagged: commentMetrics(from: $from, to: $to, sortBy: FLAG) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
describe('different comment states', () => {
|
||||
|
||||
beforeEach(() => CommentModel.create([
|
||||
{id: '1', body: 'a new comment!'},
|
||||
{id: '2', body: 'a new comment!'},
|
||||
{id: '3', body: 'a new comment!'}
|
||||
]));
|
||||
|
||||
[
|
||||
{flagged: 0, actions: []},
|
||||
{flagged: 1, actions: [{action_type: 'FLAG', item_id: '1', item_type: 'COMMENTS'}]},
|
||||
{flagged: 1, actions: [
|
||||
{action_type: 'FLAG', item_id: '1', item_type: 'COMMENTS'},
|
||||
]},
|
||||
{flagged: 1, actions: [
|
||||
{action_type: 'FLAG', item_id: '3', item_type: 'COMMENTS'}
|
||||
]}
|
||||
].forEach(({flagged, actions}) => {
|
||||
|
||||
describe(`with actions=${actions.length}`, () => {
|
||||
|
||||
beforeEach(() => ActionModel.create(actions));
|
||||
|
||||
it(`returns the correct amount of metrics flagged=${flagged}`, () => {
|
||||
const context = new Context({user: new UserModel({roles: ['ADMIN']})});
|
||||
|
||||
return graphql(schema, query, {}, context, {
|
||||
from: (new Date()).setMinutes((new Date()).getMinutes() - 5),
|
||||
to: (new Date()).setMinutes((new Date()).getMinutes() + 5)
|
||||
})
|
||||
.then(({data, errors}) => {
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.flagged).to.have.length(flagged);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('#Assets', () => {
|
||||
const query = `
|
||||
fragment metrics on Asset {
|
||||
id
|
||||
action_summaries {
|
||||
actionCount
|
||||
actionableItemCount
|
||||
}
|
||||
}
|
||||
|
||||
query Metrics($from: Date!, $to: Date!) {
|
||||
assetsByFlag: assetMetrics(from: $from, to: $to, sortBy: FLAG) {
|
||||
...metrics
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
describe('different comment states', () => {
|
||||
|
||||
beforeEach(() => Promise.all([
|
||||
AssetModel.create([
|
||||
{id: 'a1', url: 'http://localhost:3030/article/1'},
|
||||
{id: 'a2', url: 'http://localhost:3030/article/2'}
|
||||
]),
|
||||
CommentModel.create([
|
||||
{id: 'c1', asset_id: 'a1', body: 'a new comment!'},
|
||||
{id: 'c2', asset_id: 'a1', body: 'a new comment!'},
|
||||
{id: 'c3', asset_id: 'a1', body: 'a new comment!'}
|
||||
])
|
||||
]));
|
||||
|
||||
[
|
||||
{flagged: 0, actions: []},
|
||||
{flagged: 1, actions: [{action_type: 'FLAG', item_id: 'c1', item_type: 'COMMENTS'}]},
|
||||
{flagged: 1, actions: [
|
||||
{action_type: 'FLAG', item_id: 'c1', item_type: 'COMMENTS'},
|
||||
]},
|
||||
{flagged: 1, actions: [
|
||||
{action_type: 'FLAG', item_id: 'c3', item_type: 'COMMENTS'}
|
||||
]}
|
||||
].forEach(({flagged, actions}) => {
|
||||
|
||||
describe(`with actions=${actions.length}`, () => {
|
||||
|
||||
beforeEach(() => ActionModel.create(actions));
|
||||
|
||||
it(`returns the correct amount of metrics flagged=${flagged}`, () => {
|
||||
const context = new Context({user: new UserModel({roles: ['ADMIN']})});
|
||||
|
||||
return graphql(schema, query, {}, context, {
|
||||
from: (new Date()).setMinutes((new Date()).getMinutes() - 5),
|
||||
to: (new Date()).setMinutes((new Date()).getMinutes() + 5)
|
||||
})
|
||||
.then(({data, errors}) => {
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.assetsByFlag).to.have.length(flagged);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user