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 */} - - - - - - - - { - 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 ( - - - - - - - - ); - }) - : - } - -
{lang.t('streams.article')}{lang.t('modqueue.flagged')}{lang.t('modqueue.likes')}{lang.t('dashboard.comment_count')}
{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 */} + + + + + + { + assets.length + ? assets.map((asset, index) => { + const flagSummary = asset.action_summaries.find(s => s.type === 'FlagAssetActionSummary'); + return ( + + + + + + ); + }) + : + } + +
{lang.t('streams.article')}{lang.t('modqueue.flagged')}
{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 */} + + + + + + { + assets.length + ? assets.map((asset, index) => { + const likeSummary = asset.action_summaries.find(s => s.type === 'LikeAssetActionSummary'); + return ( + + + + + + ); + }) + : + } + +
{lang.t('streams.article')}{lang.t('modqueue.likes')}
{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 ? - + : } 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. -

- - +

{lang.t('FINAL.DESCRIPTION')}

+ +
); }; 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. -

- +

{lang.t('INITIAL.DESCRIPTION')}

+
); }; 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)} + /> +
+ +
+ ); +}; + +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)}> - - + +
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 { }
- - {lang.t('signIn.alreadyHaveAnAccount')} - changeView('SIGNIN')}> - {lang.t('signIn.signIn')} - - + {lang.t('signIn.alreadyHaveAnAccount')} changeView('SIGNIN')}> + {lang.t('signIn.signIn')} +
); 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); + }); + }); + + }); + + }); + + }); + }); +});