Merge pull request #51 from coralproject/reader-flags

Reader flags
This commit is contained in:
David Erwin
2016-11-10 13:26:38 -05:00
committed by GitHub
20 changed files with 479 additions and 391 deletions
@@ -45,15 +45,16 @@ Promise.all([fetch('/api/v1/comments/status/pending'), fetch('/api/v1/comments/s
.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.get('id')}/status`, {
method: 'POST',
headers: jsonHeader,
body: JSON.stringify({status: comment.get('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 => res.json())
.then(res => store.dispatch({type: 'COMMENT_UPDATE_SUCCESS', res}))
.catch(error => store.dispatch({type: 'COMMENT_UPDATE_FAILED', error}));
};
// Create a new comment
+114 -115
View File
@@ -13,52 +13,52 @@ 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';
import FlagButton from '../../coral-plugin-flags/FlagButton';
const {addItem, updateItem, postItem, getStream, postAction, appendItemArray} = itemActions;
const {addNotification, clearNotification} = notificationActions;
const {setLoggedInUser} = authActions;
@connect(
(state) => {
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));
},
updateItem: (id, property, value) => {
return dispatch(updateItem(id, property, value));
},
postItem: (data, type, id) => {
return dispatch(postItem(data, type, id));
},
getStream: (rootId) => {
return dispatch(getStream(rootId));
},
addNotification: (type, text) => {
return dispatch(addNotification(type, text));
},
clearNotification: () => {
return dispatch(clearNotification());
},
setLoggedInUser: (user_id) => {
return dispatch(setLoggedInUser(user_id));
},
postAction: (item, action, user) => {
return dispatch(postAction(item, action, user));
},
appendItemArray: (item, property, value, addToFront) => {
return dispatch(appendItemArray(item, property, value, addToFront));
}
};
}
)
const mapStateToProps = (state) => {
return {
config: state.config.toJS(),
items: state.items.toJS(),
notification: state.notification.toJS(),
auth: state.auth.toJS()
};
};
const mapDispatchToProps = (dispatch) => {
return {
addItem: (item, itemType) => {
return dispatch(addItem(item, itemType));
},
updateItem: (id, property, value, itemType) => {
return dispatch(updateItem(id, property, value, itemType));
},
postItem: (data, type, id) => {
return dispatch(postItem(data, type, id));
},
getStream: (rootId) => {
return dispatch(getStream(rootId));
},
addNotification: (type, text) => {
return dispatch(addNotification(type, text));
},
clearNotification: () => {
return dispatch(clearNotification());
},
setLoggedInUser: (user_id) => {
return dispatch(setLoggedInUser(user_id));
},
postAction: (item, action, user, itemType) => {
return dispatch(postAction(item, action, user, itemType));
},
appendItemArray: (item, property, value, addToFront, itemType) => {
return dispatch(appendItemArray(item, property, value, addToFront, itemType));
}
};
};
class CommentStream extends Component {
@@ -90,93 +90,92 @@ class CommentStream extends Component {
});
}
// TODO: Replace teststream id with id from params
// TODO: Replace teststream id with id from params
const rootItemId = 'assetTest';
const rootItem = this.props.items[rootItemId];
const rootItem = this.props.items.assets && this.props.items.assets[rootItemId];
return <div>
{
rootItem ?
<div>
<div id="commentBox">
<Count
id={rootItemId}
items={this.props.items}/>
<CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
reply={false}/>
</div>
{
rootItem.comments.map((commentId) => {
const comment = this.props.items[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">
{
// <Flag
// addNotification={this.props.addNotification}
// id={commentId}
// flag={comment.flag}
// postAction={this.props.postAction}
// currentUser={this.props.auth.user}/>
}
<ReplyButton
updateItem={this.props.updateItem}
id={commentId}/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
parent_id={commentId}
showReply={comment.showReply}/>
{
comment.children &&
comment.children.map((replyId) => {
let reply = this.props.items[replyId];
return <div className="reply" key={replyId}>
{
rootItem
? <div>
<div id="commentBox">
<Count
id={rootItemId}
items={this.props.items}/>
<CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
reply={false}/>
</div>
{
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">
<FlagButton
addNotification={this.props.addNotification}
id={commentId}
flag={this.props.items.actions[comment.flag]}
postAction={this.props.postAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<ReplyButton
updateItem={this.props.updateItem}
id={commentId}/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
parent_id={commentId}
showReply={comment.showReply}/>
{
comment.children &&
comment.children.map((replyId) => {
let reply = this.props.items.comments[replyId];
return <div className="reply" key={replyId}>
<hr aria-hidden={true}/>
<AuthorName name={reply.username}/>
<PubDate created_at={reply.created_at}/>
<Content body={reply.body}/>
<div className="replyActions">
{
// <Flag
// addNotificiation={this.props.addNotification}
// id={replyId}
// flag={reply.flag}
// postAction={this.props.postAction}
// currentUser={this.props.auth.user}/>
}
<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}/>
</div>
</div>;
})
}
</div>;
})
}
<Notification
notifLength={this.props.config.notifLength}
clearNotification={this.props.clearNotification}
notification={this.props.notification}/>
</div>
: 'Loading'
}
</div>;
})
}
</div>;
})
}
<Notification
notifLength={this.props.config.notifLength}
clearNotification={this.props.clearNotification}
notification={this.props.notification}/>
</div>
: 'Loading'
}
</div>;
}
}
export default CommentStream;
export default connect(mapStateToProps, mapDispatchToProps)(CommentStream);
@@ -1,148 +0,0 @@
import {Map, fromJS} from 'immutable';
import {expect} from 'chai';
import itemsReducer from '../../store/reducers/items';
describe ('itemsReducer', () => {
describe('ADD_ITEM', () => {
it('should add an item', () => {
const action = {
type: 'ADD_ITEM',
item: {
type: 'comment',
data: {
content: 'stuff'
},
item_id: '123'
},
item_id: '123'
};
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', () => {
const action = {
type: 'UPDATE_ITEM',
property: 'stuff',
value: 'things',
item_id: '123'
};
const store = fromJS({
'123': {
item_id: '123',
data: {
stuff: 'morestuff'
}
}
});
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;
beforeEach (() => {
action = {
type: 'APPEND_ITEM_ARRAY',
property: 'stuff',
value: 'things',
item_id: '123'
};
store = fromJS({
'123': {
item_id: '123',
data: {
stuff: ['morestuff']
}
}
});
});
it ('should append to an existing array', () => {
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);
expect(result.get('123').toJS()).to.deep.equal({
item_id: '123',
data: {
stuff: ['things']
}
});
});
});
describe('APPEND_ITEM_RELATED', () => {
let action;
let store;
beforeEach (() => {
action = {
type: 'APPEND_ITEM_RELATED',
property: 'stuff',
value: 'things',
item_id: '123'
};
store = fromJS({
'123': {
item_id: '123',
related: {
stuff: ['morestuff']
}
}
});
});
it ('should append to an existing array', () => {
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);
expect(result.get('123').toJS()).to.deep.equal({
item_id: '123',
related: {
stuff: ['things']
}
});
});
});
});
@@ -21,7 +21,9 @@ export const fetchConfig = () => async (dispatch) => {
//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({
notifLength: 4500
})});
} catch (error) {
dispatch({type: FETCH_CONFIG_FAILED});
}
+50 -35
View File
@@ -21,13 +21,14 @@ export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY';
*
*/
export const addItem = (item) => {
export const addItem = (item, item_type) => {
if (!item.id) {
console.warn('addItem called without an item id.');
}
return {
type: ADD_ITEM,
item: item,
item,
item_type,
id: item.id
};
};
@@ -42,23 +43,24 @@ export const addItem = (item) => {
* value - the value that the property should be set to
*
*/
export const updateItem = (id, property, value) => {
export const updateItem = (id, property, value, item_type) => {
return {
type: UPDATE_ITEM,
id,
property,
value
value,
item_type
};
};
export const appendItemArray = (id, property, value, addToFront) => {
export const appendItemArray = (id, property, value, add_to_front, item_type) => {
return {
type: APPEND_ITEM_ARRAY,
id,
property,
value,
addToFront
add_to_front,
item_type
};
};
@@ -80,39 +82,49 @@ export function getStream (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.forEach(item => {
dispatch(addItem(item));
/* Add items to the store */
const itemTypes = Object.keys(json);
for (let i = 0; i < itemTypes.length; i++ ) {
for (let j = 0; j < json[itemTypes[i]].length; j++ ) {
dispatch(addItem(json[itemTypes[i]][j], itemTypes[i]));
}
}
/* 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) => {
/* Check for root and child comments. */
if (
item.asset_id === assetId &&
!item.parent_id) {
rootComments.push(item.id);
h.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 = h.childComments[item.parent_id] || [];
h.childComments[item.parent_id] = children.concat(item.id);
}
}, {});
return h;
}, {rootComments: [], childComments: {}});
dispatch(addItem({
id: assetId,
comments: rootComments
}));
comments: rels.rootComments,
}, 'assets'));
const keys = Object.keys(childComments);
for (let i = 0; i < keys.length; i++ ) {
dispatch(updateItem(keys[i], 'children', childComments[keys[i]].reverse()));
const childKeys = Object.keys(rels.childComments);
for (let i = 0; i < childKeys.length; i++ ) {
dispatch(updateItem(childKeys[i], 'children', rels.childComments[childKeys[i]].reverse(), 'comments'));
}
/* Hydrate actions on comments */
for (let i = 0; i < json.actions.length; i++ ) {
dispatch(updateItem(json.actions[i].item_id, json.actions[i].action_type, json.actions[i].id, 'comments'));
}
return (json);
@@ -178,16 +190,15 @@ export function postItem (item, type, id) {
'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}));
dispatch(addItem({...item, id:json.id}, type));
return json.id;
});
};
@@ -210,24 +221,28 @@ export function postItem (item, type, id) {
*
*/
export function postAction (id, type, user_id) {
return (dispatch) => {
export function postAction (item_id, action_type, user_id, item_type) {
return () => {
const action = {
type,
action_type,
user_id
};
const options = {
method: 'POST',
headers: {
'Content-Type':'application/json'
},
body: JSON.stringify(action)
};
dispatch(appendItemArray(id, type, user_id));
return fetch(`/api/v1/comments/${ id }/actions`, options)
return fetch(`/api/v1/${item_type}/${item_id}/actions`, options)
.then(
response => {
return response.ok ? response.text()
: Promise.reject(`${response.status } ${ response.statusText}`);
return response.ok ? response.json()
: Promise.reject(`${response.status} ${response.statusText}`);
}
);
).then((json)=>{
return json;
});
};
}
+13 -12
View File
@@ -3,26 +3,27 @@
import {fromJS} from 'immutable';
import * as actions from '../actions/items';
const initialState = fromJS({});
const initialState = fromJS({
comments: {},
users: {},
actions: {}
});
export default (state = initialState, action) => {
switch (action.type) {
case actions.ADD_ITEM:
return state.set(action.id, fromJS(action.item));
return state.setIn([action.item_type, action.id], fromJS(action.item));
case actions.UPDATE_ITEM:
return state.updateIn([action.id, action.property], () =>
fromJS(action.value)
);
return state.setIn([action.item_type, 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]);
return state.updateIn([action.item_type, action.id, action.property], (prop) => {
console.log(prop);
if (action.add_to_front) {
return prop ? prop.unshift(fromJS(action.value)) : fromJS([action.value]);
} else {
return prop ? prop.push(action.value) : fromJS([action.value]);
return prop ? prop.push(fromJS(action.value)) : fromJS([action.value]);
}
}
);
});
default:
return state;
}
+11 -8
View File
@@ -26,16 +26,19 @@ class CommentBox extends Component {
username: this.state.username
};
let related;
let parent_type;
if (parent_id) {
comment.parent_id = parent_id;
related = 'children';
parent_type = 'comments';
} else {
related = 'comments';
parent_type = 'assets';
}
updateItem(parent_id, 'showReply', false);
updateItem(parent_id, 'showReply', false, 'comments');
postItem(comment, 'comments')
.then((comment_id) => {
appendItemArray((parent_id || id, related, comment_id, parent_id));
appendItemArray(parent_id || id, related, comment_id, !parent_id, parent_type);
addNotification('success', 'Your comment has been posted.');
}).catch((err) => console.error(err));
this.setState({body: ''});
@@ -45,9 +48,9 @@ class CommentBox extends Component {
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,7 +58,7 @@ 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"
@@ -63,7 +66,7 @@ class CommentBox extends Component {
{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,9 +74,9 @@ 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')}
@@ -4,7 +4,7 @@ const name = 'coral-plugin-replies';
const Content = ({body, styles}) => {
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`}>
+8 -4
View File
@@ -2,13 +2,17 @@ import React from 'react';
const name = 'coral-plugin-flags';
const FlagButton = ({flag, item_id, postAction, currentUser, addNotification}) => {
const flagged = flag && flag.includes(currentUser);
const FlagButton = ({flag, id, postAction, addItem, updateItem, addNotification}) => {
const flagged = flag && flag.current_user;
const onFlagClick = () => {
postAction(item_id, 'flag', currentUser);
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', '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`}
+3 -3
View File
@@ -4,9 +4,9 @@ import {I18n} from '../coral-framework';
const name = 'coral-plugin-replies';
const ReplyButton = (props) => <button
className={`${name }-reply-button`}
onClick={() => 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, 'comments')}>
<i className={`${name}-icon material-icons`}
aria-hidden={true}>reply</i>
{lang.t('reply')}
</button>;
+32
View File
@@ -38,6 +38,38 @@ ActionSchema.statics.findByItemIdArray = function(item_ids) {
};
/**
* Returns summaries of actions for an array of ids
* @param {String} ids array of user identifiers (uuid)
*/
ActionSchema.statics.getActionSummaries = function(item_ids) {
return ActionSchema.statics.findByItemIdArray(item_ids).then((rawActions) => {
// Create an object with a count of each action type for each item
const actionSummaries = rawActions.reduce((actionObj, action) => {
if (!actionObj[action.item_id]) {
actionObj[action.item_id] = {
id: action.id,
item_type: action.item_type,
action_type: action.action_type,
count: 1,
current_user: false //Update this later when we have authentication
};
} else {
actionObj[action.item_id].count ++;
}
return actionObj;
}, {});
// Return an array extracted from the actionSummaries object
return Object.keys(actionSummaries).reduce((actions, key) => {
let actionSummary = actionSummaries[key];
actionSummary.item_id = key;
actions.push(actionSummary);
return actions;
}, []);
});
};
/*
* Finds all comments for a specific action.
* @param {String} action_type type of action
* @param {String} item_type type of item the action is on
+13 -5
View File
@@ -8,9 +8,10 @@
"build": "webpack --config webpack.config.js --bail",
"build-watch": "webpack --config webpack.config.dev.js --watch",
"lint": "eslint bin/* .",
"lint-fix": "eslint . --fix",
"pretest": "npm install",
"test": "mocha tests --recursive",
"test-watch": "mocha tests --recursive -w",
"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"
},
"config": {
@@ -49,7 +50,6 @@
"commander": "^2.9.0",
"debug": "^2.2.0",
"ejs": "^2.5.2",
"eslint-config-postcss": "^2.0.2",
"express": "^4.14.0",
"mongoose": "^4.6.5",
"morgan": "^1.7.0",
@@ -57,10 +57,10 @@
"uuid": "^2.0.3"
},
"devDependencies": {
"autoprefixer": "^6.5.2",
"babel-core": "^6.18.2",
"babel-eslint": "^7.1.0",
"babel-jest": "^15.0.0",
"autoprefixer": "^6.5.0",
"babel-core": "^6.18.2",
"babel-loader": "^6.2.7",
"babel-plugin-transform-async-to-generator": "^6.16.0",
"babel-plugin-transform-class-properties": "^6.18.0",
@@ -77,8 +77,15 @@
"copy-webpack-plugin": "^4.0.0",
"css-loader": "^0.25.0",
"eslint": "^3.9.1",
"eslint-config-postcss": "^2.0.2",
"eslint-config-standard": "^6.2.1",
"eslint-plugin-flowtype": "^2.25.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-promise": "^3.3.1",
"eslint-plugin-react": "^6.6.0",
"eslint-plugin-standard": "^2.0.1",
"exports-loader": "^0.6.3",
"fetch-mock": "^5.5.0",
"hammerjs": "^2.0.8",
"immutable": "^3.8.1",
"imports-loader": "^0.6.5",
@@ -99,6 +106,7 @@
"react-redux": "^4.4.5",
"react-router": "^3.0.0",
"redux": "^3.6.0",
"redux-mock-store": "^1.2.1",
"redux-thunk": "^2.1.0",
"regenerator": "^0.8.46",
"style-loader": "^0.13.1",
+6 -2
View File
@@ -28,10 +28,14 @@ router.get('/', (req, res, next) => {
return Promise.all([
comments,
User.findByIdArray(comments.map((comment) => comment.author_id)),
Action.findByItemIdArray(comments.map((comment) => comment.id))
Action.getActionSummaries(comments.map((comment) => comment.id))
]);
}).then(([comments, users, actions]) => {
res.status(200).json([...comments, ...users, ...actions]);
res.json({
comments,
users,
actions
});
}).catch(error => {
next(error);
});
+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"
}
}
@@ -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 authReducer from '../../../../client/coral-framework/store/reducers/auth';
import * as actions from '../../../../client/coral-framework/store/actions/auth';
describe ('authReducer', () => {
describe('SET_LOGGED_IN_USER', () => {
@@ -2,7 +2,7 @@ import 'react';
import 'redux';
import {expect} from 'chai';
import fetchMock from 'fetch-mock';
import * as actions from '../../store/actions/items';
import * as actions from '../../../../client/coral-framework/store/actions/items';
import {Map} from 'immutable';
import configureStore from 'redux-mock-store';
@@ -11,74 +11,88 @@ const mockStore = configureStore();
describe('itemActions', () => {
let store;
const host = 'http://test.host';
beforeEach(() => {
store = mockStore(new Map({}));
fetchMock.restore();
});
describe('getItemsQuery', () => {
const query = 'all';
describe('getStream', () => {
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'}
]}
]};
const response = {
comments: [
{body: 'stuff', id: '123'},
{body: 'morestuff', id: '456'}
],
actions: [
{
type: 'like',
id: '123',
count: 1,
current_user: false
},
{
type: 'flag',
id: '456',
count: 5,
current_user: true
}
]
};
it('should get an item from a query and send the appropriate dispatches', () => {
it('should get an stream from an asset_id and send the appropriate dispatches', () => {
fetchMock.get('*', JSON.stringify(response));
return actions.getItemsQuery(query, rootId, view, host)(store.dispatch)
return actions.getStream(rootId)(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('/api/v1/stream?asset_id=1234');
expect(res).to.deep.equal(response);
expect(store.getActions()[0]).to.deep.equal({
type: actions.ADD_ITEM,
item: response.results[0].Docs[0],
item_id: '123'
item: response.comments[0],
item_type: 'comments',
id: '123'
});
expect(store.getActions()[1]).to.deep.equal({
type: actions.ADD_ITEM,
item: response.results[0].Docs[1],
item_id: '456'
item: response.comments[1],
item_type: 'comments',
id: '456'
});
});
});
it('should handle an error', () => {
fetchMock.get('*', 404);
return actions.getItemsQuery(query, rootId, view, host)(store.dispatch)
return actions.getStream(rootId)(store.dispatch)
.catch((err) => {
expect(err).to.be.truthy;
});
});
});
describe('getItemsArray', () => {
const response = {items: [{type: 'comment', item_id: '123'}, {type: 'comment', item_id: '456'}]};
//Disabling tests for this function until is is used again.
xdescribe('getItemsArray', () => {
const response = {items: [{type: 'comment', id: '123'}, {type: 'comment', 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));
return actions.getItemsArray(ids, host)(store.dispatch)
return actions.getItemsArray(ids)(store.dispatch)
.then((res) => {
expect(res).to.deep.equal(response.items);
expect(store.getActions()[0]).to.deep.equal({
type: actions.ADD_ITEM,
item: {
type: 'comment',
item_id: '123'
id: '123'
},
item_id: '123'
id: '123'
});
expect(store.getActions()[1]).to.deep.equal({
type: actions.ADD_ITEM,
item: {
type: 'comment', item_id: '456'
type: 'comment', id: '456'
},
item_id: '456'
id: '456'
});
});
});
@@ -93,37 +107,38 @@ describe('itemActions', () => {
describe('postItem', () => {
const item = {
type: 'comment',
data:{content: 'stuff'}
type: 'comments',
data: {body: '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'}});
return actions.postItem(item.data, item.type, undefined, host)(store.dispatch)
fetchMock.post('*', {id: '123'});
return actions.postItem(item.data, item.type, undefined)(store.dispatch)
.then((id) => {
expect(fetchMock.calls().matched[0][1]).to.deep.equal(
{
method: 'POST',
body: JSON.stringify({...item, version: 1})
headers: {
'Content-Type':'application/json'
},
body: JSON.stringify(item.data)
}
);
expect(id).to.equal('123');
expect(store.getActions()[0]).to.deep.equal({
type: actions.ADD_ITEM,
item: {
type: 'comment',
data: {
content: 'stuff'
},
item_id: '123'
body: 'stuff',
id: '123'
},
item_id: '123'
item_type: 'comments',
id: '123'
});
});
});
it('should handle an error', () => {
fetchMock.post('*', 404);
return actions.postItem(item, host)(store.dispatch)
return actions.postItem(item)(store.dispatch)
.catch((err) => {
expect(err).to.be.truthy;
});
@@ -132,17 +147,17 @@ describe('itemActions', () => {
describe('postAction', () => {
it ('should post an action', () => {
fetchMock.post('*', 200);
return actions.postAction('abc', 'flag', '123', host)(store.dispatch)
fetchMock.post('*', {id: '456'});
return actions.postAction('abc', 'flag', '123', 'comments')(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('/api/v1/comments/abc/actions');
expect(response).to.deep.equal({id:'456'});
});
});
it('should handle an error', () => {
fetchMock.post('*', 404);
return actions.postItem('abc', 'flag', '123', host)(store.dispatch)
return actions.postAction('abc', 'flag', '123')(store.dispatch)
.catch((err) => {
expect(err).to.be.truthy;
});
@@ -0,0 +1,95 @@
import {Map, fromJS} from 'immutable';
import {expect} from 'chai';
import itemsReducer from '../../../../client/coral-framework/store/reducers/items';
describe ('itemsReducer', () => {
describe('ADD_ITEM', () => {
it('should add an item', () => {
const action = {
type: 'ADD_ITEM',
item: {
body: 'stuff',
id: '123'
},
item_type: 'comments',
id: '123'
};
const store = new Map({});
const result = itemsReducer(store, action);
expect(result.getIn(['comments', '123']).toJS()).to.deep.equal({
body: 'stuff',
id: '123'
});
});
});
describe ('UPDATE_ITEM', () => {
it ('should update an item', () => {
const action = {
type: 'UPDATE_ITEM',
property: 'stuff',
value: 'things',
item_type: 'comments',
id: '123'
};
const store = fromJS({
'comments': {
'123': {
id: '123',
stuff: 'morestuff'
}
}
});
const result = itemsReducer(store, action);
expect(result.getIn(['comments', '123']).toJS()).to.deep.equal({
id: '123',
stuff: 'things'
});
});
});
describe('APPEND_ITEM_ARRAY', () => {
let action;
let store;
beforeEach (() => {
action = {
type: 'APPEND_ITEM_ARRAY',
property: 'stuff',
value: 'things',
id: '123',
item_type: 'comments'
};
store = fromJS({
'comments': {
'123': {
id: '123',
stuff: ['morestuff']
}
}
});
});
it ('should append to an existing array', () => {
const result = itemsReducer(store, action);
expect(result.getIn(['comments', '123']).toJS()).to.deep.equal({
id: '123',
stuff: ['morestuff', 'things']
});
});
it ('should create a new array', () => {
store = fromJS({
'comments': {
'123': {
id: '123'
}
}
});
const result = itemsReducer(store, action);
expect(result.getIn(['comments', '123']).toJS()).to.deep.equal({
id: '123',
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 notificationReducer from '../../../../client/coral-framework/store/reducers/notification';
import * as actions from '../../../../client/coral-framework/store/actions/notification';
describe ('notificationsReducer', () => {
describe('ADD_NOTIFICATION', () => {
+35 -3
View File
@@ -8,13 +8,20 @@ describe('Action: models', () => {
beforeEach(() => {
return Action.create([{
action_type: 'flag',
item_id: '123'
item_id: '123',
item_type: 'comments'
}, {
action_type: 'like',
item_id: '789'
item_id: '789',
item_type: 'comments'
}, {
action_type: 'flag',
item_id: '456'
item_id: '456',
item_type: 'comments'
}, {
action_type: 'flag',
item_id: '123',
item_type: 'comments'
}]).then((actions) => {
mockActions = actions;
});
@@ -32,7 +39,32 @@ describe('Action: models', () => {
describe('#findByItemIdArray()', () => {
it('should find an array of actions from an array of item_ids', () => {
return Action.findByItemIdArray(['123', '456']).then((result) => {
expect(result).to.have.length(3);
});
});
});
describe('#getActionSummaries()', () => {
it('should return properly formatted summaries from an array of item_ids', () => {
return Action.getActionSummaries(['123', '789']).then((result) => {
expect(result).to.have.length(2);
const sorted = result.sort((a, b) => a.count - b.count);
delete sorted[0].id;
delete sorted[1].id;
expect(sorted[0]).to.deep.equal({
action_type: 'like',
count: 1,
item_id: '789',
item_type: 'comments',
current_user: false
});
expect(sorted[1]).to.deep.equal({
action_type: 'flag',
count: 2,
item_id: '123',
item_type: 'comments',
current_user: false
});
});
});
});
+5 -3
View File
@@ -22,14 +22,14 @@ describe('api/stream: routes', () => {
id: 'abc',
body: 'comment 10',
asset_id: 'asset',
author_id: '123',
author_id: '',
parent_id: '',
status: 'accepted'
}, {
id: 'def',
body: 'comment 20',
asset_id: 'asset',
author_id: '456',
author_id: '',
parent_id: '',
status: ''
}, {
@@ -89,7 +89,9 @@ describe('api/stream: routes', () => {
.query({'asset_id': 'asset'})
.then(res => {
expect(res).to.have.status(200);
expect(res.body.length).to.equal(3);
expect(res.body.comments.length).to.equal(1);
expect(res.body.users.length).to.equal(1);
expect(res.body.actions.length).to.equal(1);
});
});
});