mirror of
https://github.com/wassname/talk.git
synced 2026-07-04 14:15:23 +08:00
Merge branch 'master' of github.com:coralproject/talk into passport
This commit is contained in:
@@ -5,7 +5,8 @@ import {
|
||||
FETCH_COMMENTERS_SUCCESS,
|
||||
FETCH_COMMENTERS_FAILURE,
|
||||
SORT_UPDATE,
|
||||
COMMENTERS_NEW_PAGE
|
||||
COMMENTERS_NEW_PAGE,
|
||||
SET_ROLE
|
||||
} from '../constants/community';
|
||||
|
||||
import {base, getInit, handleResp} from '../helpers/response';
|
||||
@@ -40,3 +41,9 @@ export const newPage = () => ({
|
||||
type: COMMENTERS_NEW_PAGE
|
||||
});
|
||||
|
||||
export const setRole = (id, role) => dispatch => {
|
||||
return fetch(`${base}/user/${id}/role`, getInit('POST', {role}))
|
||||
.then(() => {
|
||||
return dispatch({type: SET_ROLE, id, role});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,8 +3,8 @@ import React from 'react';
|
||||
import {Button, Icon} from 'react-mdl';
|
||||
import timeago from 'timeago.js';
|
||||
import styles from './CommentList.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
// Render a single comment for the list
|
||||
export default props => (
|
||||
|
||||
@@ -2,22 +2,26 @@ import React from 'react';
|
||||
import {Layout, Navigation, Drawer, Header} from 'react-mdl';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
// App header. If we add a navbar it should be here
|
||||
export default (props) => (
|
||||
<Layout fixedDrawer>
|
||||
<Header title='Talk'>
|
||||
<Navigation>
|
||||
<Link className={styles.navLink} to={'/admin/'}>Moderate</Link>
|
||||
<Link className={styles.navLink} to={'/admin/configure'}>Configure</Link>
|
||||
<Link className={styles.navLink} to={'/admin/'}>{lang.t('configure.moderate')}</Link>
|
||||
<Link className={styles.navLink} to={'/admin/configure'}>{lang.t('Configure')}</Link>
|
||||
</Navigation>
|
||||
</Header>
|
||||
<Drawer>
|
||||
<Navigation>
|
||||
<Link className={styles.navLink} to={'/admin/'}>Moderate</Link>
|
||||
<Link className={styles.navLink} to={'/admin/configure'}>Configure</Link>
|
||||
<Link className={styles.navLink} to={'/admin/'}>{lang.t('configure.moderate')}</Link>
|
||||
<Link className={styles.navLink} to={'/admin/configure'}>{lang.t('configure.Configure')}</Link>
|
||||
</Navigation>
|
||||
</Drawer>
|
||||
{props.children}
|
||||
</Layout>
|
||||
);
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
import styles from './ModerationKeysModal.css';
|
||||
|
||||
@@ -2,13 +2,17 @@ import React from 'react';
|
||||
import {Navigation, Drawer} from 'react-mdl';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
|
||||
export default () => (
|
||||
<Drawer>
|
||||
<Navigation>
|
||||
<Link className={styles.navLink} to="/admin">Moderate</Link>
|
||||
<Link className={styles.navLink} to="/admin/community">Community</Link>
|
||||
<Link className={styles.navLink} to="/admin/configure">Configure</Link>
|
||||
<Link className={styles.navLink} to="/admin">{lang.t('configure.moderate')}</Link>
|
||||
<Link className={styles.navLink} to="/admin/community">{lang.t('configure.community')}</Link>
|
||||
<Link className={styles.navLink} to="/admin/configure">{lang.t('configure.configure')}</Link>
|
||||
</Navigation>
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -2,16 +2,20 @@ import React from 'react';
|
||||
import {Navigation, Header} from 'react-mdl';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
|
||||
export default () => (
|
||||
<Header title='Talk'>
|
||||
<Navigation>
|
||||
<Link className={styles.navLink} to="/admin">Moderate</Link>
|
||||
<Link className={styles.navLink} to="/admin/community">Community</Link>
|
||||
<Link className={styles.navLink} to="/admin/configure">Configure</Link>
|
||||
<Link className={styles.navLink} to="/admin">{lang.t('configure.moderate')}</Link>
|
||||
<Link className={styles.navLink} to="/admin/community">{lang.t('configure.community')}</Link>
|
||||
<Link className={styles.navLink} to="/admin/configure">{lang.t('configure.configure')}</Link>
|
||||
<span>
|
||||
{`v${process.env.VERSION}`}
|
||||
</span>
|
||||
</Navigation>
|
||||
</Header>
|
||||
);
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -3,3 +3,4 @@ export const FETCH_COMMENTERS_SUCCESS = 'FETCH_COMMENTERS_SUCCESS';
|
||||
export const FETCH_COMMENTERS_FAILURE = 'FETCH_COMMENTERS_FAILURE';
|
||||
export const SORT_UPDATE = 'SORT_UPDATE';
|
||||
export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE';
|
||||
export const SET_ROLE = 'SET_ROLE';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import {Grid, Cell} from 'react-mdl';
|
||||
|
||||
import styles from './Community.css';
|
||||
@@ -19,6 +19,10 @@ const tableHeaders = [
|
||||
{
|
||||
title: lang.t('community.account_creation_date'),
|
||||
field: 'created_at'
|
||||
},
|
||||
{
|
||||
title: lang.t('community.newsroom_role'),
|
||||
field: 'role'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,34 +1,66 @@
|
||||
import React from 'react';
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {SelectField, Option} from 'react-mdl-selectfield';
|
||||
import styles from './Community.css';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../../translations';
|
||||
import {setRole} from '../../actions/community';
|
||||
|
||||
const Table = ({headers, data, onHeaderClickHandler}) => (
|
||||
<table className={`mdl-data-table ${styles.dataTable}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header, i) =>(
|
||||
<th
|
||||
key={i}
|
||||
className="mdl-data-table__cell--non-numeric"
|
||||
onClick={() => onHeaderClickHandler({field: header.field})}>
|
||||
{header.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, i)=> (
|
||||
<tr key={i}>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.displayName}
|
||||
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.created_at}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export default Table;
|
||||
class Table extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.onRoleChange = this.onRoleChange.bind(this);
|
||||
}
|
||||
|
||||
onRoleChange (id, role) {
|
||||
this.props.dispatch(setRole(id, role));
|
||||
}
|
||||
|
||||
render () {
|
||||
const {headers, commenters, onHeaderClickHandler} = this.props;
|
||||
|
||||
return (
|
||||
<table className={`mdl-data-table ${styles.dataTable}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header, i) =>(
|
||||
<th
|
||||
key={i}
|
||||
className="mdl-data-table__cell--non-numeric"
|
||||
onClick={() => onHeaderClickHandler({field: header.field})}>
|
||||
{header.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{commenters.map((row, i)=> (
|
||||
<tr key={i}>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.displayName}
|
||||
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.created_at}
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<SelectField label={'Select me'} value={row.roles[0] || ''}
|
||||
label={lang.t('community.role')}
|
||||
onChange={role => this.onRoleChange(row.id, role)}>
|
||||
<Option value={''}>.</Option>
|
||||
<Option value={'moderator'}>{lang.t('community.moderator')}</Option>
|
||||
<Option value={'admin'}>{lang.t('community.admin')}</Option>
|
||||
</SelectField>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({commenters: state.community.get('commenters')}))(Table);
|
||||
|
||||
@@ -23,6 +23,21 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.configSettingInfoBox {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
width: auto;
|
||||
height: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.configSettingInfoBox p {
|
||||
font-size: 12px;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.configSettingEmbed {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
@@ -53,3 +68,7 @@
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@ import {
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
ListItemAction,
|
||||
//Textfield,
|
||||
Textfield,
|
||||
Checkbox,
|
||||
Button,
|
||||
Icon
|
||||
} from 'react-mdl';
|
||||
import styles from './Configure.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
class Configure extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -24,6 +24,8 @@ class Configure extends React.Component {
|
||||
|
||||
this.copyToClipBoard = this.copyToClipBoard.bind(this);
|
||||
this.updateModeration = this.updateModeration.bind(this);
|
||||
this.updateInfoBoxEnable = this.updateInfoBoxEnable.bind(this);
|
||||
this.updateInfoBoxContent = this.updateInfoBoxContent.bind(this);
|
||||
this.saveSettings = this.saveSettings.bind(this);
|
||||
}
|
||||
|
||||
@@ -36,6 +38,16 @@ class Configure extends React.Component {
|
||||
this.props.dispatch(updateSettings({moderation}));
|
||||
}
|
||||
|
||||
updateInfoBoxEnable () {
|
||||
const infoBoxEnable = !this.props.settings.infoBoxEnable;
|
||||
this.props.dispatch(updateSettings({infoBoxEnable}));
|
||||
}
|
||||
|
||||
updateInfoBoxContent (event) {
|
||||
const infoBoxContent = event.target.value;
|
||||
this.props.dispatch(updateSettings({infoBoxContent}));
|
||||
}
|
||||
|
||||
saveSettings () {
|
||||
this.props.dispatch(saveSettingsToServer());
|
||||
}
|
||||
@@ -48,22 +60,30 @@ class Configure extends React.Component {
|
||||
onClick={this.updateModeration}
|
||||
checked={this.props.settings.moderation === 'pre'} />
|
||||
</ListItemAction>
|
||||
Enable pre-moderation
|
||||
{lang.t('configure.enable-pre-moderation')}
|
||||
</ListItem>
|
||||
{/*
|
||||
<ListItem className={styles.configSetting}>
|
||||
<ListItemAction><Checkbox /></ListItemAction>
|
||||
Include Comment Stream Description for Readers
|
||||
<ListItem threeLine className={styles.configSettingInfoBox}>
|
||||
<ListItemAction>
|
||||
<Checkbox
|
||||
onClick={this.updateInfoBoxEnable}
|
||||
checked={this.props.settings.infoBoxEnable} />
|
||||
</ListItemAction>
|
||||
<ListItemContent>
|
||||
{lang.t('configure.include-comment-stream')}
|
||||
<p>
|
||||
{lang.t('configure.include-comment-stream-desc')}
|
||||
</p>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.configSetting}>
|
||||
<ListItemAction><Checkbox /></ListItemAction>
|
||||
Limit Comment Length
|
||||
<Textfield
|
||||
pattern='-?[0-9]*(\.[0-9]+)?'
|
||||
error='Input is not a number!'
|
||||
label='Maximum Characters' />
|
||||
<ListItem className={`${styles.configSettingInfoBox} ${this.props.settings.infoBoxEnable ? null : styles.hidden}`} >
|
||||
<ListItemContent>
|
||||
<Textfield
|
||||
onChange={this.updateInfoBoxContent}
|
||||
value={this.props.settings.infoBoxContent}
|
||||
label={lang.t('configure.include-text')}
|
||||
rows={3}/>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
*/}
|
||||
</List>;
|
||||
}
|
||||
|
||||
@@ -84,7 +104,7 @@ class Configure extends React.Component {
|
||||
|
||||
return <List>
|
||||
<ListItem className={styles.configSettingEmbed}>
|
||||
<p>Copy and paste code below into your CMS to embed your comment box in your articles</p>
|
||||
<p>{lang.t('configure.copy-and-paste')}</p>
|
||||
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
|
||||
<Button raised colored className={styles.copyButton} onClick={this.copyToClipBoard}>
|
||||
{lang.t('embedlink.copy')}
|
||||
@@ -100,8 +120,8 @@ class Configure extends React.Component {
|
||||
|
||||
render () {
|
||||
let pageTitle = this.state.activeSection === 'comments'
|
||||
? 'Comment Settings'
|
||||
: 'Embed Comment Stream';
|
||||
? lang.t('configure.comment-settings')
|
||||
: lang.t('configure.embed-comment-stream');
|
||||
|
||||
if (this.props.fetchingSettings) {
|
||||
pageTitle += ' - Loading...';
|
||||
@@ -114,16 +134,16 @@ class Configure extends React.Component {
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection.bind(this, 'comments')}
|
||||
icon='settings'>Comment Settings</ListItemContent>
|
||||
icon='settings'>{lang.t('configure.comment-settings')}</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection.bind(this, 'embed')}
|
||||
icon='code'>Embed Comment Stream</ListItemContent>
|
||||
icon='code'>{lang.t('configure.embed-comment-stream')}</ListItemContent>
|
||||
</ListItem>
|
||||
</List>
|
||||
<Button raised colored onClick={this.saveSettings}>
|
||||
<Icon name='save' /> Save Changes
|
||||
<Icon name='save' /> {lang.t('configure.save-changes')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
|
||||
@@ -5,8 +5,8 @@ import CommentList from 'components/CommentList';
|
||||
import {updateStatus} from 'actions/comments';
|
||||
import styles from './ModerationQueue.css';
|
||||
import key from 'keymaster';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
/*
|
||||
* Renders the moderation queue as a tabbed layout with 3 moderation
|
||||
@@ -91,7 +91,7 @@ class ModerationQueue extends React.Component {
|
||||
commentIds={
|
||||
comments
|
||||
.get('ids')
|
||||
.filter(id =>
|
||||
.filter(id =>
|
||||
comments
|
||||
.get('byId')
|
||||
.get(id)
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
FETCH_COMMENTERS_REQUEST,
|
||||
FETCH_COMMENTERS_FAILURE,
|
||||
FETCH_COMMENTERS_SUCCESS,
|
||||
SORT_UPDATE
|
||||
SORT_UPDATE,
|
||||
SET_ROLE
|
||||
} from '../constants/community';
|
||||
|
||||
const initialState = Map({
|
||||
@@ -37,6 +38,13 @@ export default function community (state = initialState, action) {
|
||||
})
|
||||
.set('commenters', commenters); // Sets to normal array
|
||||
}
|
||||
case SET_ROLE : {
|
||||
const commenters = state.get('commenters');
|
||||
const idx = commenters.findIndex(el => el.id === action.id);
|
||||
|
||||
commenters[idx].roles[0] = action.role;
|
||||
return state.set('commenters', commenters.map(id => id));
|
||||
}
|
||||
case SORT_UPDATE :
|
||||
return state
|
||||
.set('field', action.sort.field)
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
export default {
|
||||
en: {
|
||||
'community': {
|
||||
username_and_email: 'Username and Email',
|
||||
account_creation_date: 'Account Creation Date'
|
||||
},
|
||||
'modqueue': {
|
||||
'pending': 'pending',
|
||||
'rejected': 'rejected',
|
||||
'flagged': 'flagged',
|
||||
'shortcuts': 'Shortcuts',
|
||||
'close': 'Close',
|
||||
'actions': 'Actions',
|
||||
'navigation': 'Navigation',
|
||||
'approve': 'Approve comment',
|
||||
'reject': 'Reject comment',
|
||||
'nextcomment': 'Go to the next comment',
|
||||
'prevcomment': 'Go to the previous comment',
|
||||
'singleview': 'Toggle single comment edit view',
|
||||
'thismenu': 'Open this menu'
|
||||
},
|
||||
'comment': {
|
||||
'flagged': 'flagged',
|
||||
'anon': 'Anonymous'
|
||||
},
|
||||
'embedlink': {
|
||||
'copy': 'Copy to Clipboard'
|
||||
}
|
||||
},
|
||||
es: {
|
||||
'community': {
|
||||
username_and_email: 'Usuario y E-mail',
|
||||
account_creation_date: 'Fecha de creación de la cuenta'
|
||||
},
|
||||
'modqueue': {
|
||||
'pending': 'pendiente',
|
||||
'rejected': 'rechazado',
|
||||
'flagged': 'marcado',
|
||||
'shortcuts': 'Atajos de teclado',
|
||||
'close': 'Cerrar'
|
||||
},
|
||||
'comment': {
|
||||
'flagged': 'marcado',
|
||||
'anon': 'Anónimo'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"en": {
|
||||
"community": {
|
||||
"username_and_email": "Username and Email",
|
||||
"account_creation_date": "Account Creation Date",
|
||||
"newsroom_role": "Newsroom Role",
|
||||
"admin": "Administrator",
|
||||
"moderator": "Moderator",
|
||||
"role": "Select role..."
|
||||
},
|
||||
"modqueue": {
|
||||
"pending": "pending",
|
||||
"rejected": "rejected",
|
||||
"flagged": "flagged",
|
||||
"shortcuts": "Shortcuts",
|
||||
"close": "Close",
|
||||
"actions": "Actions",
|
||||
"navigation": "Navigation",
|
||||
"approve": "Approve comment",
|
||||
"reject": "Reject comment",
|
||||
"nextcomment": "Go to the next comment",
|
||||
"prevcomment": "Go to the previous comment",
|
||||
"singleview": "Toggle single comment edit view",
|
||||
"thismenu": "Open this menu"
|
||||
},
|
||||
"comment": {
|
||||
"flagged": "flagged",
|
||||
"anon": "Anonymous"
|
||||
},
|
||||
"embedlink": {
|
||||
"copy": "Copy to Clipboard"
|
||||
},
|
||||
"configure": {
|
||||
"enable-pre-moderation": "Enable pre-moderation",
|
||||
"include-comment-stream": "Include Comment Stream Description for Readers.",
|
||||
"include-comment-stream-desc": "Write a message to be added to the top of your comment stream. Pose a topic, include community guidelines, etc.",
|
||||
"include-text": "Include your text here.",
|
||||
"comment-settings": "Comment Settings",
|
||||
"embed-comment-stream": "Embed Comment Stream",
|
||||
"save-changes": "Save Changes",
|
||||
"copy-and-paste": "Copy and paste code below into your CMS to embed your comment box in your articles",
|
||||
"moderate": "Moderate",
|
||||
"configure": "Configure",
|
||||
"community": "Community"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"community": {
|
||||
"username_and_email": "Usuario y E-mail",
|
||||
"account_creation_date": "Fecha de creación de la cuenta",
|
||||
"newsroom_role": "Rol en la redacción",
|
||||
"admin": "Administrador",
|
||||
"moderator": "Moderador",
|
||||
"role": "Select role..."
|
||||
},
|
||||
"modqueue": {
|
||||
"pending": "pendiente",
|
||||
"rejected": "rechazado",
|
||||
"flagged": "marcado",
|
||||
"shortcuts": "Atajos de teclado",
|
||||
"close": "Cerrar"
|
||||
},
|
||||
"comment": {
|
||||
"flagged": "marcado",
|
||||
"anon": "Anónimo"
|
||||
},
|
||||
"configure": {
|
||||
"enable-pre-moderation": "Habilitar pre-moderación",
|
||||
"include-comment-stream": "Incluir la Descripción a un Hilo de Comentario para los y las Lectoras.",
|
||||
"include-comment-stream-desc": "Escribir un mensaje que será agregado a la parte de arriba del tu hilo de comentarios. Por ejemplo, un tema, guias de comunidad, etc.",
|
||||
"include-text": "Incluir tu texto aqui.",
|
||||
"comment-settings": "Configuración de Comentarios",
|
||||
"embed-comment-stream": "Colocar Hilo de Comentarios",
|
||||
"save-changes": "Guardar Cambios",
|
||||
"copy-and-paste": "Copiar y pegar el código de más abajo en tu CMS para colocar la caja de comentarios en tus articulos",
|
||||
"moderate": "Moderar",
|
||||
"configure": "Configurar",
|
||||
"community": "Comunidad"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '../../coral-framework';
|
||||
import {connect} from 'react-redux';
|
||||
import CommentBox from '../../coral-plugin-commentbox/CommentBox';
|
||||
import InfoBox from '../../coral-plugin-infobox/InfoBox';
|
||||
import Content from '../../coral-plugin-commentcontent/CommentContent';
|
||||
import PubDate from '../../coral-plugin-pubdate/PubDate';
|
||||
import Count from '../../coral-plugin-comment-count/CommentCount';
|
||||
@@ -14,11 +15,12 @@ import AuthorName from '../../coral-plugin-author-name/AuthorName';
|
||||
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies';
|
||||
import Pym from 'pym.js';
|
||||
import FlagButton from '../../coral-plugin-flags/FlagButton';
|
||||
import LikeButton from '../../coral-plugin-likes/LikeButton';
|
||||
import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton';
|
||||
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
|
||||
import UserBox from '../../coral-sign-in/components/UserBox';
|
||||
|
||||
const {addItem, updateItem, postItem, getStream, postAction, appendItemArray} = itemActions;
|
||||
const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions;
|
||||
const {addNotification, clearNotification} = notificationActions;
|
||||
const {logout} = authActions;
|
||||
|
||||
@@ -39,6 +41,9 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
addNotification: (type, text) => dispatch(addNotification(type, text)),
|
||||
clearNotification: () => dispatch(clearNotification()),
|
||||
postAction: (item, action, user, itemType) => dispatch(postAction(item, action, user, itemType)),
|
||||
deleteAction: (item, action, user, itemType) => {
|
||||
return dispatch(deleteAction(item, action, user, itemType));
|
||||
},
|
||||
appendItemArray: (item, property, value, addToFront, itemType) =>
|
||||
dispatch(appendItemArray(item, property, value, addToFront, itemType)),
|
||||
logout: () => dispatch(logout())
|
||||
@@ -55,8 +60,9 @@ class CommentStream extends Component {
|
||||
componentDidMount () {
|
||||
// Set up messaging between embedded Iframe an parent component
|
||||
// Using recommended Pym init code which violates .eslint standards
|
||||
new Pym.Child({polling: 500});
|
||||
this.props.getStream('assetTest');
|
||||
const pym = new Pym.Child({polling: 100});
|
||||
const path = /https?\:\/\/([^?]+)/.exec(pym.parentUrl)[1];
|
||||
this.props.getStream(path);
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -76,7 +82,7 @@ class CommentStream extends Component {
|
||||
|
||||
// TODO: Replace teststream id with id from params
|
||||
|
||||
const rootItemId = 'assetTest';
|
||||
const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0];
|
||||
const rootItem = this.props.items.assets && this.props.items.assets[rootItemId];
|
||||
const {loggedIn, user} = this.props.auth;
|
||||
return <div>
|
||||
@@ -84,6 +90,9 @@ class CommentStream extends Component {
|
||||
rootItem
|
||||
? <div>
|
||||
<div id="commentBox">
|
||||
<InfoBox
|
||||
content={this.props.config.infoBoxContent}
|
||||
enable={this.props.config.infoBoxEnable}/>
|
||||
<Count
|
||||
id={rootItemId}
|
||||
items={this.props.items}/>
|
||||
@@ -101,28 +110,40 @@ class CommentStream extends Component {
|
||||
{!loggedIn && <SignInContainer />}
|
||||
</div>
|
||||
{
|
||||
rootItem.comments.map((commentId) => {
|
||||
rootItem.comments && rootItem.comments.map((commentId) => {
|
||||
const comment = this.props.items.comments[commentId];
|
||||
return <div className="comment" key={commentId}>
|
||||
<hr aria-hidden={true}/>
|
||||
<AuthorName name={comment.username}/>
|
||||
<PubDate created_at={comment.created_at}/>
|
||||
<Content body={comment.body}/>
|
||||
<div className="commentActions">
|
||||
<div className="commentActionsLeft">
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
id={commentId}/>
|
||||
<LikeButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={commentId}
|
||||
like={this.props.items.actions[comment.like]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
</div>
|
||||
<div className="commentActionsRight">
|
||||
<FlagButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={commentId}
|
||||
flag={this.props.items.actions[comment.flag]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
id={commentId}/>
|
||||
<PermalinkButton
|
||||
comment_id={commentId}
|
||||
asset_id={comment.asset_id}/>
|
||||
<PermalinkButton
|
||||
comment_id={commentId}
|
||||
asset_id={comment.asset_id}/>
|
||||
</div>
|
||||
<ReplyBox
|
||||
addNotification={this.props.addNotification}
|
||||
@@ -142,23 +163,35 @@ class CommentStream extends Component {
|
||||
<AuthorName name={reply.username}/>
|
||||
<PubDate created_at={reply.created_at}/>
|
||||
<Content body={reply.body}/>
|
||||
<div className="replyActions">
|
||||
<FlagButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={replyId}
|
||||
flag={this.props.items.actions[reply.flag]}
|
||||
postAction={this.props.postAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
parent_id={reply.parent_id}/>
|
||||
<PermalinkButton
|
||||
comment_id={reply.comment_id}
|
||||
asset_id={reply.comment_id}
|
||||
/>
|
||||
</div>
|
||||
<div className="replyActionsLeft">
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
id={replyId}/>
|
||||
<LikeButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={replyId}
|
||||
like={this.props.items.actions[reply.like]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
</div>
|
||||
<div className="replyActionsRight">
|
||||
<FlagButton
|
||||
addNotification={this.props.addNotification}
|
||||
id={replyId}
|
||||
flag={this.props.items.actions[reply.flag]}
|
||||
postAction={this.props.postAction}
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<PermalinkButton
|
||||
comment_id={reply.comment_id}
|
||||
asset_id={reply.comment_id}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,6 +49,23 @@ hr {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Info Box Styles */
|
||||
.coral-plugin-infobox-info {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
border: 0;
|
||||
background: rgb(105,105,105);
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Comment Box Styles */
|
||||
.coral-plugin-commentbox-container {
|
||||
display: flex;
|
||||
@@ -105,16 +122,33 @@ hr {
|
||||
|
||||
/* Comment Action Styles */
|
||||
|
||||
.commentActions, .replyActions {
|
||||
.commentActionsRight, .replyActionsRight {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 50%;
|
||||
}
|
||||
.commentActionsLeft, .replyActionsLeft {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
float: left;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.commentActions .material-icons, .replyActions .material-icons {
|
||||
.commentActionsLeft .material-icons,.commentActionsRight .material-icons,
|
||||
.replyActionsLeft .material-icons, .replyActionsRight .material-icons {
|
||||
font-size: 12px;
|
||||
margin-left: 3px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.likedButton {
|
||||
color: rgb(0,134,227);
|
||||
}
|
||||
|
||||
.flaggedIcon {
|
||||
color: #F00;
|
||||
}
|
||||
|
||||
/* Comment count styles */
|
||||
.coral-plugin-comment-count-text {
|
||||
margin-bottom: 15px;
|
||||
|
||||
@@ -8,6 +8,23 @@ export const ADD_ITEM = 'ADD_ITEM';
|
||||
export const UPDATE_ITEM = 'UPDATE_ITEM';
|
||||
export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY';
|
||||
|
||||
const getInit = (method, body) => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
const init = {method, headers};
|
||||
if (method.toLowerCase() !== 'get') {
|
||||
init.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
return init;
|
||||
};
|
||||
|
||||
const responseHandler = response => {
|
||||
return response.ok ? response.json() : Promise.reject(`${response.status} ${response.statusText}`);
|
||||
};
|
||||
/**
|
||||
* Action creators
|
||||
*/
|
||||
@@ -77,14 +94,10 @@ export const appendItemArray = (id, property, value, add_to_front, item_type) =>
|
||||
* @dispatches
|
||||
* A set of items to the item store
|
||||
*/
|
||||
export function getStream (assetId) {
|
||||
export function getStream (assetUrl) {
|
||||
return (dispatch) => {
|
||||
return fetch(`/api/v1/stream?asset_id=${assetId}`)
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.json() : Promise.reject(`${response.status} ${response.statusText}`);
|
||||
}
|
||||
)
|
||||
return fetch(`/api/v1/stream?asset_url=${encodeURIComponent(assetUrl)}`)
|
||||
.then(responseHandler)
|
||||
.then((json) => {
|
||||
|
||||
/* Add items to the store */
|
||||
@@ -95,6 +108,8 @@ export function getStream (assetId) {
|
||||
}
|
||||
}
|
||||
|
||||
const assetId = json.assets[0].id;
|
||||
|
||||
/* Sort comments by date*/
|
||||
json.comments.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||
const rels = json.comments.reduce((h, item) => {
|
||||
@@ -112,10 +127,7 @@ export function getStream (assetId) {
|
||||
return h;
|
||||
}, {rootComments: [], childComments: {}});
|
||||
|
||||
dispatch(addItem({
|
||||
id: assetId,
|
||||
comments: rels.rootComments,
|
||||
}, 'assets'));
|
||||
dispatch(updateItem(assetId, 'comments', rels.rootComments, 'assets'));
|
||||
|
||||
const childKeys = Object.keys(rels.childComments);
|
||||
for (let i = 0; i < childKeys.length; i++ ) {
|
||||
@@ -148,13 +160,8 @@ export function getStream (assetId) {
|
||||
|
||||
export function getItemsArray (ids) {
|
||||
return (dispatch) => {
|
||||
return fetch(`/v1/item/${ids}`)
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.json()
|
||||
: Promise.reject(`${response.status } ${ response.statusText}`);
|
||||
}
|
||||
)
|
||||
return fetch(`/v1/item/${ids}`, getInit('GET'))
|
||||
.then(responseHandler)
|
||||
.then((json) => {
|
||||
for (let i = 0; i < json.items.length; i++) {
|
||||
dispatch(addItem(json.items[i]));
|
||||
@@ -183,20 +190,8 @@ export function postItem (item, type, id) {
|
||||
if (id) {
|
||||
item.id = id;
|
||||
}
|
||||
let options = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(item),
|
||||
headers: {
|
||||
'Content-Type':'application/json'
|
||||
}
|
||||
};
|
||||
return fetch(`/api/v1/${type}`, options)
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.json()
|
||||
: Promise.reject(`${response.status} ${response.statusText}`);
|
||||
}
|
||||
)
|
||||
return fetch(`/api/v1/${type}`, getInit('POST', item))
|
||||
.then(responseHandler)
|
||||
.then((json) => {
|
||||
dispatch(addItem({...item, id:json.id}, type));
|
||||
return json.id;
|
||||
@@ -227,23 +222,35 @@ export function postAction (item_id, action_type, user_id, item_type) {
|
||||
action_type,
|
||||
user_id
|
||||
};
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type':'application/json'
|
||||
},
|
||||
body: JSON.stringify(action)
|
||||
};
|
||||
|
||||
return fetch(`/api/v1/${item_type}/${item_id}/actions`, options)
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.json()
|
||||
: Promise.reject(`${response.status} ${response.statusText}`);
|
||||
}
|
||||
)
|
||||
.then((json)=>{
|
||||
return json;
|
||||
});
|
||||
return fetch(`/api/v1/${item_type}/${item_id}/actions`, getInit('POST', action))
|
||||
.then(responseHandler);
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* DeleteAction
|
||||
* Deletes an action to an item
|
||||
*
|
||||
* @params
|
||||
* id - the id of the item on which the action is taking place
|
||||
* action - the name of the action
|
||||
* user - the user performing the action
|
||||
* host - the coral host
|
||||
*
|
||||
* @returns
|
||||
* A promise resolving to null or an error
|
||||
*
|
||||
*/
|
||||
|
||||
export function deleteAction (item_id, action_type, user_id, item_type) {
|
||||
return () => {
|
||||
const action = {
|
||||
action_type,
|
||||
user_id
|
||||
};
|
||||
|
||||
return fetch(`/api/v1/${item_type}/${item_id}/actions`, getInit('DELETE', action))
|
||||
.then(responseHandler);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ const name = 'coral-plugin-comment-count';
|
||||
|
||||
const CommentCount = ({items, id}) => {
|
||||
let count = 0;
|
||||
if (items[id]) {
|
||||
count += items[id].comments.length;
|
||||
if (items.assets[id] && items.assets[id].comments) {
|
||||
count += items.assets[id].comments.length;
|
||||
}
|
||||
const itemKeys = Object.keys(items);
|
||||
const itemKeys = Object.keys(items.comments);
|
||||
for (let i = 0; i < itemKeys.length; i++) {
|
||||
const item = items[itemKeys[i]];
|
||||
const item = items.comments[itemKeys[i]];
|
||||
if (item.children) {
|
||||
count += item.children.length;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"comment": "Comment",
|
||||
"name": "Name",
|
||||
"comment-post-notif": "Your comment has been posted.",
|
||||
"comment-post-notif-premod": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly."
|
||||
"comment-post-notif-premod": "Thank you for posting. Our moderation team will review your comment shortly."
|
||||
},
|
||||
"es": {
|
||||
"post": "Publicar",
|
||||
|
||||
@@ -4,27 +4,35 @@ import translations from './translations.json';
|
||||
|
||||
const name = 'coral-plugin-flags';
|
||||
|
||||
const FlagButton = ({flag, id, postAction, addItem, updateItem, addNotification}) => {
|
||||
const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, addNotification}) => {
|
||||
const flagged = flag && flag.current_user;
|
||||
const onFlagClick = () => {
|
||||
postAction(id, 'flag', '123', 'comments')
|
||||
.then((action) => {
|
||||
addItem({...action, current_user:true}, 'actions');
|
||||
updateItem(action.item_id, action.action_type, action.id, 'comments');
|
||||
});
|
||||
addNotification('success', lang.t('flag-notif'));
|
||||
if (!flagged) {
|
||||
postAction(id, 'flag', '123', 'comments')
|
||||
.then((action) => {
|
||||
addItem({...action, current_user:true}, 'actions');
|
||||
updateItem(action.item_id, action.action_type, action.id, 'comments');
|
||||
});
|
||||
addNotification('success', lang.t('flag-notif'));
|
||||
} else {
|
||||
deleteAction(id, 'flag', '123', 'comments')
|
||||
.then(() => {
|
||||
updateItem(id, 'flag', '', 'comments');
|
||||
});
|
||||
addNotification('success', lang.t('flag-notif-remove'));
|
||||
}
|
||||
};
|
||||
|
||||
return <div className={`${name }-container`}>
|
||||
<button onClick={onFlagClick} className={`${name }-button`}>
|
||||
<i className={`${name }-icon material-icons`}
|
||||
style={flagged ? styles.flaggedIcon : styles.unflaggedIcon}
|
||||
aria-hidden={true}>flag</i>
|
||||
return <div className={`${name}-container`}>
|
||||
<button onClick={onFlagClick} className={`${name}-button`}>
|
||||
{
|
||||
flagged
|
||||
? <span className={`${name}-button-text`}>{lang.t('flag')}</span>
|
||||
: <span className={`${name}-button-text`}>{lang.t('flagged')}</span>
|
||||
? <span className={`${name}-button-text`}>{lang.t('flagged')}</span>
|
||||
: <span className={`${name}-button-text`}>{lang.t('flag')}</span>
|
||||
}
|
||||
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
|
||||
style={flagged && styles.flaggedIcon}
|
||||
aria-hidden={true}>flag</i>
|
||||
</button>
|
||||
</div>;
|
||||
};
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
"en": {
|
||||
"flag": "Flag",
|
||||
"flagged": "Flagged",
|
||||
"flag-notif": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly."
|
||||
"flag-notif": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.",
|
||||
"flag-notif-remove": "Your flag has been removed."
|
||||
},
|
||||
"es": {
|
||||
"flag": "Marcar",
|
||||
"flagged": "Marcado",
|
||||
"flag-notif": "Gracias por marcar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar."
|
||||
"flag-notif": "Gracias por marcar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar.",
|
||||
"flag-notif-remove": "¡traduceme!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
const packagename = 'coral-plugin-infobox';
|
||||
|
||||
const InfoBox = ({enable, content}) =>
|
||||
<div
|
||||
className={`${packagename}-info ${enable ? null : ', hidden'}` }>
|
||||
{content}
|
||||
</div>;
|
||||
|
||||
export default InfoBox;
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {expect} from 'chai';
|
||||
import InfoBox from '../InfoBox';
|
||||
|
||||
describe('InfoBox', () => {
|
||||
let comment;
|
||||
let render;
|
||||
beforeEach(() => {
|
||||
comment = {};
|
||||
const postItem = (item) => {
|
||||
comment.posted = item;
|
||||
return Promise.resolve(4);
|
||||
};
|
||||
render = shallow(<InfoBox
|
||||
postItem={postItem}
|
||||
updateItem={(e) => comment.text = e.target.value}
|
||||
item_id={'1'}
|
||||
comments={['1', '2', '3']}/>);
|
||||
});
|
||||
|
||||
it('should render the InfoBox appropriately', () => {
|
||||
expect(render.contains('<div class="InfoBox"')).to.be.truthy;
|
||||
expect(render.contains('<button class="postCommentButton"')).to.be.truthy;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
import translations from './translations.json';
|
||||
|
||||
const name = 'coral-plugin-flags';
|
||||
|
||||
const LikeButton = ({like, id, postAction, deleteAction, addItem, updateItem}) => {
|
||||
const liked = like && like.current_user;
|
||||
const onLikeClick = () => {
|
||||
if (!liked) {
|
||||
postAction(id, 'like', '123', 'comments')
|
||||
.then((action) => {
|
||||
addItem({id: action.id, current_user:true, count: like ? like.count + 1 : 1}, 'actions');
|
||||
updateItem(action.item_id, action.action_type, action.id, 'comments');
|
||||
});
|
||||
} else {
|
||||
deleteAction(id, 'like', '123', 'comments')
|
||||
.then(() => {
|
||||
updateItem(like.id, 'count', like.count - 1, 'actions');
|
||||
updateItem(like.id, 'current_user', false, 'actions');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return <div className={`${name}-container`}>
|
||||
<button onClick={onLikeClick} className={`${name}-button ${liked && 'likedButton'}`}>
|
||||
{
|
||||
liked
|
||||
? <span className={`${name}-button-text`}>{lang.t('liked')}</span>
|
||||
: <span className={`${name}-button-text`}>{lang.t('like')}</span>
|
||||
}
|
||||
<i className={`${name}-icon material-icons`}
|
||||
aria-hidden={true}>thumb_up</i>
|
||||
<span className={`${name}-like-count`}>{like && like.count > 0 && like.count}</span>
|
||||
</button>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default LikeButton;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"en": {
|
||||
"like": "Like",
|
||||
"liked": "Liked"
|
||||
},
|
||||
"es": {
|
||||
"like": "Me Gusta",
|
||||
"liked": "Me Gustó"
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@ const name = 'coral-plugin-replies';
|
||||
const ReplyButton = (props) => <button
|
||||
className={`${name}-reply-button`}
|
||||
onClick={() => props.updateItem(props.id || props.parent_id, 'showReply', true, 'comments')}>
|
||||
{lang.t('reply')}
|
||||
<i className={`${name}-icon material-icons`}
|
||||
aria-hidden={true}>reply</i>
|
||||
{lang.t('reply')}
|
||||
</button>;
|
||||
|
||||
export default ReplyButton;
|
||||
|
||||
+11
-1
@@ -62,11 +62,21 @@ AssetSchema.statics.findByUrl = function(url) {
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a asset by its url.
|
||||
* @param {String} url identifier of the asset (uuid).
|
||||
*/
|
||||
AssetSchema.statics.findOrCreateByUrl = function(url) {
|
||||
|
||||
return Asset.findOne({url})
|
||||
.then((asset) => asset ? asset
|
||||
: Asset.upsert({url}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Upserts an asset.
|
||||
*/
|
||||
AssetSchema.statics.upsert = function(data) {
|
||||
|
||||
// If an id is not sent, create one.
|
||||
if (typeof data.id === 'undefined') {
|
||||
data.id = uuid.v4();
|
||||
|
||||
+18
-3
@@ -104,14 +104,14 @@ CommentSchema.statics.findByStatusByActionType = function(status, action_type) {
|
||||
return Action
|
||||
.findCommentsIdByActionType(action_type, 'comment')
|
||||
.then((actions) => {
|
||||
|
||||
|
||||
return Comment.find({
|
||||
'status': status,
|
||||
'status': status,
|
||||
'id': {
|
||||
'$in': actions.map(a => {
|
||||
return a.item_id;
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -188,6 +188,21 @@ CommentSchema.statics.removeById = function(id) {
|
||||
return Comment.remove({'id': id});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove an action from the comment.
|
||||
* @param {String} id identifier of the comment (uuid)
|
||||
* @param {String} action_type the type of the action to be removed
|
||||
* @param {String} user_id the id of the user performing the action
|
||||
*/
|
||||
CommentSchema.statics.removeAction = function(item_id, user_id, action_type) {
|
||||
return Action.remove({
|
||||
action_type,
|
||||
item_type: 'comment',
|
||||
item_id,
|
||||
user_id
|
||||
});
|
||||
};
|
||||
|
||||
const Comment = mongoose.model('Comment', CommentSchema);
|
||||
|
||||
module.exports = Comment;
|
||||
|
||||
+11
-1
@@ -3,7 +3,9 @@ const Schema = mongoose.Schema;
|
||||
|
||||
const SettingSchema = new Schema({
|
||||
id: {type: String, default: '1'},
|
||||
moderation: {type: String, enum: ['pre', 'post'], default: 'pre'}
|
||||
moderation: {type: String, enum: ['pre', 'post'], default: 'pre'},
|
||||
infoBoxEnable: {type: Boolean, default: false},
|
||||
infoBoxContent: {type: String, default: ''}
|
||||
}, {
|
||||
timestamps: {
|
||||
createdAt: 'created_at',
|
||||
@@ -35,6 +37,14 @@ SettingSchema.statics.getModerationSetting = function () {
|
||||
return this.findOne({id: '1'}).select('moderation');
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the info box settings and sends it back
|
||||
* @return {Promise} content the content of the info Box
|
||||
*/
|
||||
SettingSchema.statics.getInfoBoxSetting = function () {
|
||||
return this.findOne({id: '1'}).select('infoBoxEnable', 'infoBoxContent');
|
||||
};
|
||||
|
||||
/**
|
||||
* This will update the settings object with whatever you pass in
|
||||
* @param {object} setting a hash of whatever settings you want to update
|
||||
|
||||
+171
-63
@@ -2,30 +2,76 @@ const mongoose = require('../mongoose');
|
||||
const uuid = require('uuid');
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
// SALT_ROUNDS is the number of rounds that the bcrypt algorithm will run
|
||||
// through during the salting process.
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
// USER_ROLES is the array of roles that is permissible as a user role.
|
||||
const USER_ROLES = [
|
||||
'admin',
|
||||
'moderator'
|
||||
];
|
||||
|
||||
// UserSchema is the mongoose schema defined as the representation of a User in
|
||||
// MongoDB.
|
||||
const UserSchema = new mongoose.Schema({
|
||||
|
||||
// This ID represents the most unique identifier for a user, it is generated
|
||||
// when the user is created as a random uuid.
|
||||
id: {
|
||||
type: String,
|
||||
default: uuid.v4,
|
||||
unique: true,
|
||||
required: true
|
||||
},
|
||||
|
||||
// This is sourced from the social provider or set manually during user setup
|
||||
// and simply provides a name to display for the given user.
|
||||
displayName: String,
|
||||
photo: String,
|
||||
|
||||
// This is true when the user account is disabled, no action should be
|
||||
// acknowledged when they are disabled. Logins are also prevented.
|
||||
disabled: Boolean,
|
||||
|
||||
// This provides a source of identity proof for users who login using the
|
||||
// local provider. A local provider will be assumed for users who do not
|
||||
// have any social profiles.
|
||||
password: String,
|
||||
profiles: [{
|
||||
|
||||
// Profiles describes the array of identities for a given user. Any one user
|
||||
// can have multiple profiles associated with them, including multiple email
|
||||
// addresses.
|
||||
profiles: [new mongoose.Schema({
|
||||
|
||||
// ID provides the identifier for the user profile, in the case of a local
|
||||
// provider, the id would be an email, in the case of a social provider,
|
||||
// the id would be the foreign providers identifier.
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
// Provider is simply the name attached to the authentication mode. In the
|
||||
// case of a locally provided profile, this will simply be `local`, or a
|
||||
// social provider which for Facebook would just be `facebook`.
|
||||
provider: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}],
|
||||
}, {
|
||||
_id: false
|
||||
})],
|
||||
|
||||
// Roles provides an array of roles (as strings) that is associated with a
|
||||
// user.
|
||||
roles: [String]
|
||||
}, {
|
||||
|
||||
// This will ensure that we have proper timestamps available on this model.
|
||||
timestamps: {
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
}
|
||||
});
|
||||
|
||||
// Add the indixies on the user profile data.
|
||||
@@ -53,6 +99,29 @@ UserSchema.options.toJSON.transform = (doc, ret, options) => {
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* toObject overrides to remove the password field from the toObject
|
||||
* output.
|
||||
*/
|
||||
UserSchema.options.toObject = {};
|
||||
UserSchema.options.toObject.hide = 'password';
|
||||
UserSchema.options.toObject.transform = (doc, ret, options) => {
|
||||
if (options.hide) {
|
||||
options.hide.split(' ').forEach((prop) => {
|
||||
delete ret[prop];
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Create the User model.
|
||||
const UserModel = mongoose.model('User', UserSchema);
|
||||
|
||||
// UserService is the interface for the application to interact with the
|
||||
// UserModel through.
|
||||
const UserService = module.exports = {};
|
||||
|
||||
/**
|
||||
* Finds a user given their email address that we have for them in the system
|
||||
* and ensures that the retuned user matches the password passed in as well.
|
||||
@@ -60,8 +129,8 @@ UserSchema.options.toJSON.transform = (doc, ret, options) => {
|
||||
* @param {string} password - password to match against the found user
|
||||
* @param {Function} done [description]
|
||||
*/
|
||||
UserSchema.statics.findLocalUser = function(email, password) {
|
||||
return User.findOne({
|
||||
UserService.findLocalUser = (email, password) => {
|
||||
return UserModel.findOne({
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id: email,
|
||||
@@ -98,22 +167,24 @@ UserSchema.statics.findLocalUser = function(email, password) {
|
||||
* @param {String} srcUserID id of the user to which is the source of the merge
|
||||
* @return {Promise} resolves when the users are merged
|
||||
*/
|
||||
UserSchema.statics.mergeUsers = function(dstUserID, srcUserID) {
|
||||
UserService.mergeUsers = (dstUserID, srcUserID) => {
|
||||
let srcUser, dstUser;
|
||||
|
||||
return Promise.all([
|
||||
User.findOne({id: dstUserID}).exec(),
|
||||
User.findOne({id: srcUserID}).exec()
|
||||
]).then((users) => {
|
||||
dstUser = users[0];
|
||||
srcUser = users[1];
|
||||
return Promise
|
||||
.all([
|
||||
UserModel.findOne({id: dstUserID}).exec(),
|
||||
UserModel.findOne({id: srcUserID}).exec()
|
||||
])
|
||||
.then((users) => {
|
||||
dstUser = users[0];
|
||||
srcUser = users[1];
|
||||
|
||||
srcUser.profiles.forEach((profile) => {
|
||||
dstUser.profiles.push(profile);
|
||||
});
|
||||
srcUser.profiles.forEach((profile) => {
|
||||
dstUser.profiles.push(profile);
|
||||
});
|
||||
|
||||
return srcUser.remove();
|
||||
})
|
||||
return srcUser.remove();
|
||||
})
|
||||
.then(() => dstUser.save());
|
||||
};
|
||||
|
||||
@@ -123,38 +194,38 @@ UserSchema.statics.mergeUsers = function(dstUserID, srcUserID) {
|
||||
* @param {Object} profile - User social/external profile
|
||||
* @param {Function} done [description]
|
||||
*/
|
||||
UserSchema.statics.findOrCreateExternalUser = function(profile) {
|
||||
return User.findOne({
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id: profile.id,
|
||||
provider: profile.provider
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((user) => {
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
|
||||
// The user was not found, lets create them!
|
||||
user = new User({
|
||||
displayName: profile.displayName,
|
||||
roles: [],
|
||||
photo: Array.isArray(profile.photos) && profile.photos.length > 0 ? profile.photos[0].value : null,
|
||||
profiles: [
|
||||
{
|
||||
UserService.findOrCreateExternalUser = (profile) => {
|
||||
return UserModel
|
||||
.findOne({
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id: profile.id,
|
||||
provider: profile.provider
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
})
|
||||
.then((user) => {
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
|
||||
return user.save();
|
||||
});
|
||||
// The user was not found, lets create them!
|
||||
user = new UserModel({
|
||||
displayName: profile.displayName,
|
||||
roles: [],
|
||||
profiles: [
|
||||
{
|
||||
id: profile.id,
|
||||
provider: profile.provider
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return user.save();
|
||||
});
|
||||
};
|
||||
|
||||
UserSchema.statics.changePassword = function(id, password) {
|
||||
UserService.changePassword = (id, password) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => {
|
||||
if (err) {
|
||||
@@ -165,7 +236,7 @@ UserSchema.statics.changePassword = function(id, password) {
|
||||
});
|
||||
})
|
||||
.then((hashedPassword) => {
|
||||
return User.update({id}, {
|
||||
return UserModel.update({id}, {
|
||||
$set: {
|
||||
password: hashedPassword
|
||||
}
|
||||
@@ -178,9 +249,9 @@ UserSchema.statics.changePassword = function(id, password) {
|
||||
* @param {Array} users Users to create
|
||||
* @return {Promise} Resolves with the users that were created
|
||||
*/
|
||||
UserSchema.statics.createLocalUsers = function(users) {
|
||||
UserService.createLocalUsers = (users) => {
|
||||
return Promise.all(users.map((user) => {
|
||||
return User
|
||||
return UserService
|
||||
.createLocalUser(user.email, user.password, user.displayName);
|
||||
}));
|
||||
};
|
||||
@@ -192,7 +263,7 @@ UserSchema.statics.createLocalUsers = function(users) {
|
||||
* @param {String} displayName name of the display user
|
||||
* @param {Function} done callback
|
||||
*/
|
||||
UserSchema.statics.createLocalUser = function(email, password, displayName) {
|
||||
UserService.createLocalUser = (email, password, displayName) => {
|
||||
if (!email) {
|
||||
return Promise.reject('email is required');
|
||||
}
|
||||
@@ -211,7 +282,7 @@ UserSchema.statics.createLocalUser = function(email, password, displayName) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
let user = new User({
|
||||
let user = new UserModel({
|
||||
displayName: displayName,
|
||||
password: hashedPassword,
|
||||
roles: [],
|
||||
@@ -239,8 +310,8 @@ UserSchema.statics.createLocalUser = function(email, password, displayName) {
|
||||
* @param {String} id id of a user
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
UserSchema.statics.disableUser = function(id) {
|
||||
return User.update({
|
||||
UserService.disableUser = (id) => {
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$set: {
|
||||
@@ -254,8 +325,8 @@ UserSchema.statics.disableUser = function(id) {
|
||||
* @param {String} id id of a user
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
UserSchema.statics.enableUser = function(id) {
|
||||
return User.update({
|
||||
UserService.enableUser = (id) => {
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$set: {
|
||||
@@ -270,8 +341,16 @@ UserSchema.statics.enableUser = function(id) {
|
||||
* @param {String} role role to add
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
UserSchema.statics.addRoleToUser = function(id, role) {
|
||||
return User.update({
|
||||
UserService.addRoleToUser = (id, role) => {
|
||||
|
||||
// Check to see if the user role is in the allowable set of roles.
|
||||
if (USER_ROLES.indexOf(role) === -1) {
|
||||
|
||||
// User role is not supported! Error out here.
|
||||
return Promise.reject(new Error(`role ${role} is not supported`));
|
||||
}
|
||||
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$addToSet: {
|
||||
@@ -286,8 +365,8 @@ UserSchema.statics.addRoleToUser = function(id, role) {
|
||||
* @param {String} role role to remove
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
UserSchema.statics.removeRoleFromUser = function(id, role) {
|
||||
return User.update({
|
||||
UserService.removeRoleFromUser = (id, role) => {
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
$pull: {
|
||||
@@ -300,21 +379,50 @@ UserSchema.statics.removeRoleFromUser = function(id, role) {
|
||||
* Finds a user with the id.
|
||||
* @param {String} id user id (uuid)
|
||||
*/
|
||||
UserSchema.statics.findById = function(id) {
|
||||
return User.findOne({id});
|
||||
UserService.findById = (id) => {
|
||||
return UserModel.findOne({id});
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds users in an array of idd.
|
||||
* @param {Array} ids array of user identifiers (uuid)
|
||||
*/
|
||||
UserSchema.statics.findByIdArray = function(ids) {
|
||||
return User.find({
|
||||
UserService.findByIdArray = (ids) => {
|
||||
return UserModel.find({
|
||||
'id': {$in: ids}
|
||||
});
|
||||
};
|
||||
|
||||
const User = mongoose.model('User', UserSchema);
|
||||
/**
|
||||
* Finds a user using a value which gets compared using a prefix match against
|
||||
* the user's email address and/or their display name.
|
||||
* @param {String} value value to search by
|
||||
* @return {Promise}
|
||||
*/
|
||||
UserService.search = (value) => {
|
||||
return UserModel.find({
|
||||
$or: [
|
||||
|
||||
module.exports = User;
|
||||
module.exports.Schema = UserSchema;
|
||||
// Search by a prefix match on the displayName.
|
||||
{
|
||||
'displayName': {
|
||||
$regex: new RegExp(`^${value}`),
|
||||
$options: 'i'
|
||||
}
|
||||
},
|
||||
|
||||
// Search by a prefix match on the email address.
|
||||
{
|
||||
'profiles': {
|
||||
$elemMatch: {
|
||||
id: {
|
||||
$regex: new RegExp(`^${value}`),
|
||||
$options: 'i'
|
||||
},
|
||||
provider: 'local'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
+7
-16
@@ -5,24 +5,19 @@
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "./bin/www",
|
||||
"build": "webpack --config webpack.config.js --bail",
|
||||
"build-watch": "webpack --config webpack.config.dev.js --watch",
|
||||
"build": "NODE_ENV=production webpack --config webpack.config.js --bail",
|
||||
"build-watch": "NODE_ENV=development webpack --config webpack.config.dev.js --watch",
|
||||
"lint": "eslint bin/* .",
|
||||
"lint-fix": "eslint . --fix",
|
||||
"test": "mocha --compilers js:babel-core/register --recursive tests",
|
||||
"test-watch": "mocha --compilers js:babel-core/register --recursive -w tests",
|
||||
"embed-start": "npm run build && ./bin/www"
|
||||
"embed-start": "NODE_ENV=development npm run build && ./bin/www"
|
||||
},
|
||||
"config": {
|
||||
"pre-git": {
|
||||
"commit-msg": [],
|
||||
"pre-commit": [
|
||||
"npm run lint",
|
||||
"npm test"
|
||||
],
|
||||
"pre-push": [
|
||||
"npm test"
|
||||
],
|
||||
"pre-commit": ["npm run lint", "npm test"],
|
||||
"pre-push": ["npm test"],
|
||||
"post-commit": [],
|
||||
"post-merge": []
|
||||
}
|
||||
@@ -31,12 +26,7 @@
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/coralproject/talk.git"
|
||||
},
|
||||
"keywords": [
|
||||
"talk",
|
||||
"coral",
|
||||
"coralproject",
|
||||
"ask"
|
||||
],
|
||||
"keywords": ["talk", "coral", "coralproject", "ask"],
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
@@ -111,6 +101,7 @@
|
||||
"react": "15.3.2",
|
||||
"react-dom": "15.3.2",
|
||||
"react-mdl": "^1.7.2",
|
||||
"react-mdl-selectfield": "^0.2.0",
|
||||
"react-onclickoutside": "^5.7.1",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^3.0.0",
|
||||
|
||||
@@ -126,7 +126,19 @@ router.delete('/:comment_id', (req, res, next) => {
|
||||
Comment
|
||||
.removeById(req.params.comment_id)
|
||||
.then(() => {
|
||||
res.status(201).send('OK. Removed');
|
||||
res.status(201).send({});
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
});
|
||||
});
|
||||
|
||||
router.delete('/:comment_id/actions', (req, res, next) => {
|
||||
console.log(req.params);
|
||||
Comment
|
||||
.removeAction(req.params.comment_id, req.body.user_id, req.body.action_type)
|
||||
.then(() => {
|
||||
res.status(201).send({});
|
||||
})
|
||||
.catch(error => {
|
||||
next(error);
|
||||
|
||||
@@ -3,35 +3,45 @@ const express = require('express');
|
||||
const Comment = require('../../../models/comment');
|
||||
const User = require('../../../models/user');
|
||||
const Action = require('../../../models/action');
|
||||
const Asset = require('../../../models/asset');
|
||||
|
||||
const Setting = require('../../../models/setting');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Find all the comments by a specific asset_id.
|
||||
// Find all the comments by a specific asset_url.
|
||||
// . if pre: get the comments that are accepted.
|
||||
// . if post: get the comments that are new and accepted.
|
||||
router.get('/', (req, res, next) => {
|
||||
const commentsPromise = Setting.getModerationSetting().then(({moderation}) => {
|
||||
|
||||
// Get the asset_id for this url (or create it if it doesn't exist)
|
||||
Promise.all([
|
||||
Asset.findOrCreateByUrl(decodeURIComponent(req.query.asset_url)),
|
||||
Setting.getModerationSetting()
|
||||
])
|
||||
.then(([asset, {moderation}]) => {
|
||||
// Get the sitewide moderation setting and return the appropriate comments
|
||||
switch(moderation){
|
||||
case 'pre':
|
||||
return Comment.findAcceptedByAssetId(req.query.asset_id);
|
||||
return Promise.all([Comment.findAcceptedByAssetId(asset.id), asset]);
|
||||
case 'post':
|
||||
return Comment.findAcceptedAndNewByAssetId(req.query.asset_id);
|
||||
return Promise.all([Comment.findAcceptedAndNewByAssetId(asset.id), asset]);
|
||||
default:
|
||||
throw new Error('Moderation setting not found.');
|
||||
}
|
||||
});
|
||||
|
||||
})
|
||||
// Get all the users and actions for those comments.
|
||||
commentsPromise.then(comments => {
|
||||
.then(([comments, asset]) => {
|
||||
return Promise.all([
|
||||
[asset],
|
||||
comments,
|
||||
User.findByIdArray(comments.map((comment) => comment.author_id)),
|
||||
Action.getActionSummaries(comments.map((comment) => comment.id))
|
||||
]);
|
||||
}).then(([comments, users, actions]) => {
|
||||
})
|
||||
.then(([assets, comments, users, actions]) => {
|
||||
res.json({
|
||||
assets,
|
||||
comments,
|
||||
users,
|
||||
actions
|
||||
|
||||
+15
-23
@@ -11,28 +11,9 @@ router.get('/', (req, res, next) => {
|
||||
limit = 50 // Total Per Page
|
||||
} = req.query;
|
||||
|
||||
let q = {
|
||||
$or: [
|
||||
{
|
||||
'displayName': {
|
||||
$regex: new RegExp(`^${value}`),
|
||||
$options: 'i'
|
||||
},
|
||||
'profiles': {
|
||||
$elemMatch: {
|
||||
id: {
|
||||
$regex: new RegExp(`^${value}`),
|
||||
$options: 'i'
|
||||
},
|
||||
provider: 'local'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
Promise.all([
|
||||
User.find(q)
|
||||
User
|
||||
.search(value)
|
||||
.sort({[field]: (asc === 'true') ? 1 : -1})
|
||||
.skip((page - 1) * limit)
|
||||
.limit(limit),
|
||||
@@ -40,11 +21,13 @@ router.get('/', (req, res, next) => {
|
||||
])
|
||||
.then(([data, count]) => {
|
||||
const users = data.map((user) => {
|
||||
const {displayName, created_at} = user;
|
||||
const {id, displayName, created_at} = user;
|
||||
return {
|
||||
id,
|
||||
displayName,
|
||||
created_at,
|
||||
profiles: user.toObject().profiles
|
||||
profiles: user.toObject().profiles,
|
||||
roles: user.toObject().roles
|
||||
};
|
||||
});
|
||||
|
||||
@@ -60,6 +43,15 @@ router.get('/', (req, res, next) => {
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
router.post('/:user_id/role', (req, res, next) => {
|
||||
User
|
||||
.addRoleToUser(req.params.user_id, req.body.role)
|
||||
.then(role => {
|
||||
res.send(role);
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
router.post('/', (req, res, next) => {
|
||||
const {email, password, displayName} = req.body;
|
||||
|
||||
|
||||
+5
-1
@@ -6,7 +6,11 @@ router.use('/admin', require('./admin'));
|
||||
router.use('/embed', require('./embed'));
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.render('home', {});
|
||||
return res.render('article', {title: 'Coral Talk'});
|
||||
});
|
||||
|
||||
router.get('/assets/:asset_title', (req, res) => {
|
||||
return res.render('article', {title: req.params.asset_title.split('-').join(' ')});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -18,8 +18,11 @@ describe('itemActions', () => {
|
||||
});
|
||||
|
||||
describe('getStream', () => {
|
||||
const rootId = '1234';
|
||||
const assetUrl = 'http://www.test.com';
|
||||
const response = {
|
||||
assets: [{
|
||||
id: '1234', url: assetUrl
|
||||
}],
|
||||
comments: [
|
||||
{body: 'stuff', id: '123'},
|
||||
{body: 'morestuff', id: '456'}
|
||||
@@ -42,17 +45,17 @@ describe('itemActions', () => {
|
||||
|
||||
it('should get an stream from an asset_id and send the appropriate dispatches', () => {
|
||||
fetchMock.get('*', JSON.stringify(response));
|
||||
return actions.getStream(rootId)(store.dispatch)
|
||||
return actions.getStream(assetUrl)(store.dispatch)
|
||||
.then((res) => {
|
||||
expect(fetchMock.calls().matched[0][0]).to.equal('/api/v1/stream?asset_id=1234');
|
||||
expect(fetchMock.calls().matched[0][0]).to.equal('/api/v1/stream?asset_url=http%3A%2F%2Fwww.test.com');
|
||||
expect(res).to.deep.equal(response);
|
||||
expect(store.getActions()[0]).to.deep.equal({
|
||||
expect(store.getActions()[1]).to.deep.equal({
|
||||
type: actions.ADD_ITEM,
|
||||
item: response.comments[0],
|
||||
item_type: 'comments',
|
||||
id: '123'
|
||||
});
|
||||
expect(store.getActions()[1]).to.deep.equal({
|
||||
expect(store.getActions()[2]).to.deep.equal({
|
||||
type: actions.ADD_ITEM,
|
||||
item: response.comments[1],
|
||||
item_type: 'comments',
|
||||
@@ -62,7 +65,7 @@ describe('itemActions', () => {
|
||||
});
|
||||
it('should handle an error', () => {
|
||||
fetchMock.get('*', 404);
|
||||
return actions.getStream(rootId)(store.dispatch)
|
||||
return actions.getStream(assetUrl)(store.dispatch)
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy;
|
||||
});
|
||||
@@ -119,6 +122,7 @@ describe('itemActions', () => {
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type':'application/json'
|
||||
},
|
||||
body: JSON.stringify(item.data)
|
||||
@@ -162,6 +166,24 @@ describe('itemActions', () => {
|
||||
expect(err).to.be.truthy;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAction', () => {
|
||||
it ('should remove an action', () => {
|
||||
fetchMock.delete('*', {});
|
||||
return actions.deleteAction('abc', 'flag', '123', 'comments')(store.dispatch)
|
||||
.then(response => {
|
||||
expect(fetchMock.calls().matched[0][0]).to.equal('/api/v1/comments/abc/actions');
|
||||
expect(response).to.deep.equal({});
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle an error', () => {
|
||||
fetchMock.post('*', 404);
|
||||
return actions.postAction('abc', 'flag', '123')(store.dispatch)
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+97
-84
@@ -1,95 +1,108 @@
|
||||
/* eslint-env node, mocha */
|
||||
|
||||
require('../utils/mongoose');
|
||||
|
||||
const chai = require('chai');
|
||||
const expect = chai.expect;
|
||||
const server = require('../../app');
|
||||
const Asset = require('../../models/asset');
|
||||
const expect = require('chai').expect;
|
||||
|
||||
// Setup chai.
|
||||
chai.should();
|
||||
chai.use(require('chai-http'));
|
||||
describe('Asset: model', () => {
|
||||
|
||||
let fixture = {
|
||||
'url': 'http://hhgg.com/total-perspective-vortex',
|
||||
'type': 'article',
|
||||
'headline': 'The Total Perspective Vortex',
|
||||
'summary': 'You are an insignificant dot on an insignificant dot.',
|
||||
'section': 'Everything',
|
||||
'authors': ['Ford Prefect']
|
||||
};
|
||||
beforeEach(() => {
|
||||
const defaults = {url:'http://test.com'};
|
||||
return Asset.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
|
||||
});
|
||||
|
||||
describe('Asset: models', () => {
|
||||
|
||||
describe('/GET Asset', () => {
|
||||
describe('#get', () => {
|
||||
it('It should get an empty array when there are no assets.', (done) => {
|
||||
|
||||
chai.request(server)
|
||||
.get('/api/v1/asset')
|
||||
.end((err, res) => {
|
||||
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
|
||||
res.should.have.status(200);
|
||||
res.body.should.be.a('array');
|
||||
res.body.length.should.be.eql(0);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
describe('#findById', ()=> {
|
||||
it('should find an asset by the id', () => {
|
||||
return Asset.findById(1)
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('url')
|
||||
.and.to.equal('http://test.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// This test checks PUT and read
|
||||
describe('/PUT Asset', () => {
|
||||
describe('#put', () => {
|
||||
it('It should save an asset and load it again.', (done) => {
|
||||
|
||||
chai.request(server)
|
||||
.put('/api/v1/asset')
|
||||
.send(fixture)
|
||||
.end((err, res) => {
|
||||
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
|
||||
res.should.have.status(200);
|
||||
res.body.should.be.a('object');
|
||||
|
||||
// Id should be generated by the model if absent.
|
||||
res.body.should.have.property('id');
|
||||
|
||||
// Save the asset id to compare with GET result.
|
||||
let assetId = res.body.id;
|
||||
|
||||
// Load the asset to make sure it's really there.
|
||||
chai.request(server)
|
||||
.get(`/api/v1/asset?url=${encodeURIComponent(fixture.url)}`)
|
||||
.end((err, res) => {
|
||||
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
|
||||
res.should.have.status(200);
|
||||
res.body.should.be.an('array');
|
||||
|
||||
let asset = res.body[0];
|
||||
|
||||
expect(asset).to.have.property('id');
|
||||
|
||||
// Ensure the asset has the same id as above.
|
||||
// This tests the single url per Id concept.
|
||||
expect(assetId).to.equal(asset.id);
|
||||
|
||||
done();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#findByUrl', ()=> {
|
||||
it('should find an asset by a url', () => {
|
||||
return Asset.findByUrl('http://test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('url')
|
||||
.and.to.equal('http://test.com');
|
||||
});
|
||||
});
|
||||
}); // End describe /PUT Asset
|
||||
|
||||
it('should return null when a url does not exist', () => {
|
||||
return Asset.findByUrl('http://new.test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.be.null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#findOrCreateByUrl', ()=> {
|
||||
it('should find an asset by a url', () => {
|
||||
return Asset.findOrCreateByUrl('http://test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('url')
|
||||
.and.to.equal('http://test.com');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a new asset when the url does not exist', () => {
|
||||
return Asset.findOrCreateByUrl('http://new.test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('id')
|
||||
.and.to.not.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#findOrCreateByUrl', ()=> {
|
||||
it('should find an asset by a url', () => {
|
||||
return Asset.findOrCreateByUrl('http://test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('url')
|
||||
.and.to.equal('http://test.com');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a new asset when the url does not exist', () => {
|
||||
return Asset.findOrCreateByUrl('http://new.test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('id')
|
||||
.and.to.not.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#upsert', ()=> {
|
||||
it('should insert an asset with no id', () => {
|
||||
return Asset.upsert({url: 'http://newasset.test.com'})
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('id');
|
||||
});
|
||||
});
|
||||
|
||||
it('should update an asset when the id exists', () => {
|
||||
return Asset.upsert({id: 1, url: 'http://new.test.com'})
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('id')
|
||||
.and.to.equal('1');
|
||||
expect(asset).to.have.property('url')
|
||||
.and.to.equal('http://new.test.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#removeAll', ()=> {
|
||||
it('should insert an asset with no id', () => {
|
||||
return Asset.removeAll({id:1})
|
||||
.then(() => {
|
||||
return Asset.findById(1);
|
||||
})
|
||||
.then((result) => {
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,4 +134,15 @@ describe('Comment: models', () => {
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
||||
describe('#removeAction', () => {
|
||||
it('should remove an action', () => {
|
||||
return Comment.removeAction('3', '123', 'flag').then(() => {
|
||||
return Action.findByItemIdArray(['123']);
|
||||
})
|
||||
.then((actions) => {
|
||||
expect(actions.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+10
-2
@@ -8,7 +8,7 @@ const expect = require('chai').expect;
|
||||
describe('Setting: model', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
const defaults = {id: 1, moderation: 'pre'};
|
||||
const defaults = {id: 1};
|
||||
return Setting.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
|
||||
});
|
||||
|
||||
@@ -18,13 +18,21 @@ describe('Setting: model', () => {
|
||||
expect(settings).to.have.property('moderation').and.to.equal('pre');
|
||||
});
|
||||
});
|
||||
it('should have two infoBox fields defined', () => {
|
||||
return Setting.getSettings().then(settings => {
|
||||
expect(settings).to.have.property('infoBoxEnable').and.to.equal(false);
|
||||
expect(settings).to.have.property('infoBoxContent').and.to.equal('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateSettings()', () => {
|
||||
it('should update the settings with a passed object', () => {
|
||||
const mockSettings = {moderation: 'post'};
|
||||
const mockSettings = {moderation: 'post', infoBoxEnable: true, infoBoxContent: 'yeah'};
|
||||
return Setting.updateSettings(mockSettings).then(updatedSettings => {
|
||||
expect(updatedSettings).to.have.property('moderation').and.to.equal('post');
|
||||
expect(updatedSettings).to.have.property('infoBoxEnable', true);
|
||||
expect(updatedSettings).to.have.property('infoBoxContent', 'yeah');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
require('../../../utils/mongoose');
|
||||
|
||||
const chai = require('chai');
|
||||
const expect = chai.expect;
|
||||
const server = require('../../../../app');
|
||||
|
||||
// Setup chai.
|
||||
chai.should();
|
||||
chai.use(require('chai-http'));
|
||||
|
||||
let fixture = {
|
||||
'url': 'http://hhgg.com/total-perspective-vortex',
|
||||
'type': 'article',
|
||||
'headline': 'The Total Perspective Vortex',
|
||||
'summary': 'You are an insignificant dot on an insignificant dot.',
|
||||
'section': 'Everything',
|
||||
'authors': ['Ford Prefect']
|
||||
};
|
||||
|
||||
describe('Asset: routes', () => {
|
||||
|
||||
describe('/GET Asset', () => {
|
||||
describe('#get', () => {
|
||||
it('It should get an empty array when there are no assets.', (done) => {
|
||||
|
||||
chai.request(server)
|
||||
.get('/api/v1/asset')
|
||||
.end((err, res) => {
|
||||
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
|
||||
res.should.have.status(200);
|
||||
res.body.should.be.a('array');
|
||||
res.body.length.should.be.eql(0);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// This test checks PUT and read
|
||||
describe('/PUT Asset', () => {
|
||||
describe('#put', () => {
|
||||
it('It should save an asset and load it again.', (done) => {
|
||||
|
||||
chai.request(server)
|
||||
.put('/api/v1/asset')
|
||||
.send(fixture)
|
||||
.end((err, res) => {
|
||||
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
|
||||
res.should.have.status(200);
|
||||
res.body.should.be.a('object');
|
||||
|
||||
// Id should be generated by the model if absent.
|
||||
res.body.should.have.property('id');
|
||||
|
||||
// Save the asset id to compare with GET result.
|
||||
let assetId = res.body.id;
|
||||
|
||||
// Load the asset to make sure it's really there.
|
||||
chai.request(server)
|
||||
.get(`/api/v1/asset?url=${encodeURIComponent(fixture.url)}`)
|
||||
.end((err, res) => {
|
||||
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
|
||||
res.should.have.status(200);
|
||||
res.body.should.be.an('array');
|
||||
|
||||
let asset = res.body[0];
|
||||
|
||||
expect(asset).to.have.property('id');
|
||||
|
||||
// Ensure the asset has the same id as above.
|
||||
// This tests the single url per Id concept.
|
||||
expect(assetId).to.equal(asset.id);
|
||||
|
||||
done();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}); // End describe /PUT Asset
|
||||
|
||||
});
|
||||
@@ -11,6 +11,7 @@ chai.use(require('chai-http'));
|
||||
const Action = require('../../../../models/action');
|
||||
const User = require('../../../../models/user');
|
||||
const Comment = require('../../../../models/comment');
|
||||
const Asset = require('../../../../models/asset');
|
||||
|
||||
const Setting = require('../../../../models/setting');
|
||||
|
||||
@@ -21,14 +22,12 @@ describe('api/stream: routes', () => {
|
||||
const comments = [{
|
||||
id: 'abc',
|
||||
body: 'comment 10',
|
||||
asset_id: 'asset',
|
||||
author_id: '',
|
||||
parent_id: '',
|
||||
status: 'accepted'
|
||||
}, {
|
||||
id: 'def',
|
||||
body: 'comment 20',
|
||||
asset_id: 'asset',
|
||||
author_id: '',
|
||||
parent_id: '',
|
||||
status: ''
|
||||
@@ -66,29 +65,33 @@ describe('api/stream: routes', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
return User
|
||||
.createLocalUsers(users)
|
||||
.then(users => {
|
||||
return Promise.all([
|
||||
User.createLocalUsers(users),
|
||||
Asset.findOrCreateByUrl('http://test.com')
|
||||
])
|
||||
.then(([users, asset]) => {
|
||||
|
||||
comments[0].author_id = users[0].id;
|
||||
comments[1].author_id = users[1].id;
|
||||
|
||||
return Promise.all([
|
||||
Comment.create(comments),
|
||||
Action.create(actions),
|
||||
Setting.create(settings)
|
||||
]);
|
||||
comments[0].author_id = users[0].id;
|
||||
comments[1].author_id = users[1].id;
|
||||
|
||||
});
|
||||
comments[0].asset_id = asset.id;
|
||||
comments[1].asset_id = asset.id;
|
||||
|
||||
return Promise.all([
|
||||
Comment.create(comments),
|
||||
Action.create(actions),
|
||||
Setting.create(settings)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a stream with comments, users and actions', () => {
|
||||
it('should return a stream with comments, users and actions for an existing asset', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/stream')
|
||||
.query({'asset_id': 'asset'})
|
||||
.query({'asset_url': 'http://test.com'})
|
||||
.then(res => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res.body.assets.length).to.equal(1);
|
||||
expect(res.body.comments.length).to.equal(1);
|
||||
expect(res.body.users.length).to.equal(1);
|
||||
expect(res.body.actions.length).to.equal(1);
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta property="og:title" content="<%= title %>" />
|
||||
<meta property="og:author" content="A. J. Ournalist" />
|
||||
<meta property="og:description" content="A description of this article." />
|
||||
|
||||
<meta property="article:published" itemprop="datePublished" content="2016-11-16T11:46:06-05:00" />
|
||||
<meta property="article:modified" itemprop="dateModified" content="2016-11-16T12:09:44-05:00" />
|
||||
<meta property="article:section" itemprop="articleSection" content="The Section!" />
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div style="margin-left:auto; margin-right:auto; width:500px">
|
||||
<h1>Corem ipsal</h1>
|
||||
<h1><%= title %></h1>
|
||||
<p>Lorem ipsum dolor sponge amet, consectetur adipiscing clam. Ut lobortis sollicitudin pillar a ornare. Curabitur dignissim vestibulum cay non rhoncus. Cras laoreet ante vel nunc hendrerit, shelf imperdiet neque egestas. Suspendisse aliquet iaculis fermentum. Talk volutpat, tellus posuere laoreet consequat, mi lacus laoreet massa, sed vehicula mauris velit non lectus. Integer non trust nec neque congue faucibus porttitor sit amet elkhorn.</p>
|
||||
<p><a href="/admin">Visit the moderation console</a></p>
|
||||
|
||||
<div id='coralStreamEmbed'></div>
|
||||
<script type='text/javascript' src='https://pym.nprapps.org/pym.v1.min.js'></script>
|
||||
@@ -17,7 +17,12 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="coralStream"></div>
|
||||
<script src="<%= basePath %>/bundle.js" charset="utf-8"></script>
|
||||
<div id='coralStreamEmbed'></div>
|
||||
<script type='text/javascript' src='https://pym.nprapps.org/pym.v1.min.js'></script>
|
||||
<script>
|
||||
var pymParent = new pym.Parent('coralStreamEmbed', '/embed/stream', {title: 'Talk Comments'});
|
||||
pymParent.onMessage('height', function(height) {document.querySelector('#coralStreamEmbed iframe').height = height + 'px'})</script>
|
||||
</div>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -85,7 +85,7 @@ module.exports = {
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
'NODE_ENV': `"${'development'}"`,
|
||||
'NODE_ENV': `"${process.env.NODE_ENV}"`,
|
||||
'VERSION': `"${require('./package.json').version}"`
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,12 +5,6 @@ const devConfig = require('./webpack.config.dev');
|
||||
devConfig.devtool = null;
|
||||
|
||||
devConfig.plugins = devConfig.plugins.concat([
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
'NODE_ENV': `"${'production'}"`,
|
||||
'VERSION': `"${require('./package.json').version}"`
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false
|
||||
|
||||
Reference in New Issue
Block a user