diff --git a/client/.eslintrc.json b/client/.eslintrc.json
index a935eaec6..1144f985d 100644
--- a/client/.eslintrc.json
+++ b/client/.eslintrc.json
@@ -18,6 +18,7 @@
],
"rules": {
"react/jsx-uses-react": "error",
- "react/jsx-uses-vars": "error"
+ "react/jsx-uses-vars": "error",
+ "no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js
index d96bacfe0..38d06dcb1 100644
--- a/client/coral-admin/src/AppRouter.js
+++ b/client/coral-admin/src/AppRouter.js
@@ -15,7 +15,7 @@ const routes = (
-
+
diff --git a/client/coral-admin/src/actions/install.js b/client/coral-admin/src/actions/install.js
index c1d05d253..ceecf16c9 100644
--- a/client/coral-admin/src/actions/install.js
+++ b/client/coral-admin/src/actions/install.js
@@ -20,13 +20,17 @@ const validation = (formData, dispatch, next) => {
return dispatch(hasError());
}
+ const validKeys = Object.keys(formData)
+ .filter(name => name !== 'domains');
+
// Required Validation
- const empty = Object.keys(formData).filter(name => {
+ const empty = validKeys
+ .filter(name => {
const cond = !formData[name].length;
if (cond) {
- // Adding Error
+ // Adding Error
dispatch(addError(name, 'This field is required.'));
} else {
dispatch(addError(name, ''));
@@ -40,18 +44,19 @@ const validation = (formData, dispatch, next) => {
}
// RegExp Validation
- const validation = Object.keys(formData).filter(name => {
- const cond = !validate[name](formData[name]);
- if (cond) {
+ const validation = validKeys
+ .filter(name => {
+ const cond = !validate[name](formData[name]);
+ if (cond) {
// Adding Error
- dispatch(addError(name, errorMsj[name]));
- } else {
- dispatch(addError(name, ''));
- }
+ dispatch(addError(name, errorMsj[name]));
+ } else {
+ dispatch(addError(name, ''));
+ }
- return cond;
- });
+ return cond;
+ });
if (validation.length) {
return dispatch(hasError());
@@ -71,23 +76,27 @@ export const submitSettings = () => (dispatch, getState) => {
export const submitUser = () => (dispatch, getState) => {
const userFormData = getState().install.toJS().data.user;
validation(userFormData, dispatch, function() {
- const data = getState().install.toJS().data;
- dispatch(installRequest());
- coralApi('/setup', {method: 'POST', body: data})
- .then(result => {
- console.log(result);
- dispatch(installSuccess());
- dispatch(nextStep());
- })
- .catch(error => {
- console.error(error);
- dispatch(installFailure(`${error.translation_key}`));
- });
+ dispatch(nextStep());
});
};
+export const finishInstall = () => (dispatch, getState) => {
+ const data = getState().install.toJS().data;
+ dispatch(installRequest());
+ return coralApi('/setup', {method: 'POST', body: data})
+ .then(() => {
+ dispatch(installSuccess());
+ dispatch(nextStep());
+ })
+ .catch(error => {
+ console.error(error);
+ dispatch(installFailure(`${error.translation_key}`));
+ });
+};
+
export const updateSettingsFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_SETTINGS, name, value});
export const updateUserFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_USER, name, value});
+export const updatePermittedDomains = (value) => ({type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS, value});
const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
const checkInstallSuccess = installed => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
diff --git a/client/coral-admin/src/components/FlagWidget.js b/client/coral-admin/src/components/FlagWidget.js
deleted file mode 100644
index 187306c8c..000000000
--- a/client/coral-admin/src/components/FlagWidget.js
+++ /dev/null
@@ -1,66 +0,0 @@
-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 (
-
-
-
- {/* empty on purpose */}
- {lang.t('streams.article')}
- {lang.t('modqueue.flagged')}
- {lang.t('modqueue.likes')}
- {lang.t('dashboard.comment_count')}
-
-
-
- {
- assets.length
- ? assets.map((asset, index) => {
- const flagSummary = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary');
- const likeSummary = asset.action_summaries.find(s => s.__typename === 'LikeAssetActionSummary');
- return (
-
- {index + 1}.
-
- {asset.title}
- {asset.author} - Published: {new Date(asset.created_at).toLocaleDateString()}
-
- {flagSummary ? flagSummary.actionCount : 0}
- {likeSummary ? likeSummary.actionCount : 0}
- {asset.commentCount}
-
- );
- })
- : {lang.t('dashboard.no_flags')}
- }
-
-
- );
-};
-
-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;
diff --git a/client/coral-admin/src/constants/install.js b/client/coral-admin/src/constants/install.js
index b3fd7e0d0..743d68593 100644
--- a/client/coral-admin/src/constants/install.js
+++ b/client/coral-admin/src/constants/install.js
@@ -10,6 +10,7 @@ export const INSTALL_SUCCESS = 'INSTALL_SUCCESS';
export const INSTALL_FAILURE = 'INSTALL_FAILURE';
export const UPDATE_FORMDATA_USER = 'UPDATE_FORMDATA_USER';
export const UPDATE_FORMDATA_SETTINGS = 'UPDATE_FORMDATA_SETTINGS';
+export const UPDATE_PERMITTED_DOMAINS_SETTINGS = 'UPDATE_PERMITTED_DOMAINS_SETTINGS';
export const CHECK_INSTALL_REQUEST = 'CHECK_INSTALL_REQUEST';
export const CHECK_INSTALL_SUCCESS = 'CHECK_INSTALL_SUCCESS';
diff --git a/client/coral-admin/src/containers/Configure/Domainlist.js b/client/coral-admin/src/containers/Configure/Domainlist.js
index f845dfeaa..6918c0471 100644
--- a/client/coral-admin/src/containers/Configure/Domainlist.js
+++ b/client/coral-admin/src/containers/Configure/Domainlist.js
@@ -1,26 +1,28 @@
import React from 'react';
+import {Card} from 'coral-ui';
+import styles from './Configure.css';
+import TagsInput from 'react-tagsinput';
+
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
-import TagsInput from 'react-tagsinput';
-import styles from './Configure.css';
-import {Card} from 'coral-ui';
+const lang = new I18n(translations);
-const Domainlist = ({domains, onChangeDomainlist}) => (
-
-
{lang.t('configure.domain-list-title')}
-
- {lang.t('configure.domain-list-text')}
- data.split(',').map(d => d.trim())}
- onChange={tags => onChangeDomainlist('whitelist', tags)}
- />
-
-
-);
+const Domainlist = ({domains, onChangeDomainlist}) => {
+ return (
+
+
{lang.t('configure.domain-list-title')}
+
+ {lang.t('configure.domain-list-text')}
+ data.split(',').map(d => d.trim())}
+ onChange={tags => onChangeDomainlist('whitelist', tags)}
+ />
+
+
+ );
+};
export default Domainlist;
-
-const lang = new I18n(translations);
diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.css b/client/coral-admin/src/containers/Dashboard/Dashboard.css
index be2cd9f7a..72b9bd90c 100644
--- a/client/coral-admin/src/containers/Dashboard/Dashboard.css
+++ b/client/coral-admin/src/containers/Dashboard/Dashboard.css
@@ -1,19 +1,5 @@
.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 {
diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.js b/client/coral-admin/src/containers/Dashboard/Dashboard.js
index 6baa16af1..96e837a9e 100644
--- a/client/coral-admin/src/containers/Dashboard/Dashboard.js
+++ b/client/coral-admin/src/containers/Dashboard/Dashboard.js
@@ -1,38 +1,46 @@
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';
+import {compose} from 'react-apollo';
+import {connect} from 'react-redux';
+import {getMetrics} from 'coral-admin/src/graphql/queries';
+import FlagWidget from './FlagWidget';
+import LikeWidget from './LikeWidget';
+import {showBanUserDialog, hideBanUserDialog} from 'coral-admin/src/actions/moderation';
+
+import {Spinner} from 'coral-ui';
class Dashboard extends React.Component {
+
render () {
- const {data} = this.props;
- const {metrics: assets} = data;
-
- if (data.loading) {
+ if (this.props.data && this.props.data.loading) {
return ;
}
- if (data.error) {
- return {data.error} ;
- }
+ const {data: {assetsByLike, assetsByFlag}} = this.props;
return (
-
-
Top Ten Articles with the most flagged comments
-
-
-
-
Top ten comments with the most likes
-
+
+
);
}
}
+const mapStateToProps = state => {
+ return {
+ settings: state.settings.toJS(),
+ moderation: state.moderation.toJS()
+ };
+};
+
+const mapDispatchToProps = dispatch => ({
+ showBanUserDialog: (user, commentId) => dispatch(showBanUserDialog(user, commentId)),
+ hideBanUserDialog: () => dispatch(hideBanUserDialog(false))
+});
+
export default compose(
- mostFlags
+ connect(mapStateToProps, mapDispatchToProps),
+ getMetrics
)(Dashboard);
diff --git a/client/coral-admin/src/containers/Dashboard/FlagWidget.js b/client/coral-admin/src/containers/Dashboard/FlagWidget.js
new file mode 100644
index 000000000..537b89b8f
--- /dev/null
+++ b/client/coral-admin/src/containers/Dashboard/FlagWidget.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import {Link} from 'react-router';
+import styles from './Widget.css';
+import I18n from 'coral-framework/modules/i18n/i18n';
+import translations from 'coral-admin/src/translations';
+
+const lang = new I18n(translations);
+
+const FlagWidget = (props) => {
+ const {assets} = props;
+
+ return (
+
+
Articles with the most flags
+
+
+
+ {/* empty on purpose */}
+ {lang.t('streams.article')}
+ {lang.t('modqueue.flagged')}
+
+
+
+ {
+ assets.length
+ ? assets.map((asset, index) => {
+ const flagSummary = asset.action_summaries.find(s => s.type === 'FlagAssetActionSummary');
+ return (
+
+ {index + 1}.
+
+ {asset.title}
+ {asset.author} - Published: {new Date(asset.created_at).toLocaleDateString()}
+
+ {flagSummary ? flagSummary.actionCount : 0}
+
+ );
+ })
+ : {lang.t('dashboard.no_flags')}
+ }
+
+
+
+ );
+};
+
+export default FlagWidget;
diff --git a/client/coral-admin/src/containers/Dashboard/LikeWidget.js b/client/coral-admin/src/containers/Dashboard/LikeWidget.js
new file mode 100644
index 000000000..ed6a0c0ae
--- /dev/null
+++ b/client/coral-admin/src/containers/Dashboard/LikeWidget.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import {Link} from 'react-router';
+import styles from './Widget.css';
+import I18n from 'coral-framework/modules/i18n/i18n';
+import translations from 'coral-admin/src/translations';
+
+const lang = new I18n(translations);
+
+const LikeWidget = (props) => {
+
+ const {assets} = props;
+
+ return (
+
+
Articles with the most likes
+
+
+
+ {/* empty on purpose */}
+ {lang.t('streams.article')}
+ {lang.t('modqueue.likes')}
+
+
+
+ {
+ assets.length
+ ? assets.map((asset, index) => {
+ const likeSummary = asset.action_summaries.find(s => s.type === 'LikeAssetActionSummary');
+ return (
+
+ {index + 1}.
+
+ {asset.title}
+ {asset.author} - Published: {new Date(asset.created_at).toLocaleDateString()}
+
+ {likeSummary ? likeSummary.actionCount : 0}
+
+ );
+ })
+ : {lang.t('dashboard.no_likes')}
+ }
+
+
+
+ );
+};
+
+export default LikeWidget;
diff --git a/client/coral-admin/src/containers/Dashboard/MostLikedCommentsWidget.js b/client/coral-admin/src/containers/Dashboard/MostLikedCommentsWidget.js
new file mode 100644
index 000000000..0c41ac7c6
--- /dev/null
+++ b/client/coral-admin/src/containers/Dashboard/MostLikedCommentsWidget.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import I18n from 'coral-framework/modules/i18n/i18n';
+import translations from 'coral-admin/src/translations';
+import ModerationQueue from 'coral-admin/src/containers/ModerationQueue/ModerationQueue';
+import styles from './Widget.css';
+import BanUserDialog from 'coral-admin/src/components/BanUserDialog';
+
+const lang = new I18n(translations);
+
+const MostLikedCommentsWidget = props => {
+ const {
+ comments,
+ moderation,
+ settings,
+ handleBanUser,
+ showBanUserDialog,
+ hideBanUserDialog,
+ acceptComment,
+ rejectComment
+ } = props;
+
+ return (
+
+
{lang.t('most_liked_comments')}
+
+
+
+ );
+};
+
+export default MostLikedCommentsWidget;
diff --git a/client/coral-admin/src/containers/Dashboard/Widget.css b/client/coral-admin/src/containers/Dashboard/Widget.css
new file mode 100644
index 000000000..624b72519
--- /dev/null
+++ b/client/coral-admin/src/containers/Dashboard/Widget.css
@@ -0,0 +1,42 @@
+.widget {
+ box-sizing: border-box;
+ margin: 10px 5px 5px 5px;
+ box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.2);
+ padding: 15px;
+ flex: 1;
+}
+
+.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;
+}
diff --git a/client/coral-admin/src/containers/Install/InstallContainer.js b/client/coral-admin/src/containers/Install/InstallContainer.js
index 7a8634180..b70d3b8c9 100644
--- a/client/coral-admin/src/containers/Install/InstallContainer.js
+++ b/client/coral-admin/src/containers/Install/InstallContainer.js
@@ -5,19 +5,22 @@ import {Wizard, WizardNav} from 'coral-ui';
import {Layout} from '../../components/ui/Layout';
import {
- nextStep,
- previousStep,
goToStep,
+ nextStep,
+ submitUser,
+ checkInstall,
+ previousStep,
+ finishInstall,
+ submitSettings,
updateUserFormData,
updateSettingsFormData,
- submitSettings,
- submitUser,
- checkInstall
+ updatePermittedDomains
} from '../../actions/install';
import InitialStep from './components/Steps/InitialStep';
import AddOrganizationName from './components/Steps/AddOrganizationName';
import CreateYourAccount from './components/Steps/CreateYourAccount';
+import PermittedDomainsStep from './components/Steps/PermittedDomainsStep';
import FinalStep from './components/Steps/FinalStep';
class InstallContainer extends Component {
@@ -43,6 +46,7 @@ class InstallContainer extends Component {
+
@@ -65,10 +69,14 @@ const mapStateToProps = state => ({
});
const mapDispatchToProps = dispatch => ({
- checkInstall: next => dispatch(checkInstall(next)),
nextStep: () => dispatch(nextStep()),
- previousStep: () => dispatch(previousStep()),
goToStep: step => dispatch(goToStep(step)),
+ previousStep: () => dispatch(previousStep()),
+ finishInstall: () => dispatch(finishInstall()),
+ checkInstall: next => dispatch(checkInstall(next)),
+ handleDomainsChange: value => {
+ dispatch(updatePermittedDomains(value));
+ },
handleSettingsChange: e => {
const {name, value} = e.currentTarget;
dispatch(updateSettingsFormData(name, value));
diff --git a/client/coral-admin/src/containers/Install/components/Steps/AddOrganizationName.js b/client/coral-admin/src/containers/Install/components/Steps/AddOrganizationName.js
index ae23d0048..bcac6ef29 100644
--- a/client/coral-admin/src/containers/Install/components/Steps/AddOrganizationName.js
+++ b/client/coral-admin/src/containers/Install/components/Steps/AddOrganizationName.js
@@ -2,25 +2,26 @@ import React from 'react';
import styles from './style.css';
import {TextField, Button} from 'coral-ui';
+const lang = new I18n(translations);
+import translations from '../../translations.json';
+import I18n from 'coral-framework/modules/i18n/i18n';
+
const AddOrganizationName = props => {
const {handleSettingsChange, handleSettingsSubmit, install} = props;
return (
-
- Please tell us the name of your organization. This will appear in emails when
- inviting new team members
-
+
{lang.t('ADD_ORGANIZATION.DESCRIPTION')}
diff --git a/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js b/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js
index cb0f4635f..416fac195 100644
--- a/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js
+++ b/client/coral-admin/src/containers/Install/components/Steps/CreateYourAccount.js
@@ -2,6 +2,10 @@ import React from 'react';
import styles from './style.css';
import {TextField, Button, Spinner} from 'coral-ui';
+const lang = new I18n(translations);
+import translations from '../../translations.json';
+import I18n from 'coral-framework/modules/i18n/i18n';
+
const InitialStep = props => {
const {handleUserChange, handleUserSubmit, install} = props;
return (
@@ -12,7 +16,7 @@ const InitialStep = props => {
className={styles.textField}
id="email"
type="email"
- label='Email address'
+ label={lang.t('CREATE.EMAIL')}
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.email}
@@ -23,7 +27,7 @@ const InitialStep = props => {
className={styles.textField}
id="username"
type="text"
- label='Username'
+ label={lang.t('CREATE.USERNAME')}
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.username}
@@ -33,7 +37,7 @@ const InitialStep = props => {
className={styles.textField}
id="password"
type="password"
- label='Password'
+ label={lang.t('CREATE.PASSWORD')}
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.password}
@@ -43,7 +47,7 @@ const InitialStep = props => {
className={styles.textField}
id="confirmPassword"
type="password"
- label='Confirm Password'
+ label={lang.t('CREATE.CONFIRM_PASSWORD')}
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.confirmPassword}
@@ -51,7 +55,7 @@ const InitialStep = props => {
{
!props.install.isLoading ?
- Save
+ {lang.t('CREATE.SAVE')}
:
}
diff --git a/client/coral-admin/src/containers/Install/components/Steps/FinalStep.js b/client/coral-admin/src/containers/Install/components/Steps/FinalStep.js
index e549ebe63..0ad918487 100644
--- a/client/coral-admin/src/containers/Install/components/Steps/FinalStep.js
+++ b/client/coral-admin/src/containers/Install/components/Steps/FinalStep.js
@@ -3,16 +3,16 @@ import styles from './style.css';
import {Button} from 'coral-ui';
import {Link} from 'react-router';
+const lang = new I18n(translations);
+import translations from '../../translations.json';
+import I18n from 'coral-framework/modules/i18n/i18n';
+
const InitialStep = () => {
return (
-
- Thanks for installing Talk! We sent an email to verify your email
- address. While you finish setting the account, you can start engaging
- with your readers now.
-
-
Launch Talk
-
Close this Installer
+
{lang.t('FINAL.DESCRIPTION')}
+
{lang.t('FINAL.LAUNCH')}
+
{lang.t('FINAL.CLOSE')}
);
};
diff --git a/client/coral-admin/src/containers/Install/components/Steps/InitialStep.js b/client/coral-admin/src/containers/Install/components/Steps/InitialStep.js
index 9f9770fe9..1838e3178 100644
--- a/client/coral-admin/src/containers/Install/components/Steps/InitialStep.js
+++ b/client/coral-admin/src/containers/Install/components/Steps/InitialStep.js
@@ -2,16 +2,16 @@ import React from 'react';
import styles from './style.css';
import {Button} from 'coral-ui';
+const lang = new I18n(translations);
+import translations from '../../translations.json';
+import I18n from 'coral-framework/modules/i18n/i18n';
+
const InitialStep = props => {
const {nextStep} = props;
return (
-
- The remainder of the Talk installation will take about ten minutes.
- Once you complete the following two steps, you will have a free
- installation and provision of Mongo and Redis.
-
-
Get Started
+
{lang.t('INITIAL.DESCRIPTION')}
+
{lang.t('INITIAL.SUBMIT')}
);
};
diff --git a/client/coral-admin/src/containers/Install/components/Steps/PermittedDomainsStep.js b/client/coral-admin/src/containers/Install/components/Steps/PermittedDomainsStep.js
new file mode 100644
index 000000000..f2c1546f6
--- /dev/null
+++ b/client/coral-admin/src/containers/Install/components/Steps/PermittedDomainsStep.js
@@ -0,0 +1,31 @@
+import React from 'react';
+import styles from './style.css';
+import {Button, Card} from 'coral-ui';
+import TagsInput from 'react-tagsinput';
+
+const lang = new I18n(translations);
+import translations from '../../translations.json';
+import I18n from 'coral-framework/modules/i18n/i18n';
+
+const PermittedDomainsStep = props => {
+ const {finishInstall, install, handleDomainsChange} = props;
+ const domains = install.data.settings.domains.whitelist;
+ return (
+
+
{lang.t('PERMITTED_DOMAINS.TITLE')}
+
+ {lang.t('PERMITTED_DOMAINS.DESCRIPTION')}
+ data.split(',').map(d => d.trim())}
+ onChange={tags => handleDomainsChange(tags)}
+ />
+
+
{lang.t('PERMITTED_DOMAINS.SUBMIT')}
+
+ );
+};
+
+export default PermittedDomainsStep;
diff --git a/client/coral-admin/src/containers/Install/components/Steps/style.css b/client/coral-admin/src/containers/Install/components/Steps/style.css
index a5867e960..dea752298 100644
--- a/client/coral-admin/src/containers/Install/components/Steps/style.css
+++ b/client/coral-admin/src/containers/Install/components/Steps/style.css
@@ -59,6 +59,12 @@
}
}
}
+
+ .card {
+ max-width: 500px;
+ margin: 20px auto;
+ text-align: left;
+ }
}
.finalStep {
diff --git a/client/coral-admin/src/containers/Install/translations.json b/client/coral-admin/src/containers/Install/translations.json
new file mode 100644
index 000000000..e5b9f1542
--- /dev/null
+++ b/client/coral-admin/src/containers/Install/translations.json
@@ -0,0 +1,58 @@
+{
+ "en": {
+ "INITIAL" : {
+ "DESCRIPTION": "Let's set up your Talk community in just a few short steps.",
+ "SUBMIT": "Get Started"
+ },
+ "ADD_ORGANIZATION": {
+ "DESCRIPTION": "Please tell us the name of your organization. This will appear in emails when inviting new team members.",
+ "LABEL": "Organization Name",
+ "SAVE": "Save"
+ },
+ "CREATE": {
+ "EMAIL": "Email address",
+ "USERNAME": "Username",
+ "PASSWORD": "Password",
+ "CONFIRM_PASSWORD": "Confirm Password",
+ "SAVE": "Save"
+ },
+ "PERMITTED_DOMAINS": {
+ "TITLE": "Permitted domains",
+ "DESCRIPTION": "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).",
+ "SUBMIT": "Finish install"
+ },
+ "FINAL": {
+ "DESCRIPTION": "Thanks for installing Talk! We sent an email to verify your email address. While you finish setting up the account, you can start engaging with your readers now.",
+ "LAUNCH": "Launch Talk",
+ "CLOSE": "Close this Installer"
+ }
+ },
+ "es": {
+ "INITIAL" : {
+ "DESCRIPTION": "Configuremos tu comunidad de Talk en sólo algunos pasos.",
+ "SUBMIT": "Empezá!"
+ },
+ "ADD_ORGANIZATION": {
+ "DESCRIPTION": "Por favor, dinos el nombre de tu organización. Este aparecerá en los emails cuando invites nuevos miembros.",
+ "LABEL": "Nombre de la Organización",
+ "SAVE": "Guardar"
+ },
+ "CREATE": {
+ "EMAIL": "Dirección de E-Mail",
+ "USERNAME": "Usuario",
+ "PASSWORD": "Contraseña",
+ "CONFIRM_PASSWORD": "Confirmar contraseña",
+ "SAVE": "Guardar"
+ },
+ "PERMITTED_DOMAINS": {
+ "TITLE": "Dominios Permitidos",
+ "DESCRIPTION": "Agrega dominios permitidos a Talk, e.g. tu localhost, staging y ambientes de production (ex. localhost:3000, staging.domain.com, domain.com).",
+ "SUBMIT": "Finalizar instalación"
+ },
+ "FINAL": {
+ "DESCRIPTION": "Gracias por instalar Talk! Te enviamos un email para verificar tu identidad. Mientras se termina de configurar la cuenta, ya puedes empezar a interactuar con tus lectores",
+ "LAUNCH": "Lanzar Talk",
+ "CLOSE": "Cerrar este instalador"
+ }
+ }
+}
diff --git a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js
index d3e2f9194..19e6e7034 100644
--- a/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js
+++ b/client/coral-admin/src/containers/ModerationQueue/ModerationContainer.js
@@ -65,6 +65,8 @@ class ModerationContainer extends Component {
}
}
+ const comments = data[activeTab];
+
return (
@@ -76,9 +78,8 @@ class ModerationContainer extends Component {
modQueueResort={modQueueResort}
/>
{
- const areComments = props.data[activeTab].length;
+const ModerationQueue = ({comments, ...props}) => {
return (
{
- areComments
- ? props.data[activeTab].map((comment, i) => {
+ comments.length
+ ? comments.map((comment, i) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
return {
};
ModerationQueue.propTypes = {
- data: PropTypes.object.isRequired,
- acceptComment: PropTypes.func.isRequired,
- rejectComment: PropTypes.func.isRequired,
- showBanUserDialog: PropTypes.func.isRequired,
+ suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
currentAsset: PropTypes.object,
- suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired
+ showBanUserDialog: PropTypes.func.isRequired,
+ rejectComment: PropTypes.func.isRequired,
+ acceptComment: PropTypes.func.isRequired,
+ comments: PropTypes.array.isRequired
};
export default ModerationQueue;
diff --git a/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js b/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js
index d2520f784..f9b0c266f 100644
--- a/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js
+++ b/client/coral-admin/src/containers/ModerationQueue/components/ModerationMenu.js
@@ -52,8 +52,8 @@ class ModerationMenu extends Component {
label='Sort'
value={this.state.sort}
onChange={sort => this.selectSort(sort)}>
- Newest First
- Oldest First
+ Newest First
+ Oldest First
diff --git a/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql b/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql
new file mode 100644
index 000000000..37335aeaa
--- /dev/null
+++ b/client/coral-admin/src/graphql/fragments/assetMetricsView.graphql
@@ -0,0 +1,12 @@
+fragment metrics on Asset {
+ id
+ title
+ url
+ author
+ created_at
+ action_summaries {
+ type: __typename
+ actionCount
+ actionableItemCount
+ }
+}
diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js
index 809769a13..c4a30ad44 100644
--- a/client/coral-admin/src/graphql/queries/index.js
+++ b/client/coral-admin/src/graphql/queries/index.js
@@ -1,23 +1,7 @@
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()
- }
- };
- }
-});
+import METRICS from './metricsQuery.graphql';
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
options: ({params: {id = null}}) => {
@@ -34,6 +18,18 @@ export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
})
});
+export const getMetrics = graphql(METRICS, {
+ options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
+
+ return {
+ variables: {
+ from: dashboardWindowStart,
+ to: dashboardWindowEnd
+ }
+ };
+ }
+});
+
export const modQueueResort = (id, fetchMore) => (sort) => {
return fetchMore({
query: MOD_QUEUE_QUERY,
diff --git a/client/coral-admin/src/graphql/queries/metricsQuery.graphql b/client/coral-admin/src/graphql/queries/metricsQuery.graphql
new file mode 100644
index 000000000..42a9fb70e
--- /dev/null
+++ b/client/coral-admin/src/graphql/queries/metricsQuery.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/assetMetricsView.graphql"
+
+query Metrics ($from: Date!, $to: Date!) {
+ assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
+ ...metrics
+ }
+ assetsByLike: assetMetrics(from: $from, to: $to, sort: LIKE) {
+ ...metrics
+ }
+}
diff --git a/client/coral-admin/src/graphql/queries/mostFlags.graphql b/client/coral-admin/src/graphql/queries/mostFlags.graphql
deleted file mode 100644
index 182ea0c8b..000000000
--- a/client/coral-admin/src/graphql/queries/mostFlags.graphql
+++ /dev/null
@@ -1,14 +0,0 @@
-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
- }
- }
-}
diff --git a/client/coral-admin/src/reducers/install.js b/client/coral-admin/src/reducers/install.js
index 596fd16cd..1f0f079ff 100644
--- a/client/coral-admin/src/reducers/install.js
+++ b/client/coral-admin/src/reducers/install.js
@@ -1,4 +1,4 @@
-import {Map} from 'immutable';
+import {Map, List} from 'immutable';
import * as actions from '../constants/install';
@@ -6,7 +6,10 @@ const initialState = Map({
isLoading: false,
data: Map({
settings: Map({
- organizationName: ''
+ organizationName: '',
+ domains: Map({
+ whitelist: List()
+ })
}),
user: Map({
username: '',
@@ -33,6 +36,10 @@ const initialState = Map({
{
text: '2. Create your account',
step: 2
+ },
+ {
+ text: '3. Domain Whitelist',
+ step: 3
}],
installRequest: null,
installRequestError: null,
@@ -50,6 +57,9 @@ export default function install (state = initialState, action) {
case actions.GO_TO_STEP:
return state
.set('step', action.step);
+ case actions.UPDATE_PERMITTED_DOMAINS_SETTINGS:
+ return state
+ .setIn(['data', 'settings', 'domains', 'whitelist'], action.value);
case actions.UPDATE_FORMDATA_SETTINGS:
return state
.setIn(['data', 'settings', action.name], action.value);
diff --git a/client/coral-admin/src/reducers/settings.js b/client/coral-admin/src/reducers/settings.js
index a02cac198..f74eb659f 100644
--- a/client/coral-admin/src/reducers/settings.js
+++ b/client/coral-admin/src/reducers/settings.js
@@ -1,11 +1,22 @@
import {Map, List} from 'immutable';
import * as actions from '../actions/settings';
+// this is initialized here because
+// currently you have to reload the dashboard to get new stats
+// cleaner updates are planned in the future.
+// TODO: if there are more than two fields for the dashboard being created here,
+// please create a new reducer specifically for the Dashboard.
+const DASHBOARD_WINDOW_MINUTES = 5;
+let then = new Date();
+then.setMinutes(then.getMinutes() - DASHBOARD_WINDOW_MINUTES);
+
const initialState = Map({
wordlist: Map({
banned: List(),
suspect: List()
}),
+ dashboardWindowStart: then.toISOString(),
+ dashboardWindowEnd: new Date().toISOString(),
domains: Map({
whitelist: List()
}),
diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json
index 7219a22ca..dbb3e650e 100644
--- a/client/coral-admin/src/translations.json
+++ b/client/coral-admin/src/translations.json
@@ -86,7 +86,7 @@
"comment-count-text-post": " characters.",
"comment-count-error": "Please enter a valid number.",
"domain-list-title": "Domain Whitelist",
- "domain-list-text": "Some instructions on how to type the urls."
+ "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)."
},
"bandialog": {
"ban_user": "Ban User?",
@@ -111,7 +111,8 @@
},
"dashboard": {
"no_flags": "There have been no flags in the last 5 minutes! Hooray!",
- "comment_count": "Comments"
+ "no_likes": "There have been no likes in the last 5 minutes. All quiet.",
+ "comment_count": "comments"
},
"streams": {
"empty_result": "No assets match this search. Maybe try widening your search?",
@@ -206,7 +207,7 @@
"comment-count-text-post": " caracteres",
"comment-count-error": "Por favor escribe un número válido.",
"domain-list-title": "Lista de Dominios Permitidos",
- "domain-list-text": "Instrucciones de como ingresar las URLs."
+ "domain-list-text": "Agrega dominios permitidos a Talk, e.g. tu localhost, staging y ambientes de production (ex. localhost:3000, staging.domain.com, domain.com)."
},
"embedlink": {
"copy": "Copiar"
@@ -220,7 +221,8 @@
},
"dashbord": {
"no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!",
- "comment_count": "Comentarios"
+ "no_likes": "A nadie le ha gustado algún comentario en los últimos 5 minutos. Todo tranquilo.",
+ "comment_count": "comentarios"
},
"streams": {
"empty_result": "No se encuentro articulo con esta busqueda. Tal vez extender la busqueda?",
diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js
index 6a781675b..216349412 100644
--- a/client/coral-sign-in/components/SignUpContent.js
+++ b/client/coral-sign-in/components/SignUpContent.js
@@ -138,12 +138,9 @@ class SignUpContent extends React.Component {
}
);
diff --git a/client/coral-ui/components/WizardNav.css b/client/coral-ui/components/WizardNav.css
index 001ee8603..1f0b78d86 100644
--- a/client/coral-ui/components/WizardNav.css
+++ b/client/coral-ui/components/WizardNav.css
@@ -13,6 +13,7 @@
position: relative;
padding-left: 40px;
border-radius: 1px;
+ min-height: 24px;
&:hover {
cursor: pointer;
diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js
index ad78a1e92..47a8095dc 100644
--- a/graph/loaders/comments.js
+++ b/graph/loaders/comments.js
@@ -270,6 +270,21 @@ const genRecentComments = (_, ids) => {
.then(util.arrayJoinBy(ids, 'asset_id'));
};
+/**
+ * Returns the comment's by their id.
+ * @param {Object} context graph context
+ * @param {Array} ids the comment id's to fetch
+ * @return {Promise} resolves to the comments
+ */
+const genCommentsByID = (context, ids) => {
+ return CommentModel.find({
+ id: {
+ $in: ids
+ }
+ })
+ .then(util.singleJoinBy(ids, 'id'));
+};
+
/**
* Creates a set of loaders based on a GraphQL context.
* @param {Object} context the context of the GraphQL request
@@ -277,6 +292,7 @@ const genRecentComments = (_, ids) => {
*/
module.exports = (context) => ({
Comments: {
+ get: new DataLoader((ids) => genCommentsByID(context, ids)),
getByQuery: (query) => getCommentsByQuery(context, query),
getCountByQuery: (query) => getCommentCountByQuery(context, query),
countByAssetID: new util.SharedCacheDataLoader('Comments.countByAssetID', 3600, (ids) => getCountsByAssetID(context, ids)),
diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js
index a842165c0..2627d82d9 100644
--- a/graph/loaders/metrics.js
+++ b/graph/loaders/metrics.js
@@ -2,10 +2,12 @@ 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}) => {
+/**
+ * Returns a list of assets with action metadata included on the models.
+ */
+const getAssetMetrics = ({loaders: {Metrics, Assets, Comments}}, {from, to, sort, limit}) => {
let commentMetrics = {};
let assetMetrics = [];
@@ -14,6 +16,10 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
.then((actionSummaries) => {
commentMetrics = actionSummaries.reduce((acc, {item_id, action_type, count}) => {
+ if (action_type !== sort) {
+ return acc;
+ }
+
if (!(item_id in acc)) {
acc[item_id] = [];
}
@@ -24,10 +30,10 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
}, {});
// Collect just the comment id's.
- let commentIDs = _.uniq(actionSummaries.map((as) => as.item_id));
+ let commentIDs = Object.keys(commentMetrics);
// Find those comments.
- return Metrics.getSpecificComments.loadMany(commentIDs);
+ return Comments.get.loadMany(commentIDs);
})
.then((comments) => {
@@ -44,29 +50,24 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
}));
return {action_summaries, id: asset_id};
- });
+ })
+
+ .filter((asset) => {
+ let contextActionSummary = asset.action_summaries.find((({action_type}) => action_type === sort));
+ if (contextActionSummary === null) {
+ 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.
- assetMetrics.sort((a, b) => {
+ .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;
@@ -99,6 +100,75 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
});
};
+/**
+ * Returns a list of comments that are retrieved based on most activity within
+ * the indicated time range.
+ */
+const getCommentMetrics = ({loaders: {Metrics, Comments}}, {from, to, sort, limit}) => {
+
+ let commentActionSummaries = {};
+
+ return Metrics.getRecentActions.load({from, to})
+ .then((actionSummaries) => {
+
+ actionSummaries.sort((a, b) => {
+ let aActionSummary = a.action_type === sort ? a : null;
+ let bActionSummary = b.action_type === sort ? 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 === sort);
+ 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.
+ return Comments.get.loadMany(commentIDs);
+ })
+ .then((comments) => 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([
@@ -131,25 +201,17 @@ const getRecentActions = (context, {from, to}) => {
]);
};
-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})
+ Assets: {
+ get: ({from, to, sort, limit}) => getAssetMetrics(context, {from, to, sort, limit})
+ },
+ Comments: {
+ get: ({from, to, sort, limit}) => getCommentMetrics(context, {from, to, sort, limit})
+ }
}
});
diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js
index ef77ff6b5..de1909df1 100644
--- a/graph/resolvers/comment.js
+++ b/graph/resolvers/comment.js
@@ -25,7 +25,11 @@ const Comment = {
return null;
},
- action_summaries({id}, _, {loaders: {Actions}}) {
+ action_summaries({id, action_summaries}, _, {loaders: {Actions}}) {
+ if (action_summaries) {
+ return action_summaries;
+ }
+
return Actions.getSummariesByItemID.load(id);
},
asset({asset_id}, _, {loaders: {Assets}}) {
diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js
index ced4e65cf..8800bfebf 100644
--- a/graph/resolvers/root_query.js
+++ b/graph/resolvers/root_query.js
@@ -56,12 +56,20 @@ const RootQuery = {
return Comments.getCountByQuery({statuses, asset_id, parent_id});
},
- metrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics}}) {
+ assetMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Assets}}}) {
if (user == null || !user.hasRoles('ADMIN')) {
return null;
}
- return Metrics.get({from, to, sort, limit});
+ return Assets.get({from, to, sort, limit});
+ },
+
+ commentMetrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics: {Comments}}}) {
+ if (user == null || !user.hasRoles('ADMIN')) {
+ return null;
+ }
+
+ return Comments.get({from, to, sort, limit});
},
// This returns the current user, ensure that if we aren't logged in, we
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index fbdeb4279..4dd9a4ba8 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -494,9 +494,13 @@ type RootQuery {
# role.
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]
+ # Asset metrics related to user actions are saturated into the assets
+ # returned.
+ assetMetrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Asset!]
+
+ # Comment metrics related to user actions are saturated into the comments
+ # returned.
+ commentMetrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Comment!]
}
################################################################################
diff --git a/test/graph/loaders/metrics.js b/test/graph/loaders/metrics.js
new file mode 100644
index 000000000..29f2eeff1
--- /dev/null
+++ b/test/graph/loaders/metrics.js
@@ -0,0 +1,151 @@
+const {expect} = require('chai');
+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');
+
+describe('graph.loaders.Metrics', () => {
+ beforeEach(() => SettingsService.init());
+
+ describe('#Comments', () => {
+ const query = `
+ query CommentMetrics($from: Date!, $to: Date!) {
+ liked: commentMetrics(from: $from, to: $to, sort: LIKE) {
+ id
+ }
+ flagged: commentMetrics(from: $from, to: $to, sort: 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!'}
+ ]));
+
+ [
+ {liked: 0, flagged: 0, actions: []},
+ {liked: 1, flagged: 0, actions: [{action_type: 'LIKE', item_id: '1', item_type: 'COMMENTS'}]},
+ {liked: 0, flagged: 1, actions: [{action_type: 'FLAG', item_id: '1', item_type: 'COMMENTS'}]},
+ {liked: 1, flagged: 1, actions: [
+ {action_type: 'FLAG', item_id: '1', item_type: 'COMMENTS'},
+ {action_type: 'LIKE', item_id: '1', item_type: 'COMMENTS'}
+ ]},
+ {liked: 3, flagged: 1, actions: [
+ {action_type: 'LIKE', item_id: '1', item_type: 'COMMENTS'},
+ {action_type: 'LIKE', item_id: '2', item_type: 'COMMENTS'},
+ {action_type: 'LIKE', item_id: '3', item_type: 'COMMENTS'},
+ {action_type: 'FLAG', item_id: '3', item_type: 'COMMENTS'}
+ ]}
+ ].forEach(({liked, flagged, actions}) => {
+
+ describe(`with actions=${actions.length}`, () => {
+
+ beforeEach(() => ActionModel.create(actions));
+
+ it(`returns the correct amount of metrics liked=${liked} 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.liked).to.have.length(liked);
+ expect(data.flagged).to.have.length(flagged);
+ });
+ });
+
+ });
+
+ });
+
+ });
+ });
+
+ describe('#Assets', () => {
+ const query = `
+ fragment metrics on Asset {
+ id
+ action_summaries {
+ type: __typename
+ actionCount
+ actionableItemCount
+ }
+ }
+
+ query Metrics($from: Date!, $to: Date!) {
+ assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
+ ...metrics
+ }
+ assetsByLike: assetMetrics(from: $from, to: $to, sort: LIKE) {
+ ...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!'}
+ ])
+ ]));
+
+ [
+ {liked: 0, flagged: 0, actions: []},
+ {liked: 1, flagged: 0, actions: [{action_type: 'LIKE', item_id: 'c1', item_type: 'COMMENTS'}]},
+ {liked: 0, flagged: 1, actions: [{action_type: 'FLAG', item_id: 'c1', item_type: 'COMMENTS'}]},
+ {liked: 1, flagged: 1, actions: [
+ {action_type: 'FLAG', item_id: 'c1', item_type: 'COMMENTS'},
+ {action_type: 'LIKE', item_id: 'c1', item_type: 'COMMENTS'}
+ ]},
+ {liked: 1, flagged: 1, actions: [
+ {action_type: 'LIKE', item_id: 'c1', item_type: 'COMMENTS'},
+ {action_type: 'LIKE', item_id: 'c2', item_type: 'COMMENTS'},
+ {action_type: 'LIKE', item_id: 'c3', item_type: 'COMMENTS'},
+ {action_type: 'FLAG', item_id: 'c3', item_type: 'COMMENTS'}
+ ]}
+ ].forEach(({liked, flagged, actions}) => {
+
+ describe(`with actions=${actions.length}`, () => {
+
+ beforeEach(() => ActionModel.create(actions));
+
+ it(`returns the correct amount of metrics liked=${liked} 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.assetsByLike).to.have.length(liked);
+ expect(data.assetsByFlag).to.have.length(flagged);
+ });
+ });
+
+ });
+
+ });
+
+ });
+ });
+});