Merge branch 'master' into auth

This commit is contained in:
Wyatt Johnson
2016-11-09 12:52:09 -07:00
60 changed files with 891 additions and 834 deletions
-1
View File
@@ -1,2 +1 @@
client
dist
+23
View File
@@ -0,0 +1,23 @@
{
"env": {
"browser": true,
"es6": true,
"mocha": true
},
"extends": "../.eslintrc.json",
"parserOptions": {
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
},
"sourceType": "module"
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error"
}
}
+11 -11
View File
@@ -1,12 +1,12 @@
import React from 'react'
import { Router, Route, Redirect, IndexRoute, browserHistory } from 'react-router'
import React from 'react';
import {Router, Route, IndexRoute, browserHistory} from 'react-router';
import ModerationQueue from 'containers/ModerationQueue'
import CommentStream from 'containers/CommentStream'
import EmbedLink from 'components/EmbedLink'
import Configure from 'containers/Configure'
import CommunityContainer from 'containers/CommunityContainer'
import LayoutContainer from 'containers/LayoutContainer'
import ModerationQueue from 'containers/ModerationQueue';
import CommentStream from 'containers/CommentStream';
import EmbedLink from 'components/EmbedLink';
import Configure from 'containers/Configure';
import CommunityContainer from 'containers/CommunityContainer';
import LayoutContainer from 'containers/LayoutContainer';
const routes = (
<Route path='admin' component={LayoutContainer}>
@@ -16,8 +16,8 @@ const routes = (
<Route path='community' component={CommunityContainer} />
<Route path='configure' component={Configure} />
</Route>
)
);
const AppRouter = () => <Router history={browserHistory} routes={routes} />
const AppRouter = () => <Router history={browserHistory} routes={routes} />;
export default AppRouter
export default AppRouter;
+8 -8
View File
@@ -4,15 +4,15 @@
*/
export const updateStatus = (status, id) => (dispatch, getState) => {
dispatch({ type: 'COMMENT_STATUS_UPDATE', id, status })
dispatch({ type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id) })
}
dispatch({type: 'COMMENT_STATUS_UPDATE', id, status});
dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)});
};
export const flagComment = id => (dispatch, getState) => {
dispatch({ type: 'COMMENT_FLAG', id })
dispatch({ type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id) })
}
dispatch({type: 'COMMENT_FLAG', id});
dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)});
};
export const createComment = (name, body) => dispatch => {
dispatch({ type: 'COMMENT_CREATE', name, body })
}
dispatch({type: 'COMMENT_CREATE', name, body});
};
+7 -8
View File
@@ -1,17 +1,16 @@
import React from 'react'
import { Provider } from 'react-redux'
import 'material-design-lite'
import { Layout } from 'react-mdl'
import store from 'services/store'
import React from 'react';
import {Provider} from 'react-redux';
import 'material-design-lite';
import store from 'services/store';
import AppRouter from '../AppRouter'
import AppRouter from '../AppRouter';
export default class App extends React.Component {
render (props) {
render () {
return (
<Provider store={store}>
<AppRouter store={store} />
</Provider>
)
);
}
}
+13 -13
View File
@@ -1,10 +1,10 @@
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/i18n/i18n'
import translations from '../translations'
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/i18n/i18n';
import translations from '../translations';
// Render a single comment for the list
export default props => (
@@ -30,17 +30,17 @@ export default props => (
<span className={styles.body}>{props.comment.get('body')}</span>
</div>
</li>
)
);
// Check if an action can be performed over a comment
const canShowAction = (action, comment) => {
const status = comment.get('status')
const flagged = comment.get('flagged')
const status = comment.get('status');
const flagged = comment.get('flagged');
if (action === 'flag' && (status || flagged === true)) {
return false
return false;
}
return true
}
return true;
};
const lang = new I18n(translations)
const lang = new I18n(translations);
+11 -11
View File
@@ -1,23 +1,23 @@
import React from 'react'
import styles from './CommentBox.css'
import { Button } from 'react-mdl'
import React from 'react';
import styles from './CommentBox.css';
import {Button} from 'react-mdl';
// Renders a comment box for creating a new comment
export default class CommentBox extends React.Component {
constructor (props) {
super(props)
this.state = { name: '', body: '' }
this.onSubmit = this.onSubmit.bind(this)
super(props);
this.state = {name: '', body: ''};
this.onSubmit = this.onSubmit.bind(this);
}
onSubmit () {
const { name, body } = this.state
this.props.onSubmit({ name, body })
this.setState({ body: '', name: '' })
const {name, body} = this.state;
this.props.onSubmit({name, body});
this.setState({body: '', name: ''});
}
render (props, { name, body }) {
render (props, {name, body}) {
return (
<div>
<div class={`${styles.textareaContainer} mdl-textfield mdl-js-textfield`}>
@@ -30,6 +30,6 @@ export default class CommentBox extends React.Component {
</div>
<Button onClick={this.onSubmit} raised>Post</Button>
</div>
)
);
}
}
@@ -1,99 +1,99 @@
import React from 'react'
import styles from './CommentList.css'
import key from 'keymaster'
import Hammer from 'hammerjs'
import Comment from 'components/Comment'
import React from 'react';
import styles from './CommentList.css';
import key from 'keymaster';
import Hammer from 'hammerjs';
import Comment from 'components/Comment';
// Each action has different meaning and configuration
const actions = {
'reject': { status: 'Rejected', icon: 'close', key: 'r' },
'approve': { status: 'Approved', icon: 'done', key: 't' },
'flag': { status: 'flagged', icon: 'flag', filter: 'Untouched' }
}
'reject': {status: 'Rejected', icon: 'close', key: 'r'},
'approve': {status: 'Approved', icon: 'done', key: 't'},
'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'}
};
// Renders a comment list and allow performing actions
export default class CommentList extends React.Component {
constructor (props) {
super(props)
super(props);
this.state = { active: null }
this.onClickAction = this.onClickAction.bind(this)
this.state = {active: null};
this.onClickAction = this.onClickAction.bind(this);
}
// remove key handlers before leaving
componentWillUnmount () {
this.unbindKeyHandlers()
this.unbindKeyHandlers();
}
// add key handlers and gestures
componentDidMount () {
this.bindKeyHandlers()
this.bindKeyHandlers();
// this.bindGestures() // need to check whether we're on a mobile device or this throws an Error
}
// If entering to singleview and no active, active is the first eleement
componentWillReceiveProps (nextProps) {
if (nextProps.singleView && !this.state.active) {
this.setState({ active: nextProps.commentIds.get(0) })
this.setState({active: nextProps.commentIds.get(0)});
}
}
// Add swipe to approve or reject
bindGestures () {
const { actions } = this.props
this._hammer = new Hammer(this.base)
this._hammer.get('swipe').set({ direction: Hammer.DIRECTION_HORIZONTAL })
const {actions} = this.props;
this._hammer = new Hammer(this.base);
this._hammer.get('swipe').set({direction: Hammer.DIRECTION_HORIZONTAL});
if (actions.indexOf('reject') !== -1) {
this._hammer.on('swipeleft', () => this.props.singleView && this.actionKeyHandler('Rejected'))
this._hammer.on('swipeleft', () => this.props.singleView && this.actionKeyHandler('Rejected'));
}
if (actions.indexOf('approve') !== -1) {
this._hammer.on('swiperight', () => this.props.singleView && this.actionKeyHandler('Approved'))
this._hammer.on('swiperight', () => this.props.singleView && this.actionKeyHandler('Approved'));
}
}
// Add key handlers. Each action has one and added j/k for moving around
bindKeyHandlers () {
this.props.actions.filter(action => actions[action].key).forEach(action => {
key(actions[action].key, 'commentList', () => this.props.isActive && this.actionKeyHandler(actions[action].status))
})
key('j', 'commentList', () => this.props.isActive && this.moveKeyHandler('down'))
key('k', 'commentList', () => this.props.isActive && this.moveKeyHandler('up'))
key.setScope('commentList')
key(actions[action].key, 'commentList', () => this.props.isActive && this.actionKeyHandler(actions[action].status));
});
key('j', 'commentList', () => this.props.isActive && this.moveKeyHandler('down'));
key('k', 'commentList', () => this.props.isActive && this.moveKeyHandler('up'));
key.setScope('commentList');
}
// Perform an action using the keys only if the comment is active
actionKeyHandler (action) {
if (this.props.isActive && this.state.active) {
this.onClickAction(action, this.state.active)
this.onClickAction(action, this.state.active);
}
}
// move around with j/k
moveKeyHandler (direction) {
if (!this.props.isActive) {
return
return;
}
const { commentIds } = this.props
const { active } = this.state
const {commentIds} = this.props;
const {active} = this.state;
// check boundaries
if (active == null || !commentIds.size) {
this.setState({ active: commentIds.get(0) })
if (active === null || !commentIds.size) {
this.setState({active: commentIds.get(0)});
} else if (direction === 'up' && active !== commentIds.first()) {
this.setState({ active: commentIds.get(commentIds.indexOf(active) - 1) })
this.setState({active: commentIds.get(commentIds.indexOf(active) - 1)});
} else if (direction === 'down' && active !== commentIds.last()) {
this.setState({ active: commentIds.get(commentIds.indexOf(active) + 1) })
this.setState({active: commentIds.get(commentIds.indexOf(active) + 1)});
}
// scroll to the position
const index = Math.max(commentIds.indexOf(this.state.active), 0)
this.base.childNodes[index] && this.base.childNodes[index].focus()
const index = Math.max(commentIds.indexOf(this.state.active), 0);
this.base.childNodes[index] && this.base.childNodes[index].focus();
}
unbindKeyHandlers () {
key.deleteScope('commentList')
key.deleteScope('commentList');
}
// If we are performing an action over a comment (aka removing from the list) we need to select a new active.
@@ -101,26 +101,26 @@ export default class CommentList extends React.Component {
// resolve since the content of the list could change externally. For now it works as expected
onClickAction (action, id) {
if (id === this.state.active) {
const { commentIds } = this.props
const {commentIds} = this.props;
if (commentIds.last() === this.state.active) {
this.setState({ active: commentIds.get(commentIds.size - 2) })
this.setState({active: commentIds.get(commentIds.size - 2)});
} else {
this.setState({ active: commentIds.get(Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.size - 1)) })
this.setState({active: commentIds.get(Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.size - 1))});
}
}
this.props.onClickAction(action, id)
this.props.onClickAction(action, id);
}
render () {
const {singleView, commentIds, comments, hideActive} = this.props
const {active} = this.state
const {singleView, commentIds, comments, hideActive} = this.props;
const {active} = this.state;
return (
<ul className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
{commentIds.map((commentId, index) => (
<Comment comment={comments.get(commentId)}
ref={el => { if (el && commentId === active) { this._active = el } }}
key={index + 'comment'}
ref={el => { if (el && commentId === active) { this._active = el; } }}
key={`${index }comment`}
index={index}
onClickAction={this.onClickAction}
actions={this.props.actions}
@@ -129,6 +129,6 @@ export default class CommentList extends React.Component {
hideActive={hideActive} />
)).toArray()}
</ul>
)
);
}
}
+15 -15
View File
@@ -1,22 +1,22 @@
import React from 'react'
import styles from './EmbedLink.css'
import I18n from 'coral-framework/i18n/i18n'
import translations from '../translations'
import { Button } from 'react-mdl'
import React from 'react';
import styles from './EmbedLink.css';
import I18n from 'coral-framework/i18n/i18n';
import translations from '../translations';
import {Button} from 'react-mdl';
const embedText =
`<div id='coralStreamEmbed'></div><script type='text/javascript' src='http://pym.nprapps.org/pym.v1.min.js'></script><script>var pymParent = new pym.Parent('coralStreamEmbed', '${window.location.protocol}//${window.location.host}/embedScript/index.html', {});</script>`
`<div id='coralStreamEmbed'></div><script type='text/javascript' src='http://pym.nprapps.org/pym.v1.min.js'></script><script>var pymParent = new pym.Parent('coralStreamEmbed', '${window.location.protocol}//${window.location.host}/embedScript/index.html', {});</script>`;
const copyToClipBoard = event => {
const copyTextarea = document.querySelector('.' + styles.embedTextarea)
copyTextarea.select()
const copyToClipBoard = () => {
const copyTextarea = document.querySelector(`.${ styles.embedTextarea}`);
copyTextarea.select();
try {
document.execCommand('copy')
document.execCommand('copy');
} catch (err) {
console.error('Unable to copy')
console.error('Unable to copy');
}
}
};
const EmbedLink = () => <div id={styles.embedLink}>
<h3>Embed Comment Stream</h3>
@@ -30,8 +30,8 @@ const EmbedLink = () => <div id={styles.embedLink}>
{lang.t('embedlink.copy')}
</Button>
</div>
</div>
</div>;
export default EmbedLink
export default EmbedLink;
const lang = new I18n(translations)
const lang = new I18n(translations);
@@ -0,0 +1,23 @@
import React from 'react';
import {Layout, Navigation, Drawer, Header} from 'react-mdl';
import {Link} from 'react-router';
import styles from './Header.css';
// 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>
</Navigation>
</Header>
<Drawer>
<Navigation>
<Link className={styles.navLink} to={'/admin/'}>Moderate</Link>
<Link className={styles.navLink} to={'/admin/configure'}>Configure</Link>
</Navigation>
</Drawer>
{props.children}
</Layout>
);
+5 -5
View File
@@ -1,13 +1,13 @@
import React from 'react'
import { Button, Icon } from 'react-mdl'
import styles from './Modal.css'
import React from 'react';
import {Button, Icon} from 'react-mdl';
import styles from './Modal.css';
export default ({ open, children, onClose }) => (
export default ({open, children, onClose}) => (
<div className={`${styles.container} ${!open ? styles.hide : ''}`}>
<div className={styles.inner}>
<Button className={styles.close} onClick={onClose}><Icon name='close' /></Button>
{children}
</div>
</div>
)
);
@@ -1,9 +1,8 @@
import I18n from 'coral-framework/i18n/i18n'
import translations from '../translations'
import React from 'react'
import Modal from 'components/Modal'
import styles from './ModerationKeysModal.css'
import { Map } from 'immutable'
import I18n from 'coral-framework/i18n/i18n';
import translations from '../translations';
import React from 'react';
import Modal from 'components/Modal';
import styles from './ModerationKeysModal.css';
const shortcuts = [
{
@@ -22,9 +21,9 @@ const shortcuts = [
'r': 'modqueue.reject'
}
}
]
];
export default ({ open, onClose }) => (
export default ({open, onClose}) => (
<Modal open={open} onClose={onClose}>
<h3>{lang.t('modqueue.shortcuts')}</h3>
<div className={styles.container}>
@@ -37,7 +36,7 @@ export default ({ open, onClose }) => (
</thead>
<tbody>
{Object.keys(shortcut.shortcuts).map(key => (
<tr key={key + 'tr'}>
<tr key={`${key }tr`}>
<td className={styles.shortcut}><span className={styles.key}>{key}</span></td>
<td>{lang.t(shortcut.shortcuts[key])}</td>
</tr>
@@ -47,6 +46,6 @@ export default ({ open, onClose }) => (
))}
</div>
</Modal>
)
);
const lang = new I18n(translations)
const lang = new I18n(translations);
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
import {Layout} from 'react-mdl';
import 'material-design-lite';
import Header from 'components/Header';
export default (props) => (
<Layout>
<Header>
{props.children}
</Header>
</Layout>
);
@@ -1,7 +1,7 @@
import React from 'react'
import { Navigation, Drawer } from 'react-mdl'
import { Link } from 'react-router'
import styles from './Header.css'
import React from 'react';
import {Navigation, Drawer} from 'react-mdl';
import {Link} from 'react-router';
import styles from './Header.css';
export default () => (
<Drawer>
@@ -11,4 +11,4 @@ export default () => (
<Link className={styles.navLink} to="/admin/configure">Configure</Link>
</Navigation>
</Drawer>
)
);
@@ -1,7 +1,7 @@
import React from 'react'
import { Navigation, Header } from 'react-mdl'
import { Link } from 'react-router'
import styles from './Header.css'
import React from 'react';
import {Navigation, Header} from 'react-mdl';
import {Link} from 'react-router';
import styles from './Header.css';
export default () => (
<Header title='Talk'>
@@ -11,4 +11,4 @@ export default () => (
<Link className={styles.navLink} to="/admin/configure">Configure</Link>
</Navigation>
</Header>
)
);
@@ -1,12 +1,12 @@
import React from 'react'
import { Layout as LayoutMDL} from 'react-mdl'
import Header from './Header'
import Drawer from './Drawer'
import React from 'react';
import {Layout as LayoutMDL} from 'react-mdl';
import Header from './Header';
import Drawer from './Drawer';
export const Layout = ({ children }) => (
export const Layout = ({children}) => (
<LayoutMDL fixedDrawer>
<Header />
<Drawer />
{children}
</LayoutMDL>
)
);
@@ -1,11 +1,10 @@
import React from 'react'
import styles from './CommentStream.css'
import { Snackbar } from 'react-mdl'
import { connect } from 'react-redux'
import { createComment, flagComment } from 'actions/comments'
import CommentList from 'components/CommentList'
import CommentBox from 'components/CommentBox'
import React from 'react';
import styles from './CommentStream.css';
import {Snackbar} from 'react-mdl';
import {connect} from 'react-redux';
import {createComment, flagComment} from 'actions/comments';
import CommentList from 'components/CommentList';
import CommentBox from 'components/CommentBox';
/**
* Renders a comment stream using a CommentList component
@@ -14,34 +13,34 @@ import CommentBox from 'components/CommentBox'
class CommentStream extends React.Component {
constructor (props) {
super(props)
this.state = { snackbar: false, snackbarMsg: '' }
this.onSubmit = this.onSubmit.bind(this)
this.onClickAction = this.onClickAction.bind(this)
super(props);
this.state = {snackbar: false, snackbarMsg: ''};
this.onSubmit = this.onSubmit.bind(this);
this.onClickAction = this.onClickAction.bind(this);
}
// Fetch the comments before mounting
componentWillMount () {
this.props.dispatch({ type: 'COMMENT_STREAM_FETCH' })
this.props.dispatch({type: 'COMMENT_STREAM_FETCH'});
}
// Submit the new comment
onSubmit (comment) {
this.props.dispatch(createComment(comment.name, comment.body))
this.props.dispatch(createComment(comment.name, comment.body));
}
// The only action for now is flagging
onClickAction (action, id) {
if (action === 'flagged') {
this.props.dispatch(flagComment(id))
clearTimeout(this._snackTimeout)
this.setState({ snackbar: true, snackbarMsg: 'Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.' })
this._snackTimeout = setTimeout(() => this.setState({ snackbar: false }), 30000)
this.props.dispatch(flagComment(id));
clearTimeout(this._snackTimeout);
this.setState({snackbar: true, snackbarMsg: 'Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.'});
this._snackTimeout = setTimeout(() => this.setState({snackbar: false}), 30000);
}
}
// Render the comment box along with the CommentList
render ({ comments }, { snackbar, snackbarMsg }) {
render ({comments}, {snackbar, snackbarMsg}) {
return (
<div className={styles.container}>
<CommentBox onSubmit={this.onSubmit} />
@@ -54,8 +53,8 @@ class CommentStream extends React.Component {
loading={comments.loading} />
<Snackbar active={snackbar}>{snackbarMsg}</Snackbar>
</div>
)
);
}
}
export default connect(({ comments }) => ({ comments }))(CommentStream)
export default connect(({comments}) => ({comments}))(CommentStream);
@@ -1,7 +1,4 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import I18n from 'coral-framework/i18n/i18n'
import translations from '../translations'
import React, {Component} from 'react';
export default class CommunityContainer extends Component {
render() {
@@ -9,6 +6,6 @@ export default class CommunityContainer extends Component {
<div>
<h1>Community</h1>
</div>
)
);
}
}
}
@@ -25,10 +25,19 @@
.configSettingEmbed {
border: 1px solid #ccc;
border-radius: 4px;
height: 90px;
margin-bottom: 10px;
display: block;
height: 170px;
}
.copiedText {
color: #008000;
float: right;
padding: 12px;
font-size: 14px;
}
.copyButton {
float: right;
}
.embedInput {
@@ -40,6 +49,6 @@
margin-bottom: 10px;
color: #555;
padding: 14px;
font-size: 16px;
font-size: 14px;
letter-spacing: 0.03em;
}
+28 -25
View File
@@ -1,5 +1,5 @@
import React from 'react'
import {connect} from 'react-redux'
import React from 'react';
import {connect} from 'react-redux';
import {
List,
ListItem,
@@ -9,15 +9,16 @@ import {
Checkbox,
Button,
Icon
} from 'react-mdl'
import styles from './Configure.css'
import I18n from 'coral-framework/i18n/i18n'
import translations from '../translations'
} from 'react-mdl';
import styles from './Configure.css';
import I18n from 'coral-framework/i18n/i18n';
import translations from '../translations';
class Configure extends React.Component {
constructor (props) {
super(props)
this.state = {activeSection: 'comments'}
super(props);
this.state = {activeSection: 'comments', copied: false};
this.copyToClipBoard = this.copyToClipBoard.bind(this);
}
getCommentSettings () {
@@ -38,43 +39,45 @@ class Configure extends React.Component {
error='Input is not a number!'
label='Maximum Characters' />
</ListItem>
</List>
</List>;
}
copyToClipBoard (event) {
const copyTextarea = document.querySelector('.' + styles.embedInput)
copyTextarea.select()
copyToClipBoard () {
const copyTextarea = document.querySelector(`.${ styles.embedInput}`);
copyTextarea.select();
try {
document.execCommand('copy')
document.execCommand('copy');
this.setState({copied: true});
} catch (err) {
console.error('Unable to copy')
console.error('Unable to copy', err);
}
}
getEmbed () {
const embedText =
`<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', '${window.location.protocol}//${window.location.host}/client/coral-embed-stream/', {title: 'comments'});</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', '${window.location.protocol}//${window.location.host}/client/embed/stream/bundle.js', {title: 'comments'});</script>`;
return <List>
<ListItem className={styles.configSettingEmbed}>
<p>Copy and paste code below into your CMS to embed your comment box in your articles</p>
<textarea type='text' className={styles.embedInput}>
{embedText}
</textarea>
<Button raised colored>{lang.t('embedlink.copy')}</Button>
<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')}
</Button>
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
</ListItem>
</List>
</List>;
}
changeSection (activeSection) {
this.setState({activeSection})
this.setState({activeSection});
}
render () {
const pageTitle = this.state.activeSection === 'comments'
? 'Comment Settings'
: 'Embed Comment Stream'
: 'Embed Comment Stream';
return (
<div className={styles.container}>
@@ -104,10 +107,10 @@ class Configure extends React.Component {
}
</div>
</div>
)
);
}
}
export default connect(x => x)(Configure)
export default connect(x => x)(Configure);
const lang = new I18n(translations)
const lang = new I18n(translations);
@@ -1,19 +1,18 @@
import React, { Component }from 'react'
import { connect } from 'react-redux'
import React, {Component}from 'react';
import {connect} from 'react-redux';
import { Layout } from '../components/ui/Layout'
import {Layout} from '../components/ui/Layout';
class LayoutContainer extends Component {
render () {
return <Layout { ...this.props } />
return <Layout { ...this.props } />;
}
}
LayoutContainer.propTypes = {}
LayoutContainer.propTypes = {};
const mapStateToProps = state => ({ data: {} })
const mapStateToProps = () => ({data: {}});
const mapDispatchToProps = (dispatch, ownProps) => ({ dispatch })
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer)
const mapDispatchToProps = (dispatch) => ({dispatch});
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
@@ -1,13 +1,12 @@
import React from 'react'
import { connect } from 'react-redux'
import ModerationKeysModal from 'components/ModerationKeysModal'
import CommentList from 'components/CommentList'
import { updateStatus } from 'actions/comments'
import styles from './ModerationQueue.css'
import key from 'keymaster'
import I18n from 'coral-framework/i18n/i18n'
import translations from '../translations'
import React from 'react';
import {connect} from 'react-redux';
import ModerationKeysModal from 'components/ModerationKeysModal';
import CommentList from 'components/CommentList';
import {updateStatus} from 'actions/comments';
import styles from './ModerationQueue.css';
import key from 'keymaster';
import I18n from 'coral-framework/i18n/i18n';
import translations from '../translations';
/*
* Renders the moderation queue as a tabbed layout with 3 moderation
@@ -17,46 +16,47 @@ import translations from '../translations'
class ModerationQueue extends React.Component {
constructor (props) {
super(props)
super(props);
this.state = { activeTab: 'pending', singleView: false, modalOpen: false }
this.state = {activeTab: 'pending', singleView: false, modalOpen: false};
}
// Fetch comments and bind singleView key before render
componentWillMount () {
this.props.dispatch({ type: 'COMMENTS_MODERATION_QUEUE_FETCH' })
key('s', () => this.setState({ singleView: !this.state.singleView }))
key('shift+/', () => this.setState({ modalOpen: true }))
key('esc', () => this.setState({ modalOpen: false }))
this.props.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH'});
key('s', () => this.setState({singleView: !this.state.singleView}));
key('shift+/', () => this.setState({modalOpen: true}));
key('esc', () => this.setState({modalOpen: false}));
}
// Unbind singleView key before unmount
componentWillUnmount () {
key.unbind('s')
key.unbind('shift+/')
key.unbind('esc')
key.unbind('s');
key.unbind('shift+/');
key.unbind('esc');
}
// Hack for dynamic mdl tabs
componentDidMount () {
if (typeof componentHandler !== 'undefined') {
componentHandler.upgradeAllRegistered()
// FIXME: fix this hack
componentHandler.upgradeAllRegistered(); // eslint-disable-line no-undef
}
}
// Dispatch the update status action
onCommentAction (status, id) {
this.props.dispatch(updateStatus(status, id))
this.props.dispatch(updateStatus(status, id));
}
onTabClick (activeTab) {
this.setState({ activeTab })
this.setState({activeTab});
}
// Render the tabbed lists moderation queues
render () {
const { comments } = this.props
const { activeTab, singleView, modalOpen } = this.state
const {comments} = this.props;
const {activeTab, singleView, modalOpen} = this.state;
return (
<div>
@@ -94,8 +94,8 @@ class ModerationQueue extends React.Component {
isActive={activeTab === 'rejected'}
singleView={singleView}
commentIds={comments.get('ids').filter(id => {
const data = comments.get('byId').get(id)
return !data.get('status') && data.get('flagged') === true
const data = comments.get('byId').get(id);
return !data.get('status') && data.get('flagged') === true;
})}
comments={comments.get('byId')}
onClickAction={(action, id) => this.onCommentAction(action, id)}
@@ -103,13 +103,13 @@ class ModerationQueue extends React.Component {
loading={comments.loading} />
</div>
<ModerationKeysModal open={modalOpen}
onClose={() => this.setState({ modalOpen: false })} />
onClose={() => this.setState({modalOpen: false})} />
</div>
</div>
)
);
}
}
export default connect(({ comments }) => ({ comments }))(ModerationQueue)
export default connect(({comments}) => ({comments}))(ModerationQueue);
const lang = new I18n(translations)
const lang = new I18n(translations);
+4 -4
View File
@@ -1,7 +1,7 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/App'
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
// Render the application into the DOM
ReactDOM.render(<App />, document.querySelector('#root'))
ReactDOM.render(<App />, document.querySelector('#root'));
+27 -27
View File
@@ -1,5 +1,5 @@
import { Map, List, fromJS } from 'immutable'
import {Map, List, fromJS} from 'immutable';
/**
* Comments state is stored using 2 structures:
@@ -12,48 +12,48 @@ const initialState = Map({
byId: Map(),
ids: List(),
loading: false
})
});
// Handle the comment actions
export default (state = initialState, action) => {
switch (action.type) {
case 'COMMENTS_MODERATION_QUEUE_FETCH': return state.set('loading', true)
case 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceComments(action, state)
case 'COMMENTS_MODERATION_QUEUE_FAILED': return state.set('loading', false)
case 'COMMENT_STATUS_UPDATE': return updateStatus(state, action)
case 'COMMENT_FLAG': return flag(state, action)
case 'COMMENT_CREATE_SUCCESS': return addComment(state, action)
case 'COMMENT_STREAM_FETCH_SUCCESS': return replaceComments(action, state)
default: return state
case 'COMMENTS_MODERATION_QUEUE_FETCH': return state.set('loading', true);
case 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceComments(action, state);
case 'COMMENTS_MODERATION_QUEUE_FAILED': return state.set('loading', false);
case 'COMMENT_STATUS_UPDATE': return updateStatus(state, action);
case 'COMMENT_FLAG': return flag(state, action);
case 'COMMENT_CREATE_SUCCESS': return addComment(state, action);
case 'COMMENT_STREAM_FETCH_SUCCESS': return replaceComments(action, state);
default: return state;
}
}
};
// Update a comment status
const updateStatus = (state, action) => {
const byId = state.get('byId')
const data = byId.get(action.id).get('data').set('status', action.status)
const comment = byId.get(action.id).set('data', data)
return state.set('byId', byId.set(action.id, comment))
}
const byId = state.get('byId');
const data = byId.get(action.id).get('data').set('status', action.status);
const comment = byId.get(action.id).set('data', data);
return state.set('byId', byId.set(action.id, comment));
};
// Flag a comment
const flag = (state, action) => {
const byId = state.get('byId')
const data = byId.get(action.id).get('data').set('flagged', true)
const comment = byId.get(action.id).set('data', data)
return state.set('byId', byId.set(action.id, comment))
}
const byId = state.get('byId');
const data = byId.get(action.id).get('data').set('flagged', true);
const comment = byId.get(action.id).set('data', data);
return state.set('byId', byId.set(action.id, comment));
};
// Replace the comment list with a new one
const replaceComments = (action, state) => {
const comments = fromJS(action.comments.reduce((prev, curr) => { prev[curr._id] = curr; return prev }, {}))
const comments = fromJS(action.comments.reduce((prev, curr) => { prev[curr._id] = curr; return prev; }, {}));
return state.set('byId', comments).set('loading', false)
.set('ids', List(comments.keys()))
}
.set('ids', List(comments.keys()));
};
// Add a new comment
const addComment = (state, action) => {
const comment = fromJS(action.comment)
const comment = fromJS(action.comment);
return state.set('byId', state.get('byId').set(comment.get('item_id'), comment))
.set('ids', state.get('ids').unshift(comment.get('item_id')))
}
.set('ids', state.get('ids').unshift(comment.get('item_id')));
};
+3 -3
View File
@@ -1,8 +1,8 @@
import { combineReducers } from 'redux'
import comments from 'reducers/comments'
import {combineReducers} from 'redux';
import comments from 'reducers/comments';
// Combine all reducers into a main one
export default combineReducers({
comments
})
});
+4 -4
View File
@@ -5,10 +5,10 @@
*/
try {
module.exports = require('../../config.json')
module.exports = require('../../config.json');
} catch (error) {
const message = `The config.json file under the root directory is missing
or invalid Please add one to use this app. You can use config.sample.json as a guide.`
window.alert(message)
throw new Error(message)
or invalid Please add one to use this app. You can use config.sample.json as a guide.`;
window.alert(message);
throw new Error(message);
}
+5 -5
View File
@@ -1,8 +1,8 @@
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import mainReducer from 'reducers'
import talkAdapter from 'services/talk-adapter'
import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import mainReducer from 'reducers';
import talkAdapter from 'services/talk-adapter';
/**
* Create the store by merging the app reducers with
@@ -15,4 +15,4 @@ export default createStore(
mainReducer,
window.devToolsExtension && window.devToolsExtension(),
applyMiddleware(thunk, talkAdapter)
)
);
+24 -24
View File
@@ -10,44 +10,44 @@
// Intercept redux actions and act over the ones we are interested
export default store => next => action => {
switch (action.type) {
case 'COMMENTS_MODERATION_QUEUE_FETCH':
fetchModerationQueueComments(store)
break
case 'COMMENT_STREAM_FETCH':
fetchCommentStream(store)
break
case 'COMMENT_UPDATE':
updateComment(store, action.comment)
break
case 'COMMENT_CREATE':
createComment(store, action.name, action.body)
break
case 'COMMENTS_MODERATION_QUEUE_FETCH':
fetchModerationQueueComments(store);
break;
// case 'COMMENT_STREAM_FETCH':
// fetchCommentStream(store);
// break;
case 'COMMENT_UPDATE':
updateComment(store, action.comment);
break;
case 'COMMENT_CREATE':
createComment(store, action.name, action.body);
break;
}
next(action)
}
next(action);
};
// Get comments to fill each of the three lists on the mod queue
const fetchModerationQueueComments = store =>
fetch(`/api/v1/queue`)
fetch('/api/v1/queue')
.then(res => res.json())
.then(res => store.dispatch({ type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS',
comments: res }))
.catch(error => store.dispatch({ type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error }))
.then(res => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS',
comments: res}))
.catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error}));
// Update a comment. Now to update a comment we need to send back the whole object
const updateComment = (store, comment) =>
fetch(`/api/v1/comments/${comment._id}/status`, {
method: 'POST',
body: JSON.stringify({ status: comment.status })
body: JSON.stringify({status: comment.status})
})
.then(res => res.json())
.then(res => store.dispatch({ type: 'COMMENT_UPDATE_SUCCESS', res }))
.catch(error => store.dispatch({ type: 'COMMENT_UPDATE_FAILED', error }))
.then(res => store.dispatch({type: 'COMMENT_UPDATE_SUCCESS', res}))
.catch(error => store.dispatch({type: 'COMMENT_UPDATE_FAILED', error}));
// Create a new comment
const createComment = (store, name, comment) =>
fetch(`/api/v1/comments`, {
fetch('/api/v1/comments', {
method: 'POST',
body: JSON.stringify({
status: 'Untouched',
@@ -56,5 +56,5 @@ fetch(`/api/v1/comments`, {
createdAt: Date.now()
})
}).then(res => res.json())
.then(res => store.dispatch({ type: 'COMMENT_CREATE_SUCCESS', comment: res }))
.catch(error => store.dispatch({ type: 'COMMENT_CREATE_FAILED', error }))
.then(res => store.dispatch({type: 'COMMENT_CREATE_SUCCESS', comment: res}))
.catch(error => store.dispatch({type: 'COMMENT_CREATE_FAILED', error}));
+29 -29
View File
@@ -1,39 +1,39 @@
export default {
en: {
"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"
'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"
'comment': {
'flagged': 'flagged',
'anon': 'Anonymous'
},
"embedlink": {
"copy": "Copy to Clipboard"
'embedlink': {
'copy': 'Copy to Clipboard'
}
},
es: {
"modqueue": {
"pending": "pendiente",
"rejected": "rechazado",
"flagged": "marcado",
"shortcuts": "Atajos de teclado",
"close": "Cerrar"
'modqueue': {
'pending': 'pendiente',
'rejected': 'rechazado',
'flagged': 'marcado',
'shortcuts': 'Atajos de teclado',
'close': 'Cerrar'
},
"comment": {
"flagged": "marcado",
"anon": "Anónimo"
'comment': {
'flagged': 'marcado',
'anon': 'Anónimo'
}
}
}
};
+48 -53
View File
@@ -1,63 +1,62 @@
import React, {Component, PropTypes} from 'react'
import React, {Component, PropTypes} from 'react';
import {
itemActions,
Notification,
notificationActions,
authActions
} from '../../coral-framework'
import {connect} from 'react-redux'
import CommentBox from '../../coral-plugin-commentbox/CommentBox'
import Content from '../../coral-plugin-commentcontent/CommentContent'
import PubDate from '../../coral-plugin-pubdate/PubDate'
import Count from '../../coral-plugin-comment-count/CommentCount'
import Flag from '../../coral-plugin-flags/FlagButton'
import AuthorName from '../../coral-plugin-author-name/AuthorName'
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies'
import Pym from 'pym.js'
} from '../../coral-framework';
import {connect} from 'react-redux';
import CommentBox from '../../coral-plugin-commentbox/CommentBox';
import Content from '../../coral-plugin-commentcontent/CommentContent';
import PubDate from '../../coral-plugin-pubdate/PubDate';
import Count from '../../coral-plugin-comment-count/CommentCount';
import AuthorName from '../../coral-plugin-author-name/AuthorName';
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies';
import Pym from 'pym.js';
const {addItem, updateItem, postItem, getStream, postAction, appendItemArray} = itemActions
const {addNotification, clearNotification} = notificationActions
const {setLoggedInUser} = authActions
const {addItem, updateItem, postItem, getStream, postAction, appendItemArray} = itemActions;
const {addNotification, clearNotification} = notificationActions;
const {setLoggedInUser} = authActions;
@connect(
(state) => {
return {
return {
config: state.config.toJS(),
items: state.items.toJS(),
notification: state.notification.toJS(),
auth: state.auth.toJS()
}
};
},
(dispatch) => {
return {
addItem: (item) => {
return dispatch(addItem(item))
return dispatch(addItem(item));
},
updateItem: (id, property, value) => {
return dispatch(updateItem(id, property, value))
return dispatch(updateItem(id, property, value));
},
postItem: (data, type, id) => {
return dispatch(postItem(data, type, id))
return dispatch(postItem(data, type, id));
},
getStream: (rootId) => {
return dispatch(getStream(rootId))
return dispatch(getStream(rootId));
},
addNotification: (type, text) => {
return dispatch(addNotification(type, text))
return dispatch(addNotification(type, text));
},
clearNotification: () => {
return dispatch(clearNotification())
return dispatch(clearNotification());
},
setLoggedInUser: (user_id) => {
return dispatch(setLoggedInUser(user_id))
return dispatch(setLoggedInUser(user_id));
},
postAction: (item, action, user) => {
return dispatch(postAction(item, action, user))
return dispatch(postAction(item, action, user));
},
appendItemArray: (item, property, value, addToFront) => {
return dispatch(appendItemArray(item, property, value, addToFront))
return dispatch(appendItemArray(item, property, value, addToFront));
}
}
};
}
)
@@ -72,33 +71,30 @@ 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')
new Pym.Child({polling: 500});
this.props.getStream('assetTest');
}
render () {
if (Object.keys(this.props.items).length === 0) {
if (Object.keys(this.props.items).length === 0) {
// Loading mock asset
this.props.postItem({
comments: [],
url: 'http://coralproject.net'
}, 'asset', 'assetTest')
this.props.postItem({
comments: [],
url: 'http://coralproject.net'
}, 'asset', 'assetTest');
// Loading mock user
this.props.postItem({name: 'Ban Ki-Moon'}, 'user', 'user_8989')
this.props.postItem({name: 'Ban Ki-Moon'}, 'user', 'user_8989')
.then((id) => {
this.props.setLoggedInUser(id)
})
}
this.props.setLoggedInUser(id);
});
}
// TODO: Replace teststream id with id from params
const rootItemId = 'assetTest'
const rootItem = this.props.items[rootItemId]
return <div>
const rootItemId = 'assetTest';
const rootItem = this.props.items[rootItemId];
return <div>
{
rootItem ?
<div>
@@ -116,7 +112,7 @@ class CommentStream extends Component {
</div>
{
rootItem.comments.map((commentId) => {
const comment = this.props.items[commentId]
const comment = this.props.items[commentId];
return <div className="comment" key={commentId}>
<hr aria-hidden={true}/>
<AuthorName name={comment.username}/>
@@ -146,8 +142,8 @@ class CommentStream extends Component {
{
comment.children &&
comment.children.map((replyId) => {
let reply = this.props.items[replyId]
return <div className="reply" key={replyId}>
let reply = this.props.items[replyId];
return <div className="reply" key={replyId}>
<hr aria-hidden={true}/>
<AuthorName name={reply.username}/>
<PubDate created_at={reply.created_at}/>
@@ -165,10 +161,10 @@ class CommentStream extends Component {
updateItem={this.props.updateItem}
parent_id={reply.parent_id}/>
</div>
</div>
})
</div>;
})
}
</div>
</div>;
})
}
<Notification
@@ -176,12 +172,11 @@ class CommentStream extends Component {
clearNotification={this.props.clearNotification}
notification={this.props.notification}/>
</div>
:'Loading'
: 'Loading'
}
</div>
</div>;
}
}
export default CommentStream
export default CommentStream;
+7 -7
View File
@@ -1,13 +1,13 @@
import React from 'react'
import { render } from 'react-dom'
import CommentStream from './CommentStream'
import { Provider } from 'react-redux'
import { fetchConfig, store } from '../../coral-framework'
import React from 'react';
import {render} from 'react-dom';
import CommentStream from './CommentStream';
import {Provider} from 'react-redux';
import {fetchConfig, store} from '../../coral-framework';
store.dispatch(fetchConfig())
store.dispatch(fetchConfig());
render(
<Provider store={store}>
<CommentStream />
</Provider>
, document.querySelector('#coralStream'))
, document.querySelector('#coralStream'));
@@ -1,7 +1,7 @@
import { Map } from 'immutable'
import {expect} from 'chai'
import authReducer from '../../store/reducers/auth'
import * as actions from '../../store/actions/auth'
import {Map} from 'immutable';
import {expect} from 'chai';
import authReducer from '../../store/reducers/auth';
import * as actions from '../../store/actions/auth';
describe ('authReducer', () => {
describe('SET_LOGGED_IN_USER', () => {
@@ -9,23 +9,23 @@ describe ('authReducer', () => {
const action = {
type: actions.SET_LOGGED_IN_USER,
user_id: '123'
}
const store = new Map({})
const result = authReducer(store, action)
expect(result.get('user')).to.equal(action.user_id)
})
})
};
const store = new Map({});
const result = authReducer(store, action);
expect(result.get('user')).to.equal(action.user_id);
});
});
describe('LOG_OUT_USER', () => {
it('should clear the user store', () => {
const action = {
type: actions.LOG_OUT_USER
}
};
const store = new Map({
user: '123'
})
const result = authReducer(store, action)
expect(result.get('user')).to.equal(undefined)
})
})
})
});
const result = authReducer(store, action);
expect(result.get('user')).to.equal(undefined);
});
});
});
@@ -1,70 +1,70 @@
import 'react'
import 'redux'
import {expect} from 'chai'
import fetchMock from 'fetch-mock'
import * as actions from '../../store/actions/items'
import {Map} from 'immutable'
import 'react';
import 'redux';
import {expect} from 'chai';
import fetchMock from 'fetch-mock';
import * as actions from '../../store/actions/items';
import {Map} from 'immutable';
import configureStore from 'redux-mock-store'
import configureStore from 'redux-mock-store';
const mockStore = configureStore()
const mockStore = configureStore();
describe('itemActions', () => {
let store
const host = 'http://test.host'
let store;
const host = 'http://test.host';
beforeEach(() => {
store = mockStore(new Map({}))
fetchMock.restore()
})
store = mockStore(new Map({}));
fetchMock.restore();
});
describe('getItemsQuery', () => {
const query = 'all'
const rootId = '1234'
const view = 'testView'
const query = 'all';
const rootId = '1234';
const view = 'testView';
const response = {results: [
{Docs: [
{type: 'comment', data: {content: 'stuff'}, item_id: '123'},
{type: 'comment', data: {content: 'morestuff'}, item_id: '456'}
]}
]}
]};
it('should get an item from a query and send the appropriate dispatches', () => {
fetchMock.get('*', JSON.stringify(response))
fetchMock.get('*', JSON.stringify(response));
return actions.getItemsQuery(query, rootId, view, host)(store.dispatch)
.then((res) => {
expect(fetchMock.calls().matched[0][0]).to.equal('http://test.host/v1/exec/all/view/testView/1234')
expect(res).to.deep.equal(response.results[0].Docs)
expect(fetchMock.calls().matched[0][0]).to.equal('http://test.host/v1/exec/all/view/testView/1234');
expect(res).to.deep.equal(response.results[0].Docs);
expect(store.getActions()[0]).to.deep.equal({
type: actions.ADD_ITEM,
item: response.results[0].Docs[0],
item_id: '123'
})
});
expect(store.getActions()[1]).to.deep.equal({
type: actions.ADD_ITEM,
item: response.results[0].Docs[1],
item_id: '456'
})
})
})
});
});
});
it('should handle an error', () => {
fetchMock.get('*', 404)
fetchMock.get('*', 404);
return actions.getItemsQuery(query, rootId, view, host)(store.dispatch)
.catch((err) => {
expect(err).to.be.truthy
})
})
})
expect(err).to.be.truthy;
});
});
});
describe('getItemsArray', () => {
const response = {items: [{type: 'comment', item_id: '123'}, {type: 'comment', item_id: '456'}]}
const ids = [1, 2]
const response = {items: [{type: 'comment', item_id: '123'}, {type: 'comment', item_id: '456'}]};
const ids = [1, 2];
it('should get an item from an array of ids and send the appropriate dispatches', () => {
fetchMock.get('*', JSON.stringify(response))
fetchMock.get('*', JSON.stringify(response));
return actions.getItemsArray(ids, host)(store.dispatch)
.then((res) => {
expect(res).to.deep.equal(response.items)
expect(res).to.deep.equal(response.items);
expect(store.getActions()[0]).to.deep.equal({
type: actions.ADD_ITEM,
item: {
@@ -72,33 +72,33 @@ describe('itemActions', () => {
item_id: '123'
},
item_id: '123'
})
});
expect(store.getActions()[1]).to.deep.equal({
type: actions.ADD_ITEM,
item: {
type: 'comment', item_id: '456'
},
item_id: '456'
})
})
})
});
});
});
it('should handle an error', () => {
fetchMock.get('*', 404)
fetchMock.get('*', 404);
return actions.getItemsArray(ids, host)(store.dispatch)
.catch((err) => {
expect(err).to.be.truthy
})
})
})
expect(err).to.be.truthy;
});
});
});
describe('postItem', () => {
const item = {
type: 'comment',
data:{content: 'stuff'}
}
};
it ('should post an item, return an id, then dispatch that item to the store', () => {
fetchMock.post('*', {item_id: '123', type: 'comment', data: {content: 'stuff'}})
fetchMock.post('*', {item_id: '123', type: 'comment', data: {content: 'stuff'}});
return actions.postItem(item.data, item.type, undefined, host)(store.dispatch)
.then((id) => {
expect(fetchMock.calls().matched[0][1]).to.deep.equal(
@@ -106,8 +106,8 @@ describe('itemActions', () => {
method: 'POST',
body: JSON.stringify({...item, version: 1})
}
)
expect(id).to.equal('123')
);
expect(id).to.equal('123');
expect(store.getActions()[0]).to.deep.equal({
type: actions.ADD_ITEM,
item: {
@@ -118,35 +118,35 @@ describe('itemActions', () => {
item_id: '123'
},
item_id: '123'
})
})
})
});
});
});
it('should handle an error', () => {
fetchMock.post('*', 404)
fetchMock.post('*', 404);
return actions.postItem(item, host)(store.dispatch)
.catch((err) => {
expect(err).to.be.truthy
})
})
})
expect(err).to.be.truthy;
});
});
});
describe('postAction', () => {
it ('should post an action', () => {
fetchMock.post('*', 200)
fetchMock.post('*', 200);
return actions.postAction('abc', 'flag', '123', host)(store.dispatch)
.then(response => {
expect(fetchMock.calls().matched[0][0]).to.equal('http://test.host/v1/action/flag/user/123/on/item/abc')
expect(response).to.equal('')
})
})
expect(fetchMock.calls().matched[0][0]).to.equal('http://test.host/v1/action/flag/user/123/on/item/abc');
expect(response).to.equal('');
});
});
it('should handle an error', () => {
fetchMock.post('*', 404)
fetchMock.post('*', 404);
return actions.postItem('abc', 'flag', '123', host)(store.dispatch)
.catch((err) => {
expect(err).to.be.truthy
})
})
expect(err).to.be.truthy;
});
});
})
})
});
});
@@ -1,7 +1,6 @@
import { Map, fromJS } from 'immutable'
import {expect} from 'chai'
import itemsReducer from '../../store/reducers/items'
import * as actions from '../../store/actions/items'
import {Map, fromJS} from 'immutable';
import {expect} from 'chai';
import itemsReducer from '../../store/reducers/items';
describe ('itemsReducer', () => {
describe('ADD_ITEM', () => {
@@ -16,18 +15,18 @@ describe ('itemsReducer', () => {
item_id: '123'
},
item_id: '123'
}
const store = new Map({})
const result = itemsReducer(store, action)
};
const store = new Map({});
const result = itemsReducer(store, action);
expect(result.get('123').toJS()).to.deep.equal({
type: 'comment',
data: {
content: 'stuff'
},
item_id: '123'
})
})
})
});
});
});
describe ('UPDATE_ITEM', () => {
it ('should update an item', () => {
@@ -36,7 +35,7 @@ describe ('itemsReducer', () => {
property: 'stuff',
value: 'things',
item_id: '123'
}
};
const store = fromJS({
'123': {
item_id: '123',
@@ -44,20 +43,20 @@ describe ('itemsReducer', () => {
stuff: 'morestuff'
}
}
})
const result = itemsReducer(store, action)
});
const result = itemsReducer(store, action);
expect(result.get('123').toJS()).to.deep.equal({
item_id: '123',
data: {
stuff: 'things'
}
})
})
})
});
});
});
describe('APPEND_ITEM_ARRAY', () => {
let action
let store
let action;
let store;
beforeEach (() => {
action = {
@@ -65,7 +64,7 @@ describe ('itemsReducer', () => {
property: 'stuff',
value: 'things',
item_id: '123'
}
};
store = fromJS({
'123': {
item_id: '123',
@@ -73,37 +72,37 @@ describe ('itemsReducer', () => {
stuff: ['morestuff']
}
}
})
})
});
});
it ('should append to an existing array', () => {
const result = itemsReducer(store, action)
const result = itemsReducer(store, action);
expect(result.get('123').toJS()).to.deep.equal({
item_id: '123',
data: {
stuff: ['morestuff', 'things']
}
})
})
});
});
it ('should create a new array', () => {
store = fromJS({
'123': {
item_id: '123',
data: {}
}
})
const result = itemsReducer(store, action)
});
const result = itemsReducer(store, action);
expect(result.get('123').toJS()).to.deep.equal({
item_id: '123',
data: {
stuff: ['things']
}
})
})
})
});
});
});
describe('APPEND_ITEM_RELATED', () => {
let action
let store
let action;
let store;
beforeEach (() => {
action = {
@@ -111,7 +110,7 @@ describe ('itemsReducer', () => {
property: 'stuff',
value: 'things',
item_id: '123'
}
};
store = fromJS({
'123': {
item_id: '123',
@@ -119,31 +118,31 @@ describe ('itemsReducer', () => {
stuff: ['morestuff']
}
}
})
})
});
});
it ('should append to an existing array', () => {
const result = itemsReducer(store, action)
const result = itemsReducer(store, action);
expect(result.get('123').toJS()).to.deep.equal({
item_id: '123',
related: {
stuff: ['morestuff', 'things']
}
})
})
});
});
it ('should create a new array', () => {
store = fromJS({
'123': {
item_id: '123',
related: {}
}
})
const result = itemsReducer(store, action)
});
const result = itemsReducer(store, action);
expect(result.get('123').toJS()).to.deep.equal({
item_id: '123',
related: {
stuff: ['things']
}
})
})
})
})
});
});
});
});
@@ -1,7 +1,7 @@
import { Map } from 'immutable'
import {expect} from 'chai'
import notificationReducer from '../../store/reducers/notification'
import * as actions from '../../store/actions/notification'
import {Map} from 'immutable';
import {expect} from 'chai';
import notificationReducer from '../../store/reducers/notification';
import * as actions from '../../store/actions/notification';
describe ('notificationsReducer', () => {
describe('ADD_NOTIFICATION', () => {
@@ -10,26 +10,26 @@ describe ('notificationsReducer', () => {
type: actions.ADD_NOTIFICATION,
text: 'Test notification',
notifType: 'test'
}
const store = new Map({})
const result = notificationReducer(store, action)
expect(result.get('text')).to.equal(action.text)
expect(result.get('type')).to.equal(action.notifType)
})
})
};
const store = new Map({});
const result = notificationReducer(store, action);
expect(result.get('text')).to.equal(action.text);
expect(result.get('type')).to.equal(action.notifType);
});
});
describe('CLEAR_NOTIFICATION', () => {
it('should clear a notification', () => {
const action = {
type: actions.CLEAR_NOTIFICATION
}
};
const store = new Map({
text: 'Test notification',
type: 'test'
})
const result = notificationReducer(store, action)
expect(result.get('text')).to.equal(undefined)
expect(result.get('type')).to.equal(undefined)
})
})
})
});
const result = notificationReducer(store, action);
expect(result.get('text')).to.equal(undefined);
expect(result.get('type')).to.equal(undefined);
});
});
});
+30 -28
View File
@@ -1,5 +1,5 @@
import timeago from 'timeago.js'
import esTA from 'timeago.js/locales/es'
import timeago from 'timeago.js';
import esTA from 'timeago.js/locales/es';
/**
* Default locales, this should be overriden by config file
@@ -11,32 +11,34 @@ class i18n {
* Register locales
*/
this.locales = {'en': 'en', 'es': 'es'}
timeago.register('es_ES', esTA)
this.timeagoInstance = new timeago()
this.locales = {'en': 'en', 'es': 'es'};
timeago.register('es_ES', esTA);
this.timeagoInstance = new timeago();
/**
* Load translations
*/
let trans = translations || { en: {} }
let trans = translations || {en: {}};
try {
const locale = localStorage.getItem('locale') || navigator.language
localStorage.setItem('locale', locale)
const lang = this.locales[locale.split('-')[0]] || 'en'
this.translations = trans[lang]
const locale = localStorage.getItem('locale') || navigator.language;
localStorage.setItem('locale', locale);
const lang = this.locales[locale.split('-')[0]] || 'en';
this.translations = trans[lang];
} catch (err) {
this.translations = trans['en']
this.translations = trans['en'];
}
this.setLocale = (locale) => {
try {
localStorage.setItem('locale', locale)
} catch (err) {}
}
localStorage.setItem('locale', locale);
} catch (err) {
console.error(err);
}
};
this.getLocale = () => (
localStorage.getItem('locale') || navigator.locale || 'en-US'
)
);
/**
* Expose the translation function
@@ -47,28 +49,28 @@ class i18n {
*/
this.t = (key) => {
const arr = key.split('.')
let translation = this.translations
const arr = key.split('.');
let translation = this.translations;
try {
for (var i = 0; i < arr.length; i++) translation = translation[arr[i]]
for (let i = 0; i < arr.length; i++) {translation = translation[arr[i]];}
} catch (error) {
console.warn(`${key} language key not set`)
return key
console.warn(`${key} language key not set`);
return key;
}
const val = String(translation)
const val = String(translation);
if (val) {
return val
return val;
} else {
console.warn(`${key} language key not set`)
return key
console.warn(`${key} language key not set`);
return key;
}
}
};
this.timeago = (time) => {
return this.timeagoInstance.format(time)
}
return this.timeagoInstance.format(time);
};
}
}
export default i18n
export default i18n;
+8 -8
View File
@@ -1,10 +1,10 @@
import Notification from './notification/Notification'
import store from './store/store'
import {fetchConfig} from './store/actions/config'
import * as itemActions from './store/actions/items'
import I18n from './i18n/i18n'
import * as notificationActions from './store/actions/notification'
import * as authActions from './store/actions/auth'
import Notification from './notification/Notification';
import store from './store/store';
import {fetchConfig} from './store/actions/config';
import * as itemActions from './store/actions/items';
import I18n from './i18n/i18n';
import * as notificationActions from './store/actions/notification';
import * as authActions from './store/actions/auth';
export {
Notification,
@@ -14,4 +14,4 @@ export {
I18n,
notificationActions,
authActions
}
};
@@ -1,19 +1,19 @@
import React from 'react'
import React from 'react';
const Notification = (props) => {
if (props.notification.text) {
setTimeout(() => {
props.clearNotification()
}, props.notifLength)
props.clearNotification();
}, props.notifLength);
}
return <div>
{
props.notification.text &&
<dialog open id='coral-notif' className={'coral-notif-' + props.notification.type}>
<dialog open id='coral-notif' className={`coral-notif-${ props.notification.type}`}>
{props.notification.text}
</dialog>
}
</div>
}
</div>;
};
export default Notification
export default Notification;
+6 -6
View File
@@ -1,17 +1,17 @@
/* Auth Actions */
export const SET_LOGGED_IN_USER = 'SET_LOGGED_IN_USER'
export const LOG_OUT_USER = 'LOG_OUT_USER'
export const SET_LOGGED_IN_USER = 'SET_LOGGED_IN_USER';
export const LOG_OUT_USER = 'LOG_OUT_USER';
export const setLoggedInUser = (user_id) => {
return {
type: SET_LOGGED_IN_USER,
user_id
}
}
};
};
export const LogOutUser = () => {
return {
type: LOG_OUT_USER
}
}
};
};
@@ -1,28 +1,28 @@
/* @flow */
import { fromJS } from 'immutable'
import {fromJS} from 'immutable';
/**
* Action name constants
*/
export const FETCH_CONFIG_REQUEST = 'FETCH_CONFIG_REQUEST'
export const FETCH_CONFIG_FAILED = 'FETCH_CONFIG_FAILED'
export const FETCH_CONFIG_SUCCESS = 'FETCH_CONFIG_SUCCESS'
export const FETCH_CONFIG_REQUEST = 'FETCH_CONFIG_REQUEST';
export const FETCH_CONFIG_FAILED = 'FETCH_CONFIG_FAILED';
export const FETCH_CONFIG_SUCCESS = 'FETCH_CONFIG_SUCCESS';
/**
* Action creators
*/
export const fetchConfig = () => async (dispatch) => {
dispatch({ type: FETCH_CONFIG_REQUEST })
dispatch({type: FETCH_CONFIG_REQUEST});
try {
//TODO: Replace with fetching config from backend
// const response = await fetch(`./talk.config.json`)
// const json = await response.json()
dispatch({ type: FETCH_CONFIG_SUCCESS, config: fromJS({}) })
dispatch({type: FETCH_CONFIG_SUCCESS, config: fromJS({})});
} catch (error) {
dispatch({ type: FETCH_CONFIG_FAILED })
dispatch({type: FETCH_CONFIG_FAILED});
}
}
};
+50 -54
View File
@@ -1,15 +1,12 @@
/* Item Actions */
import { fromJS } from 'immutable'
import mocks from '../../mocks.json'
/**
* Action name constants
*/
export const ADD_ITEM = 'ADD_ITEM'
export const UPDATE_ITEM = 'UPDATE_ITEM'
export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY'
export const ADD_ITEM = 'ADD_ITEM';
export const UPDATE_ITEM = 'UPDATE_ITEM';
export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY';
/**
* Action creators
@@ -26,14 +23,14 @@ export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY'
export const addItem = (item) => {
if (!item.id) {
console.warn('addItem called without an item id.')
console.warn('addItem called without an item id.');
}
return {
type: ADD_ITEM,
item: item,
id: item.id
}
}
};
};
/*
* Updates an item in the local store without posting it to the server
@@ -46,15 +43,14 @@ export const addItem = (item) => {
*
*/
export const updateItem = (id, property, value) => {
return {
type: UPDATE_ITEM,
id,
property,
value
}
}
};
};
export const appendItemArray = (id, property, value, addToFront) => {
return {
@@ -63,8 +59,8 @@ export const appendItemArray = (id, property, value, addToFront) => {
property,
value,
addToFront
}
}
};
};
/*
* Get Items from Query
@@ -81,47 +77,47 @@ export const appendItemArray = (id, property, value, addToFront) => {
*/
export function getStream (assetId) {
return (dispatch) => {
return fetch('/api/v1/stream?asset_id='+assetId)
return fetch(`/api/v1/stream?asset_id=${assetId}`)
.then(
response => {
return response.ok ? response.json() : Promise.reject(response.status + ' ' + response.statusText)
return response.ok ? response.json() : Promise.reject(`${response.status } ${ response.statusText}`);
}
)
.then((json) => {
/* Sort comments by date*/
let rootComments = []
let childComments = {}
json.sort((a,b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
json.reduce((prev, item) => {
dispatch(addItem(item))
let rootComments = [];
let childComments = {};
json.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
json.forEach(item => {
dispatch(addItem(item));
/* Check for root and child comments. */
if (
item.asset_id === assetId &&
!item.parent_id) {
rootComments.push(item.id)
rootComments.push(item.id);
} else if (
item.asset_id === assetId
) {
let children = childComments[item.parent_id] || []
childComments[item.parent_id] = children.concat(item.id)
let children = childComments[item.parent_id] || [];
childComments[item.parent_id] = children.concat(item.id);
}
}, {})
}, {});
dispatch(addItem({
id: assetId,
comments: rootComments
}))
}));
const keys = Object.keys(childComments)
for (var i=0; i < keys.length; i++ ) {
dispatch(updateItem(keys[i], 'children', childComments[keys[i]].reverse()))
const keys = Object.keys(childComments);
for (let i = 0; i < keys.length; i++ ) {
dispatch(updateItem(keys[i], 'children', childComments[keys[i]].reverse()));
}
return (json)
})
}
return (json);
});
};
}
/*
@@ -140,20 +136,20 @@ export function getStream (assetId) {
export function getItemsArray (ids) {
return (dispatch) => {
return fetch('/v1/item/' + ids)
return fetch(`/v1/item/${ ids}`)
.then(
response => {
return response.ok ? response.json()
: Promise.reject(response.status + ' ' + response.statusText)
: Promise.reject(`${response.status } ${ response.statusText}`);
}
)
.then((json) => {
for (var i = 0; i < json.items.length; i++) {
dispatch(addItem(json.items[i]))
for (let i = 0; i < json.items.length; i++) {
dispatch(addItem(json.items[i]));
}
return json.items
})
}
return json.items;
});
};
}
/*
@@ -173,7 +169,7 @@ export function getItemsArray (ids) {
export function postItem (item, type, id) {
return (dispatch) => {
if (id) {
item.id = id
item.id = id;
}
let options = {
method: 'POST',
@@ -181,20 +177,20 @@ export function postItem (item, type, id) {
headers: {
'Content-Type':'application/json'
}
}
};
console.log('postItem', options);
return fetch('/api/v1/' + type, options)
return fetch(`/api/v1/${ type}`, options)
.then(
response => {
return response.ok ? response.json()
: Promise.reject(response.status + ' ' + response.statusText)
: Promise.reject(`${response.status } ${ response.statusText}`);
}
)
.then((json) => {
dispatch(addItem({...item, id:json.id}))
return json.id
})
}
dispatch(addItem({...item, id:json.id}));
return json.id;
});
};
}
//http://localhost:16180/v1/action/flag/user/user_89654/on/item/87e418c5-aafb-4eb7-9ce4-78f28793782a
@@ -219,19 +215,19 @@ export function postAction (id, type, user_id) {
const action = {
type,
user_id
}
};
const options = {
method: 'POST',
body: JSON.stringify(action)
}
};
dispatch(appendItemArray(id, type, user_id))
return fetch('/api/v1/comments/' + id + '/actions', options)
dispatch(appendItemArray(id, type, user_id));
return fetch(`/api/v1/comments/${ id }/actions`, options)
.then(
response => {
return response.ok ? response.text()
: Promise.reject(response.status + ' ' + response.statusText)
: Promise.reject(`${response.status } ${ response.statusText}`);
}
)
}
);
};
}
@@ -1,18 +1,18 @@
/* Notification Actions */
export const ADD_NOTIFICATION = 'ADD_NOTIFICATION'
export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION'
export const ADD_NOTIFICATION = 'ADD_NOTIFICATION';
export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION';
export const addNotification = (notifType, text) => {
return {
type: ADD_NOTIFICATION,
notifType,
text
}
}
};
};
export const clearNotification = () => {
return {
type: CLEAR_NOTIFICATION
}
}
};
};
+10 -10
View File
@@ -1,17 +1,17 @@
/* Auth */
import * as actions from '../actions/auth'
import { fromJS } from 'immutable'
import * as actions from '../actions/auth';
import {fromJS} from 'immutable';
const initialState = fromJS({})
const initialState = fromJS({});
export default (state = initialState, action) => {
switch (action.type) {
case actions.SET_LOGGED_IN_USER:
return state.set('user', action.user_id)
case actions.LOG_OUT_USER:
return initialState
default:
return state
case actions.SET_LOGGED_IN_USER:
return state.set('user', action.user_id);
case actions.LOG_OUT_USER:
return initialState;
default:
return state;
}
}
};
+12 -12
View File
@@ -1,25 +1,25 @@
/* @flow */
import { Map, fromJS } from 'immutable'
import * as actions from '../actions/config'
import {Map} from 'immutable';
import * as actions from '../actions/config';
const initialState = Map({
features: Map({})
})
});
export default (state = initialState, action) => {
switch(action.type) {
case actions.FETCH_CONFIG_REQUEST:
return state.set('loading', true)
case actions.FETCH_CONFIG_REQUEST:
return state.set('loading', true);
case actions.FETCH_CONFIG_FAILED:
return state.set('loading', false)
case actions.FETCH_CONFIG_FAILED:
return state.set('loading', false);
// Override config if worked
case actions.FETCH_CONFIG_SUCCESS:
return action.config.set('loading', false)
case actions.FETCH_CONFIG_SUCCESS:
return action.config.set('loading', false);
default:
return state
default:
return state;
}
}
};
@@ -1,10 +1,10 @@
/* @flow */
import { combineReducers } from 'redux'
import config from './config'
import items from './items'
import notification from './notification'
import auth from './auth'
import {combineReducers} from 'redux';
import config from './config';
import items from './items';
import notification from './notification';
import auth from './auth';
/**
* Expose the combined main reducer
@@ -15,4 +15,4 @@ export default combineReducers({
items,
notification,
auth
})
});
+20 -20
View File
@@ -1,29 +1,29 @@
/* Items Reducer */
import { Map, fromJS } from 'immutable'
import * as actions from '../actions/items'
import {fromJS} from 'immutable';
import * as actions from '../actions/items';
const initialState = fromJS({})
const initialState = fromJS({});
export default (state = initialState, action) => {
switch (action.type) {
case actions.ADD_ITEM:
return state.set(action.id, fromJS(action.item))
case actions.UPDATE_ITEM:
return state.updateIn([action.id, action.property], () =>
case actions.ADD_ITEM:
return state.set(action.id, fromJS(action.item));
case actions.UPDATE_ITEM:
return state.updateIn([action.id, action.property], () =>
fromJS(action.value)
)
case actions.APPEND_ITEM_ARRAY:
return state.updateIn([action.id, action.property], (prop) => {
if (action.addToFront) {
return prop ? prop.unshift(action.value) : fromJS([action.value])
} else {
return prop ? prop.push(action.value) : fromJS([action.value])
}
);
case actions.APPEND_ITEM_ARRAY:
return state.updateIn([action.id, action.property], (prop) => {
if (action.addToFront) {
return prop ? prop.unshift(action.value) : fromJS([action.value]);
} else {
return prop ? prop.push(action.value) : fromJS([action.value]);
}
)
default:
return state
}
);
default:
return state;
}
}
};
@@ -1,17 +1,17 @@
/* Items Notifications */
import * as actions from '../actions/notification'
import { fromJS } from 'immutable'
import * as actions from '../actions/notification';
import {fromJS} from 'immutable';
const initialState = fromJS({})
const initialState = fromJS({});
export default (state = initialState, action) => {
switch (action.type) {
case actions.ADD_NOTIFICATION:
return state.set('text', action.text).set('type', action.notifType)
case actions.CLEAR_NOTIFICATION:
return initialState
default:
return state
case actions.ADD_NOTIFICATION:
return state.set('text', action.text).set('type', action.notifType);
case actions.CLEAR_NOTIFICATION:
return initialState;
default:
return state;
}
}
};
+4 -4
View File
@@ -1,10 +1,10 @@
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import mainReducer from './reducers'
import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import mainReducer from './reducers';
export default createStore(
mainReducer,
window.devToolsExtension && window.devToolsExtension(),
applyMiddleware(thunk)
)
);
@@ -1,9 +1,9 @@
import React from 'react'
const packagename = 'coral-plugin-author-name'
import React from 'react';
const packagename = 'coral-plugin-author-name';
const AuthorName = ({name}) =>
<div className={packagename + '-text'}>
<div className={`${packagename }-text`}>
{name}
</div>
</div>;
export default AuthorName
export default AuthorName;
@@ -1,23 +1,23 @@
import React from 'react'
import React from 'react';
const name = 'coral-plugin-comment-count'
const name = 'coral-plugin-comment-count';
const CommentCount = ({items, id}) => {
let count = 0
let count = 0;
if (items[id]) {
count += items[id].comments.length
count += items[id].comments.length;
}
const itemKeys = Object.keys(items)
for (var i=0; i < itemKeys.length; i++) {
const item = items[itemKeys[i]]
const itemKeys = Object.keys(items);
for (let i = 0; i < itemKeys.length; i++) {
const item = items[itemKeys[i]];
if (item.children) {
count += item.children.length
count += item.children.length;
}
}
return <div className={name + '-text'}>
{count + ' ' + (count === 1 ? 'Comment':'Comments')}
</div>
}
return <div className={`${name }-text`}>
{`${count } ${ count === 1 ? 'Comment' : 'Comments'}`}
</div>;
};
export default CommentCount
export default CommentCount;
+25 -25
View File
@@ -1,7 +1,7 @@
import React, {Component, PropTypes} from 'react'
import {I18n} from '../coral-framework'
import React, {Component, PropTypes} from 'react';
import {I18n} from '../coral-framework';
const name='coral-plugin-commentbox'
const name = 'coral-plugin-commentbox';
class CommentBox extends Component {
@@ -19,35 +19,35 @@ class CommentBox extends Component {
}
postComment = () => {
const {postItem, updateItem, id, parent_id, addNotification, appendItemArray} = this.props
const {postItem, updateItem, id, parent_id, addNotification, appendItemArray} = this.props;
let comment = {
body: this.state.body,
asset_id: id,
username: this.state.username
}
let related
};
let related;
if (parent_id) {
comment.parent_id = parent_id
related = 'children'
comment.parent_id = parent_id;
related = 'children';
} else {
related = 'comments'
related = 'comments';
}
updateItem(parent_id, 'showReply', false)
updateItem(parent_id, 'showReply', false);
postItem(comment, 'comments')
.then((comment_id) => {
appendItemArray(parent_id || id, related, comment_id, parent_id ? false : true)
addNotification('success', 'Your comment has been posted.')
}).catch((err) => console.error(err))
this.setState({body: ''})
appendItemArray((parent_id || id, related, comment_id, parent_id));
addNotification('success', 'Your comment has been posted.');
}).catch((err) => console.error(err));
this.setState({body: ''});
}
render () {
const {styles, reply} = this.props
const {styles, reply} = this.props;
// How to handle language in plugins? Should we have a dependency on our central translation file?
return <div>
<div className={name + '-container'}>
<div className={`${name }-container`}>
<input type='text'
className={name + '-username'}
className={`${name }-username`}
style={styles && styles.textarea}
value={this.state.username}
id={reply ? 'replyUser' : 'commentUser'}
@@ -55,15 +55,15 @@ class CommentBox extends Component {
onChange={(e) => this.setState({username: e.target.value})}/>
</div>
<div
className={name + '-container'}>
className={`${name }-container`}>
<label
htmlFor={ reply ? 'replyText' : 'commentText'}
className="screen-reader-text"
aria-hidden={true}>
{reply ? lang.t('reply'): lang.t('comment')}
{reply ? lang.t('reply') : lang.t('comment')}
</label>
<textarea
className={name + '-textarea'}
className={`${name }-textarea`}
style={styles && styles.textarea}
value={this.state.body}
placeholder='Comment'
@@ -71,19 +71,19 @@ class CommentBox extends Component {
onChange={(e) => this.setState({body: e.target.value})}
rows={3}/>
</div>
<div className={name + '-button-container'}>
<div className={`${name }-button-container`}>
<button
className={name + '-button'}
className={`${name }-button`}
style={styles && styles.button}
onClick={this.postComment}>
{lang.t('post')}
</button>
</div>
</div>
</div>;
}
}
export default CommentBox
export default CommentBox;
const lang = new I18n({
en: {
@@ -96,4 +96,4 @@ const lang = new I18n({
reply: 'Respuesta',
comment: 'Comentario'
}
})
});
@@ -1,26 +1,26 @@
import React from 'react'
import {shallow, mount} from 'enzyme'
import {expect} from 'chai'
import CommentBox from '../CommentBox'
import React from 'react';
import {shallow} from 'enzyme';
import {expect} from 'chai';
import CommentBox from '../CommentBox';
describe('CommentBox', () => {
let comment
let render
let comment;
let render;
beforeEach(() => {
comment = {}
comment = {};
const postItem = (item) => {
comment.posted=item
return Promise.resolve(4)
}
comment.posted = item;
return Promise.resolve(4);
};
render = shallow(<CommentBox
postItem={postItem}
updateItem={(e) => comment.text=e.target.value}
updateItem={(e) => comment.text = e.target.value}
item_id={'1'}
comments={['1', '2', '3']}/>)
})
comments={['1', '2', '3']}/>);
});
it('should render the CommentBox appropriately', () => {
expect(render.contains('<div class="CommentBox"')).to.be.truthy
expect(render.contains('<button class="postCommentButton"')).to.be.truthy
})
})
expect(render.contains('<div class="CommentBox"')).to.be.truthy;
expect(render.contains('<button class="postCommentButton"')).to.be.truthy;
});
});
@@ -1,17 +1,17 @@
import React from 'react'
const name = 'coral-plugin-replies'
import React from 'react';
const name = 'coral-plugin-replies';
const Content = ({body, styles}) => {
const textbreaks = body.split('\n')
const textbreaks = body.split('\n');
return <div
className={name + '-text'}
className={`${name }-text`}
style={styles && styles.text}>
{
textbreaks.map((line, i) => <span key={i} className={name+'-line'}>
{line} <br className={name+'-linebreak'}/>
textbreaks.map((line, i) => <span key={i} className={`${name}-line`}>
{line} <br className={`${name}-linebreak`}/>
</span>)
}
</div>
}
</div>;
};
export default Content
export default Content;
@@ -1,11 +1,11 @@
import React from 'react'
import {shallow, mount} from 'enzyme'
import {expect} from 'chai'
import CommentContent from '../CommentContent'
import React from 'react';
import {shallow} from 'enzyme';
import {expect} from 'chai';
import CommentContent from '../CommentContent';
describe('CommentContent', () => {
it('should render content', () => {
const render = shallow(<CommentContent content="test"/>)
expect(render.contains('test')).to.be.truthy
})
})
const render = shallow(<CommentContent content="test"/>);
expect(render.contains('test')).to.be.truthy;
});
});
+15 -15
View File
@@ -1,29 +1,29 @@
import React from 'react'
import React from 'react';
const name='coral-plugin-flags'
const name = 'coral-plugin-flags';
const FlagButton = ({flag, item_id, postAction, currentUser, addNotification}) => {
const flagged = flag && flag.includes(currentUser)
const flagged = flag && flag.includes(currentUser);
const onFlagClick = () => {
postAction(item_id, 'flag', currentUser)
addNotification('success', 'Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.')
postAction(item_id, 'flag', currentUser);
addNotification('success', 'Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.');
}
return <div className={name + '-container'}>
<button onClick={onFlagClick} className={name + '-button'}>
<i className={name + '-icon material-icons'}
};
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>
{
flagged
? <span className={name + '-button-text'}>Flagged</span>
: <span className={name + '-button-text'}>Flag</span>
? <span className={`${name }-button-text`}>Flagged</span>
: <span className={`${name }-button-text`}>Flag</span>
}
</button>
</div>
}
</div>;
};
export default FlagButton
export default FlagButton;
const styles = {
flaggedIcon: {
@@ -32,4 +32,4 @@ const styles = {
unflaggedIcon: {
color: 'inherit'
}
}
};
+7 -7
View File
@@ -1,11 +1,11 @@
import React from 'react'
import {I18n} from '../coral-framework'
import React from 'react';
import {I18n} from '../coral-framework';
const lang = new I18n()
const name = 'coral-plugin-pubdate'
const lang = new I18n();
const name = 'coral-plugin-pubdate';
const PubDate = ({created_at}) => <div className={name + '-text'}>
const PubDate = ({created_at}) => <div className={`${name }-text`}>
{lang.timeago(created_at)}
</div>
</div>;
export default PubDate
export default PubDate;
+6 -6
View File
@@ -1,10 +1,10 @@
import React from 'react'
import CommentBox from '../coral-plugin-commentbox/CommentBox'
import React from 'react';
import CommentBox from '../coral-plugin-commentbox/CommentBox';
const name = 'coral-plugin-replies'
const name = 'coral-plugin-replies';
const ReplyBox = (props) => <div
className={name + '-textarea'}
className={`${name }-textarea`}
style={props.styles && props.styles.container}>
{
props.showReply && <CommentBox
@@ -17,6 +17,6 @@ const ReplyBox = (props) => <div
comments = {props.child}
reply = {true}/>
}
</div>
</div>;
export default ReplyBox
export default ReplyBox;
+9 -9
View File
@@ -1,17 +1,17 @@
import React from 'react'
import {I18n} from '../coral-framework'
import React from 'react';
import {I18n} from '../coral-framework';
const name = 'coral-plugin-replies'
const name = 'coral-plugin-replies';
const ReplyButton = (props) => <button
className={name + '-reply-button'}
onClick={(e) => props.updateItem(props.id || props.parent_id, 'showReply', true)}>
<i className={name + '-icon material-icons'}
className={`${name }-reply-button`}
onClick={() => props.updateItem(props.id || props.parent_id, 'showReply', true)}>
<i className={`${name }-icon material-icons`}
aria-hidden={true}>reply</i>
{lang.t('reply')}
</button>
</button>;
export default ReplyButton
export default ReplyButton;
const lang = new I18n({
en: {
@@ -20,4 +20,4 @@ const lang = new I18n({
es: {
'reply': '¡traduceme!'
}
})
});
+3 -3
View File
@@ -1,7 +1,7 @@
import ReplyBox from './ReplyBox'
import ReplyButton from './ReplyButton'
import ReplyBox from './ReplyBox';
import ReplyButton from './ReplyButton';
export {
ReplyBox,
ReplyButton
}
};
+3
View File
@@ -55,6 +55,8 @@
"uuid": "^2.0.3"
},
"devDependencies": {
"babel-eslint": "^7.1.0",
"babel-jest": "^15.0.0",
"autoprefixer": "^6.5.0",
"babel-core": "^6.18.2",
"babel-loader": "^6.2.7",
@@ -71,6 +73,7 @@
"copy-webpack-plugin": "^4.0.0",
"css-loader": "^0.25.0",
"eslint": "^3.9.1",
"eslint-plugin-react": "^6.6.0",
"exports-loader": "^0.6.3",
"hammerjs": "^2.0.8",
"immutable": "^3.8.1",