mirror of
https://github.com/wassname/talk.git
synced 2026-07-09 03:46:23 +08:00
Merge pull request #4 from coralproject/add-coral-embed-stream
Adding coral embed stream
This commit is contained in:
@@ -6,7 +6,11 @@
|
||||
],
|
||||
"plugins": [
|
||||
["transform-decorators-legacy"],
|
||||
["transform-react-jsx", { "pragma": "h" }],
|
||||
["transform-object-assign"]
|
||||
["transform-react-jsx"],
|
||||
["transform-object-assign"],
|
||||
["transform-class-properties"],
|
||||
["transform-async-to-generator"],
|
||||
["transform-object-rest-spread"],
|
||||
["transform-class-properties"]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
var path = require('path')
|
||||
var express = require('express')
|
||||
var http = require('http')
|
||||
var webpack = require('webpack')
|
||||
var config = require('./webpack.config.dev')
|
||||
var Dashboard = require('webpack-dashboard')
|
||||
var DashboardPlugin = require('webpack-dashboard/plugin')
|
||||
|
||||
var app = express()
|
||||
var server = http.Server(app)
|
||||
|
||||
var compiler = webpack(config)
|
||||
var dashboard = new Dashboard()
|
||||
compiler.apply(new DashboardPlugin(dashboard.setData))
|
||||
|
||||
app.use(express.static('public'))
|
||||
|
||||
app.use(require('webpack-dev-middleware')(compiler, {
|
||||
noInfo: true,
|
||||
quiet: true,
|
||||
publicPath: config.output.publicPath
|
||||
}))
|
||||
|
||||
app.use(require('webpack-hot-middleware')(compiler, {log: () => {}}))
|
||||
|
||||
app.get('/default.css', function (req, res) {
|
||||
res.sendFile(path.join(__dirname, '/style/default.css'))
|
||||
})
|
||||
|
||||
app.get('*', function (req, res) {
|
||||
res.sendFile(path.join(__dirname, 'index.html'))
|
||||
})
|
||||
|
||||
server.listen(6182, 'localhost', function (err) {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Listening at http://localhost:6182')
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="default.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Lato|Open+Sans" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="coralStream"/>
|
||||
<script src="./bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,180 @@
|
||||
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 {ReplyBox, ReplyButton} from '../../coral-plugin-replies'
|
||||
import Pym from 'pym.js'
|
||||
|
||||
const {addItem, updateItem, postItem, getItemsQuery, postAction, appendItemRelated} = 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))
|
||||
},
|
||||
getItemsQuery: (rootId) => {
|
||||
return dispatch(getItemsQuery(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))
|
||||
},
|
||||
appendItemRelated: (item, property, value) => {
|
||||
return dispatch(appendItemRelated(item, property, value))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
class CommentStream extends Component {
|
||||
|
||||
static propTypes = {
|
||||
items: PropTypes.object.isRequired,
|
||||
addItem: PropTypes.func.isRequired,
|
||||
updateItem: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
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.getItemsQuery('assetTest')
|
||||
}
|
||||
|
||||
render () {
|
||||
if (Object.keys(this.props.items).length === 0) {
|
||||
// Loading mock asset
|
||||
this.props.postItem({
|
||||
comments: [],
|
||||
url: 'http://coralproject.net'
|
||||
}, 'asset', 'assetTest')
|
||||
|
||||
// Loading mock user
|
||||
this.props.postItem({name: 'Ban Ki-Moon'}, 'user', 'user_8989')
|
||||
.then((id) => {
|
||||
this.props.setLoggedInUser(id)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// TODO: Replace teststream id with id from params
|
||||
|
||||
|
||||
|
||||
const rootItemId = 'assetTest'
|
||||
const rootItem = this.props.items[rootItemId]
|
||||
console.log(this.props.items);
|
||||
return <div>
|
||||
{
|
||||
rootItem ?
|
||||
<div>
|
||||
<div id="commentBox">
|
||||
<Count
|
||||
item_id={rootItemId}
|
||||
items={this.props.items}/>
|
||||
<CommentBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemRelated={this.props.appendItemRelated}
|
||||
updateItem={this.props.updateItem}
|
||||
item_id={rootItemId}/>
|
||||
</div>
|
||||
{
|
||||
rootItem.related.comment.map((commentId) => {
|
||||
const comment = this.props.items[commentId]
|
||||
return <div className="comment">
|
||||
<hr aria-hidden={true}/>
|
||||
<PubDate created_at={comment.created_at}/>
|
||||
<Content content={comment.data.content}/>
|
||||
<div className="commentActions">
|
||||
<Flag
|
||||
addNotification={this.props.addNotification}
|
||||
item_id={commentId}
|
||||
flag={comment.flag}
|
||||
postAction={this.props.postAction}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
item_id={commentId}/>
|
||||
</div>
|
||||
<ReplyBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemRelated={this.props.appendItemRelated}
|
||||
updateItem={this.props.updateItem}
|
||||
item_id={commentId}
|
||||
showReply={comment.showReply}/>
|
||||
{
|
||||
comment.related &&
|
||||
comment.related.child &&
|
||||
comment.related.child.map((replyId) => {
|
||||
let reply = this.props.items[replyId]
|
||||
return <div className="reply">
|
||||
<hr aria-hidden={true}/>
|
||||
<PubDate created_at={reply.created_at}/>
|
||||
<Content content={reply.data.content}/>
|
||||
<div className="replyActions">
|
||||
<Flag
|
||||
addNotificiation={this.props.addNotification}
|
||||
item_id={replyId}
|
||||
flag={reply.flag}
|
||||
postAction={this.props.postAction}
|
||||
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>
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default CommentStream
|
||||
@@ -0,0 +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'
|
||||
|
||||
store.dispatch(fetchConfig())
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<CommentStream />
|
||||
</Provider>
|
||||
, document.querySelector('#coralStream'))
|
||||
@@ -0,0 +1,113 @@
|
||||
body {
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 5px 10px;
|
||||
margin: 5px;
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-radius: 2px;
|
||||
color: #FFF;
|
||||
background-color: rgb(155, 155, 155);
|
||||
}
|
||||
|
||||
button i {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
height: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.screen-reader-text {
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
overflow: hidden;
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
/* Notification styles */
|
||||
#coral-notif {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
border: 0;
|
||||
background: rgb(105,105,105);
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Comment Box Styles */
|
||||
.coral-plugin-commentbox-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.coral-plugin-commentbox-textarea {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.coral-plugin-commentbox-button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#coralStream .coral-plugin-commentbox-button {
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
padding: 5px 10px;
|
||||
background: rgb(105, 105, 105);
|
||||
color: #FFF;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Comment styles */
|
||||
.comment {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.coral-plugin-commentcontent-text {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* Reply styles */
|
||||
|
||||
.comment .reply {
|
||||
margin: 0px 0px 10px 20px;
|
||||
}
|
||||
|
||||
/* Comment Action Styles */
|
||||
|
||||
.commentActions, .replyActions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.commentActions .material-icons, .replyActions .material-icons {
|
||||
font-size: 12px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Comment count styles */
|
||||
.coral-plugin-comment-count-text {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.coral-plugin-pubdate-text {
|
||||
color: #CCC;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
var path = require('path')
|
||||
var webpack = require('webpack')
|
||||
|
||||
module.exports = {
|
||||
devtool: 'eval',
|
||||
entry: [
|
||||
'babel-polyfill',
|
||||
'webpack-hot-middleware/client',
|
||||
path.join(__dirname, 'src', 'app')
|
||||
],
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
filename: 'bundle.js',
|
||||
publicPath: '/'
|
||||
},
|
||||
resolve: {
|
||||
root: [
|
||||
path.resolve(__dirname, 'src')
|
||||
],
|
||||
extensions: ['', '.js', '.jsx']
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
'NODE_ENV': JSON.stringify('development')
|
||||
}
|
||||
}),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.ProvidePlugin({
|
||||
'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
|
||||
}),
|
||||
new webpack.NoErrorsPlugin()
|
||||
],
|
||||
module: {
|
||||
loaders: [{
|
||||
test: /\.(js|jsx)$/,
|
||||
|
||||
loaders: ['babel'],
|
||||
exclude: /node_modules/,
|
||||
include: path.join(__dirname, '../')
|
||||
}, {
|
||||
test: /\.css$/,
|
||||
loader: 'style-loader!css-loader'
|
||||
}, {
|
||||
test: /\.png$/,
|
||||
loader: 'url-loader?limit=100000'
|
||||
}, {
|
||||
test: /\.(jpg|png|gif|svg)$/,
|
||||
loader: 'file-loader'
|
||||
}, {
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader'
|
||||
}, {
|
||||
test: /\.woff$/,
|
||||
loader: 'url?limit=100000'
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const Copy = require('copy-webpack-plugin')
|
||||
|
||||
//Keeping this file for reference, it should move to a global webpack.
|
||||
|
||||
module.exports = {
|
||||
devtool: 'source-map',
|
||||
entry: [
|
||||
'babel-polyfill',
|
||||
'./src/app'
|
||||
],
|
||||
output: {
|
||||
path: path.join(__dirname, '..', '..','dist', 'coral-embed-stream'),
|
||||
filename: 'bundle.js',
|
||||
publicPath: '/dist/'
|
||||
},
|
||||
resolve: {
|
||||
root: [
|
||||
path.resolve(__dirname, 'src')
|
||||
],
|
||||
extensions: ['', '.js', '.jsx']
|
||||
},
|
||||
plugins: [
|
||||
new Copy([{
|
||||
from: './index.html'
|
||||
},
|
||||
{
|
||||
from: './style/default.css'
|
||||
},
|
||||
{
|
||||
from: './public/',
|
||||
to: './'
|
||||
},
|
||||
{
|
||||
from: path.resolve(__dirname, '..', 'coral-framework', 'i18n', 'translations'),
|
||||
to: './translations'
|
||||
}]),
|
||||
new webpack.optimize.OccurenceOrderPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
'NODE_ENV': JSON.stringify('production')
|
||||
}
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
|
||||
}),
|
||||
new webpack.ExtendedAPIPlugin()
|
||||
],
|
||||
module: {
|
||||
loaders: [{
|
||||
test: /\.(js|jsx)$/,
|
||||
loaders: ['babel'],
|
||||
exclude: /node_modules/,
|
||||
include: path.join(__dirname, '../')
|
||||
}, {
|
||||
test: /\.css$/,
|
||||
loader: 'style-loader!css-loader'
|
||||
}, {
|
||||
test: /\.png$/,
|
||||
loader: 'url-loader?limit=100000'
|
||||
}, {
|
||||
test: /\.jpg$/,
|
||||
loader: 'file-loader'
|
||||
}, {
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader'
|
||||
}, {
|
||||
test: /\.woff$/,
|
||||
loader: 'url?limit=100000'
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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', () => {
|
||||
it('should set a logged in user', () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,152 @@
|
||||
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'
|
||||
|
||||
const mockStore = configureStore()
|
||||
|
||||
describe('itemActions', () => {
|
||||
let store
|
||||
const host = 'http://test.host'
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(new Map({}))
|
||||
fetchMock.restore()
|
||||
})
|
||||
|
||||
describe('getItemsQuery', () => {
|
||||
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))
|
||||
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(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)
|
||||
return actions.getItemsQuery(query, rootId, view, host)(store.dispatch)
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getItemsArray', () => {
|
||||
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))
|
||||
return actions.getItemsArray(ids, host)(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'
|
||||
},
|
||||
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)
|
||||
return actions.getItemsArray(ids, host)(store.dispatch)
|
||||
.catch((err) => {
|
||||
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'}})
|
||||
return actions.postItem(item.data, item.type, undefined, host)(store.dispatch)
|
||||
.then((id) => {
|
||||
expect(fetchMock.calls().matched[0][1]).to.deep.equal(
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({...item, version: 1})
|
||||
}
|
||||
)
|
||||
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'
|
||||
},
|
||||
item_id: '123'
|
||||
})
|
||||
})
|
||||
})
|
||||
it('should handle an error', () => {
|
||||
fetchMock.post('*', 404)
|
||||
return actions.postItem(item, host)(store.dispatch)
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('postAction', () => {
|
||||
it ('should post an action', () => {
|
||||
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('')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle an error', () => {
|
||||
fetchMock.post('*', 404)
|
||||
return actions.postItem('abc', 'flag', '123', host)(store.dispatch)
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,149 @@
|
||||
import { Map, fromJS } from 'immutable'
|
||||
import {expect} from 'chai'
|
||||
import itemsReducer from '../../store/reducers/items'
|
||||
import * as actions from '../../store/actions/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']
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
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', () => {
|
||||
it('should add a notification', () => {
|
||||
const action = {
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
import timeago from 'timeago.js'
|
||||
import esTA from 'timeago.js/locales/es'
|
||||
|
||||
/**
|
||||
* Default locales, this should be overriden by config file
|
||||
*/
|
||||
|
||||
class i18n {
|
||||
constructor (translations) {
|
||||
/**
|
||||
* Register locales
|
||||
*/
|
||||
|
||||
this.locales = {'en': 'en', 'es': 'es'}
|
||||
timeago.register('es_ES', esTA)
|
||||
this.timeagoInstance = new timeago()
|
||||
/**
|
||||
* Load translations
|
||||
*/
|
||||
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]
|
||||
} catch (err) {
|
||||
this.translations = trans['en']
|
||||
}
|
||||
|
||||
this.setLocale = (locale) => {
|
||||
try {
|
||||
localStorage.setItem('locale', locale)
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
this.getLocale = () => (
|
||||
localStorage.getItem('locale') || navigator.locale || 'en-US'
|
||||
)
|
||||
|
||||
/**
|
||||
* Expose the translation function
|
||||
*
|
||||
* it takes a string with the translation key and returns
|
||||
* the translation value or the key itself if not found
|
||||
* it works with nested translations (my.page.title)
|
||||
*/
|
||||
|
||||
this.t = (key) => {
|
||||
const arr = key.split('.')
|
||||
let translation = this.translations
|
||||
try {
|
||||
for (var i = 0; i < arr.length; i++) translation = translation[arr[i]]
|
||||
} catch (error) {
|
||||
console.warn(`${key} language key not set`)
|
||||
return key
|
||||
}
|
||||
|
||||
const val = String(translation)
|
||||
if (val) {
|
||||
return val
|
||||
} else {
|
||||
console.warn(`${key} language key not set`)
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
this.timeago = (time) => {
|
||||
return this.timeagoInstance.format(time)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default i18n
|
||||
@@ -0,0 +1,17 @@
|
||||
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,
|
||||
store,
|
||||
fetchConfig,
|
||||
itemActions,
|
||||
I18n,
|
||||
notificationActions,
|
||||
authActions
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"query": [
|
||||
{
|
||||
"item_id": "assetTest",
|
||||
"type": "asset",
|
||||
"data": {
|
||||
"url": "http://coralproject.net"
|
||||
},
|
||||
"related": {
|
||||
"comment": [
|
||||
"abc",
|
||||
"def"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"item_id": "abc",
|
||||
"type": "comment",
|
||||
"data": {
|
||||
"content": "Sample comment"
|
||||
},
|
||||
"related":{
|
||||
"child": [
|
||||
"xyz"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"item_id": "xyz",
|
||||
"type": "comment",
|
||||
"data": {
|
||||
"content": "Sample reply"
|
||||
}
|
||||
},
|
||||
{
|
||||
"item_id": "def",
|
||||
"type": "comment",
|
||||
"data": {
|
||||
"content": "Another reply"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
|
||||
const Notification = (props) => {
|
||||
if (props.notification.text) {
|
||||
setTimeout(() => {
|
||||
props.clearNotification()
|
||||
}, props.notifLength)
|
||||
}
|
||||
return <div>
|
||||
{
|
||||
props.notification.text &&
|
||||
<dialog open id='coral-notif' className={'coral-notif-' + props.notification.type}>
|
||||
{props.notification.text}
|
||||
</dialog>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default Notification
|
||||
@@ -0,0 +1,17 @@
|
||||
/* Auth Actions */
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/* @flow */
|
||||
|
||||
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'
|
||||
|
||||
/**
|
||||
* Action creators
|
||||
*/
|
||||
|
||||
export const fetchConfig = () => async (dispatch) => {
|
||||
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({}) })
|
||||
} catch (error) {
|
||||
dispatch({ type: FETCH_CONFIG_FAILED })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/* 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 APPEND_ITEM_RELATED = 'APPEND_ITEM_RELATED'
|
||||
|
||||
/**
|
||||
* Action creators
|
||||
*/
|
||||
|
||||
/*
|
||||
* Adds an item to the local store without posting it to the server
|
||||
* Useful for optimistic posting, etc.
|
||||
*
|
||||
* @params
|
||||
* item - the item to be posted
|
||||
*
|
||||
*/
|
||||
|
||||
export const addItem = (item) => {
|
||||
if (!item.item_id) {
|
||||
console.warn('addItem called without an item id.')
|
||||
}
|
||||
return {
|
||||
type: ADD_ITEM,
|
||||
item: item,
|
||||
item_id: item.item_id
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Updates an item in the local store without posting it to the server
|
||||
* Useful for item-level toggles, etc.
|
||||
*
|
||||
* @params
|
||||
* item_id - the id of the item to be posted
|
||||
* property - the property to be updated
|
||||
* value - the value that the property should be set to
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
export const updateItem = (item_id, property, value) => {
|
||||
return {
|
||||
type: UPDATE_ITEM,
|
||||
item_id,
|
||||
property,
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
export const appendItemArray = (item_id, property, value) => {
|
||||
return {
|
||||
type: APPEND_ITEM_ARRAY,
|
||||
item_id,
|
||||
property,
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const appendItemRelated = (item_id, property, value) => {
|
||||
return {
|
||||
type: APPEND_ITEM_RELATED,
|
||||
item_id,
|
||||
property,
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Get Items from Query
|
||||
* Gets a set of items from a predefined query
|
||||
*
|
||||
* @params
|
||||
* Query - a predefiend query for retreiving items
|
||||
*
|
||||
* @returns
|
||||
* A promise resolving to a set of items
|
||||
*
|
||||
* @dispatches
|
||||
* A set of items to the item store
|
||||
*/
|
||||
export function getItemsQuery (rootId) {
|
||||
return (dispatch) => {
|
||||
// return fetch('/v1/exec/view/' + rootId)
|
||||
// .then(
|
||||
// response => {
|
||||
// return response.ok ? response.json() : Promise.reject(response.status + ' ' + response.statusText)
|
||||
// }
|
||||
// )
|
||||
// .then((json) => {
|
||||
// let items = json.results[0].Docs
|
||||
// for (var i = 0; i < items.length; i++) {
|
||||
// dispatch(addItem(items[i]))
|
||||
// }
|
||||
// return (items)
|
||||
// })
|
||||
console.log('Loading mock data', mocks);
|
||||
let mockData = mocks.query
|
||||
for (var i=0; i < mockData.length; i++ ) {
|
||||
dispatch(addItem(mockData[i]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Get Items Array
|
||||
* Gets a set of items from an array of item ids
|
||||
*
|
||||
* @params
|
||||
* Query - a predefiend query for retreiving items
|
||||
*
|
||||
* @returns
|
||||
* A promise resolving to a set of items
|
||||
*
|
||||
* @dispatches
|
||||
* A set of items to the item store
|
||||
*/
|
||||
|
||||
export function getItemsArray (ids) {
|
||||
return (dispatch) => {
|
||||
return fetch('/v1/item/' + ids)
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.json()
|
||||
: Promise.reject(response.status + ' ' + response.statusText)
|
||||
}
|
||||
)
|
||||
.then((json) => {
|
||||
for (var i = 0; i < json.items.length; i++) {
|
||||
dispatch(addItem(json.items[i]))
|
||||
}
|
||||
return json.items
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* PutItem
|
||||
* Puts an item
|
||||
*
|
||||
* @params
|
||||
* Item - the item to be put
|
||||
*
|
||||
* @returns
|
||||
* A promise resolving to an item is
|
||||
*
|
||||
* @dispatches
|
||||
* The newly put item to the item store
|
||||
*/
|
||||
|
||||
export function postItem (data, type, id) {
|
||||
return (dispatch) => {
|
||||
let item = {
|
||||
type,
|
||||
data,
|
||||
version: 1
|
||||
}
|
||||
if (id) {
|
||||
item.item_id = id
|
||||
}
|
||||
let options = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(item)
|
||||
}
|
||||
return fetch('/v1/item', options)
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.json()
|
||||
: Promise.reject(response.status + ' ' + response.statusText)
|
||||
}
|
||||
)
|
||||
.then((json) => {
|
||||
// Patch until ID is returned from backend
|
||||
dispatch(addItem(json))
|
||||
return json.item_id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//http://localhost:16180/v1/action/flag/user/user_89654/on/item/87e418c5-aafb-4eb7-9ce4-78f28793782a
|
||||
|
||||
/*
|
||||
* PostAction
|
||||
* Posts an action to an item
|
||||
*
|
||||
* @params
|
||||
* item_id - the id of the item on which the action is taking place
|
||||
* action - the name of the action
|
||||
* user - the user performing the action
|
||||
* host - the coral host
|
||||
*
|
||||
* @returns
|
||||
* A promise resolving to null or an error
|
||||
*
|
||||
*/
|
||||
|
||||
export function postAction (item, action, user) {
|
||||
return (dispatch) => {
|
||||
let options = {
|
||||
method: 'POST'
|
||||
}
|
||||
dispatch(appendItemArray(item, action, user))
|
||||
return fetch('/v1/action/' + action + '/user/' + user + '/on/item/' + item, options)
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.text()
|
||||
: Promise.reject(response.status + ' ' + response.statusText)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/* Notification Actions */
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/* Auth */
|
||||
|
||||
import * as actions from '../actions/auth'
|
||||
import { fromJS } from 'immutable'
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/* @flow */
|
||||
|
||||
import { Map, fromJS } 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_FAILED:
|
||||
return state.set('loading', false)
|
||||
|
||||
// Override config if worked
|
||||
case actions.FETCH_CONFIG_SUCCESS:
|
||||
return action.config.set('loading', false)
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/* @flow */
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
export default combineReducers({
|
||||
config,
|
||||
items,
|
||||
notification,
|
||||
auth
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
/* Items Reducer */
|
||||
|
||||
import { Map, fromJS } from 'immutable'
|
||||
import * as actions from '../actions/items'
|
||||
|
||||
const initialState = fromJS({})
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case actions.ADD_ITEM:
|
||||
return state.set(action.item_id, fromJS(action.item))
|
||||
case actions.UPDATE_ITEM:
|
||||
return state.updateIn([action.item_id, 'data', action.property], () =>
|
||||
fromJS(action.value)
|
||||
)
|
||||
case actions.APPEND_ITEM_ARRAY:
|
||||
return state.updateIn([action.item_id, 'data', action.property], (prop) => {
|
||||
return prop ? prop.push(action.value) : fromJS([action.value])
|
||||
}
|
||||
)
|
||||
case actions.APPEND_ITEM_RELATED:
|
||||
return state.updateIn([action.item_id, 'related', action.property], (prop) => {
|
||||
return prop ? prop.push(action.value) : fromJS([action.value])
|
||||
}
|
||||
)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/* Items Notifications */
|
||||
|
||||
import * as actions from '../actions/notification'
|
||||
import { fromJS } from 'immutable'
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
import { createStore, applyMiddleware } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
import mainReducer from './reducers'
|
||||
|
||||
export default createStore(
|
||||
mainReducer,
|
||||
window.devToolsExtension && window.devToolsExtension(),
|
||||
applyMiddleware(thunk)
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
|
||||
const name = 'coral-plugin-comment-count'
|
||||
|
||||
const CommentCount = ({items, item_id}) => {
|
||||
let count = 0
|
||||
if (items[item_id]) {
|
||||
count += items[item_id].related.comment.length
|
||||
}
|
||||
const itemKeys = Object.keys(items)
|
||||
for (var i=0; i < itemKeys.length; i++) {
|
||||
const item = items[itemKeys[i]]
|
||||
if (item.type === 'comment' && item.related && item.related.child) {
|
||||
count += item.related.child.length
|
||||
}
|
||||
}
|
||||
|
||||
return <div className={name + '-text'}>
|
||||
{count + ' ' + (count === 1 ? 'Comment':'Comments')}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default CommentCount
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import {I18n} from '../coral-framework'
|
||||
|
||||
const name='coral-plugin-commentbox'
|
||||
|
||||
class CommentBox extends Component {
|
||||
|
||||
static propTypes = {
|
||||
postItem: PropTypes.func,
|
||||
updateItem: PropTypes.func,
|
||||
item_id: PropTypes.string,
|
||||
comments: PropTypes.array,
|
||||
reply: PropTypes.bool
|
||||
}
|
||||
|
||||
state = {
|
||||
content: ''
|
||||
}
|
||||
|
||||
postComment = () => {
|
||||
const {postItem, updateItem, item_id, reply, addNotification, appendItemRelated} = this.props
|
||||
let comment = {
|
||||
content: this.state.content
|
||||
}
|
||||
let related
|
||||
if (reply) {
|
||||
comment.parent_id = item_id
|
||||
related = 'child'
|
||||
} else {
|
||||
comment.asset_id = item_id
|
||||
related = 'comment'
|
||||
}
|
||||
updateItem(item_id, 'showReply', false)
|
||||
postItem(comment, 'comment')
|
||||
.then((id) => {
|
||||
appendItemRelated(item_id, related, id)
|
||||
addNotification('success', 'Your comment has been posted.')
|
||||
}).catch((err) => console.error(err))
|
||||
this.setState({content: ''})
|
||||
}
|
||||
|
||||
render () {
|
||||
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'}
|
||||
style={styles && styles.container}>
|
||||
<label
|
||||
htmlFor={ reply ? 'replyText' : 'commentText'}
|
||||
className="screen-reader-text"
|
||||
aria-hidden={true}>
|
||||
{reply ? 'Reply': 'Comment'}
|
||||
</label>
|
||||
<textarea
|
||||
className={name + '-textarea'}
|
||||
style={styles && styles.textarea}
|
||||
value={this.state.content}
|
||||
id={reply ? 'replyText' : 'commentText'}
|
||||
onChange={(e) => this.setState({content: e.target.value})}
|
||||
rows={3}/>
|
||||
</div>
|
||||
<div className={name + '-button-container'}>
|
||||
<button
|
||||
className={name + '-button'}
|
||||
style={styles && styles.button}
|
||||
onClick={this.postComment}>
|
||||
{lang.t('post')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default CommentBox
|
||||
|
||||
const lang = new I18n({
|
||||
en: {
|
||||
post: 'Post'
|
||||
},
|
||||
es: {
|
||||
post: 'Publicar'
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import {shallow, mount} from 'enzyme'
|
||||
import {expect} from 'chai'
|
||||
import CommentBox from '../CommentBox'
|
||||
|
||||
describe('CommentBox', () => {
|
||||
let comment
|
||||
let render
|
||||
beforeEach(() => {
|
||||
comment = {}
|
||||
const postItem = (item) => {
|
||||
comment.posted=item
|
||||
return Promise.resolve(4)
|
||||
}
|
||||
render = shallow(<CommentBox
|
||||
postItem={postItem}
|
||||
updateItem={(e) => comment.text=e.target.value}
|
||||
item_id={'1'}
|
||||
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
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
const name = 'coral-plugin-replies'
|
||||
|
||||
const Content = (props) => <div
|
||||
className={name + '-text'}
|
||||
style={props.styles && props.styles.text}>
|
||||
{props.content}
|
||||
</div>
|
||||
|
||||
export default Content
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import {shallow, mount} 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
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
|
||||
const name='coral-plugin-flags'
|
||||
|
||||
const FlagButton = ({flag, item_id, postAction, currentUser, addNotification}) => {
|
||||
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.')
|
||||
|
||||
}
|
||||
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>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default FlagButton
|
||||
|
||||
const styles = {
|
||||
flaggedIcon: {
|
||||
color: '#F00'
|
||||
},
|
||||
unflaggedIcon: {
|
||||
color: 'inherit'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import {I18n} from '../coral-framework'
|
||||
|
||||
const lang = new I18n()
|
||||
const name = 'coral-plugin-pubdate'
|
||||
|
||||
const PubDate = ({created_at}) => <div className={name + '-text'}>
|
||||
{lang.timeago(created_at)}
|
||||
</div>
|
||||
|
||||
export default PubDate
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import CommentBox from '../coral-plugin-commentbox/CommentBox'
|
||||
|
||||
const name = 'coral-plugin-replies'
|
||||
|
||||
const ReplyBox = (props) => <div
|
||||
className={name + '-textarea'}
|
||||
style={props.styles && props.styles.container}>
|
||||
{
|
||||
props.showReply && <CommentBox
|
||||
item_id = {props.item_id}
|
||||
postItem = {props.postItem}
|
||||
addNotification = {props.addNotification}
|
||||
appendItemRelated = {props.appendItemRelated}
|
||||
updateItem = {props.updateItem}
|
||||
comments = {props.child}
|
||||
reply = {true}/>
|
||||
}
|
||||
</div>
|
||||
|
||||
export default ReplyBox
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import {I18n} from '../coral-framework'
|
||||
|
||||
const name = 'coral-plugin-replies'
|
||||
|
||||
const ReplyButton = (props) => <button
|
||||
className={name + '-reply-button'}
|
||||
onClick={(e) => props.updateItem(props.item_id || props.parent_id, 'showReply', true)}>
|
||||
<i className={name + '-icon material-icons'}
|
||||
aria-hidden={true}>reply</i>
|
||||
{lang.t('reply')}
|
||||
</button>
|
||||
|
||||
export default ReplyButton
|
||||
|
||||
const lang = new I18n({
|
||||
en: {
|
||||
'reply': 'Reply'
|
||||
},
|
||||
es: {
|
||||
'reply': '¡traduceme!'
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import ReplyBox from './ReplyBox'
|
||||
import ReplyButton from './ReplyButton'
|
||||
|
||||
export {
|
||||
ReplyBox,
|
||||
ReplyButton
|
||||
}
|
||||
+34
-1
@@ -5,7 +5,8 @@
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"start": "./bin/www"
|
||||
"start": "./bin/www",
|
||||
"embed-start": "node client/coral-embed-stream/dev-server.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -27,5 +28,37 @@
|
||||
"debug": "^2.2.0",
|
||||
"express": "^4.14.0",
|
||||
"mongoose": "^4.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "6.14.0",
|
||||
"babel-jest": "^15.0.0",
|
||||
"babel-loader": "6.2.5",
|
||||
"babel-plugin-transform-async-to-generator": "^6.8.0",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-object-assign": "^6.8.0",
|
||||
"babel-plugin-transform-react-jsx": "^6.8.0",
|
||||
"babel-polyfill": "^6.13.0",
|
||||
"babel-preset-es2015": "6.13.0",
|
||||
"babel-preset-es2015-minimal": "^2.1.0",
|
||||
"babel-preset-stage-0": "^6.16.0",
|
||||
"copy-webpack-plugin": "^3.0.1",
|
||||
"exports-loader": "^0.6.3",
|
||||
"immutable": "^3.8.1",
|
||||
"imports-loader": "^0.6.5",
|
||||
"json-loader": "^0.5.4",
|
||||
"pym.js": "^1.1.1",
|
||||
"react": "15.3.2",
|
||||
"react-dom": "15.3.2",
|
||||
"react-redux": "^4.4.5",
|
||||
"redux": "^3.6.0",
|
||||
"redux-thunk": "^2.1.0",
|
||||
"regenerator": "^0.8.46",
|
||||
"timeago.js": "^2.0.3",
|
||||
"webpack": "^1.13.2",
|
||||
"webpack-dashboard": "^0.2.0",
|
||||
"webpack-dev-middleware": "^1.8.3",
|
||||
"webpack-hot-middleware": "^2.12.2",
|
||||
"webpack-module-hot-accept": "^1.0.4",
|
||||
"whatwg-fetch": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user