mirror of
https://github.com/wassname/talk.git
synced 2026-07-17 11:33:39 +08:00
Merge branch 'master' into font-size-docs
This commit is contained in:
@@ -1,16 +1,15 @@
|
||||
{
|
||||
"sourceMaps": true,
|
||||
"presets": [
|
||||
"stage-0",
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
["transform-decorators-legacy"],
|
||||
["transform-react-jsx"],
|
||||
["transform-object-assign"],
|
||||
["transform-class-properties"],
|
||||
["transform-async-to-generator"],
|
||||
["transform-object-rest-spread"],
|
||||
["transform-class-properties"]
|
||||
"add-module-exports",
|
||||
"transform-async-to-generator",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,62 +1,49 @@
|
||||
# Talk [](https://circleci.com/gh/coralproject/talk)
|
||||
A commenting platform from The Coral Project. [https://coralproject.net](https://coralproject.net)
|
||||
|
||||
A commenting platform from [The Coral Project](https://coralproject.net).
|
||||
|
||||
## Contributing to Talk
|
||||
|
||||
### Product Roadmap
|
||||
You can view what the Coral Team is working on next here: https://www.pivotaltracker.com/n/projects/1863625
|
||||
|
||||
You can view product ideas and our longer term roadmap here: https://trello.com/b/ILND751a/talk
|
||||
You can view what the Coral Team is working on next here https://www.pivotaltracker.com/n/projects/1863625.
|
||||
|
||||
### Local Dependencies
|
||||
Node
|
||||
You can view product ideas and our longer term roadmap here https://trello.com/b/ILND751a/talk.
|
||||
|
||||
Mongo
|
||||
|
||||
### Getting Started
|
||||
`npm install`
|
||||
Run it once to install the dependencies.
|
||||
|
||||
`npm start`
|
||||
Runs Talk.
|
||||
## Usage
|
||||
|
||||
### Configuration
|
||||
|
||||
The Talk application requires specific configuration options to be available
|
||||
inside the environment in order to run, those variables are listed here:
|
||||
|
||||
- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to
|
||||
- `TALK_MONGO_URL` (*required*) - the database connection string for the MongoDB database.
|
||||
- `TALK_REDIS_URL` (*required*) - the database connection string for the Redis database.
|
||||
- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to
|
||||
secure cookies.
|
||||
- `TALK_FACEBOOK_APP_ID` (*required*) - the Facebook app id for your Facebook
|
||||
Login enabled app.
|
||||
- `TALK_FACEBOOK_APP_SECRET` (*required*) - the Facebook app secret for your
|
||||
Facebook Login enabled app.
|
||||
- `TALK_ROOT_URL` (*required*) - root url of the installed application externally
|
||||
- `TALK_ROOT_URL` (*required*) - root url of the installed application externally
|
||||
available in the format: `<scheme>://<host>` without the path.
|
||||
- `TALK_SMTP_PROVIDER` (*required*) - SMTP provider name.
|
||||
- `TALK_SMTP_EMAIL` (*required*) - the address to send emails from using the
|
||||
SMTP provider.
|
||||
- `TALK_SMTP_USERNAME` (*required*) - username of the SMTP provider you are using.
|
||||
- `TALK_SMTP_PASSWORD` (*required*) - password for the SMTP provider you are using.
|
||||
- `TALK_SMTP_HOST` (*required*) - SMTP host url with format `smtp.domain.com`.
|
||||
- `TALK_SMTP_PORT` (*required*) - SMTP port.
|
||||
|
||||
### Running with Docker
|
||||
Make sure you have Docker running first and then run `docker-compose up -d`
|
||||
|
||||
### Testing
|
||||
`npm test`
|
||||
|
||||
### Lint
|
||||
`npm run lint`
|
||||
|
||||
### Helpful URLs
|
||||
Comment stream: http://localhost:3000/
|
||||
|
||||
Comment stream embedded on sample article: http://localhost:3000/assets/samplearticle.html
|
||||
|
||||
Moderator view: http://localhost:3000/admin
|
||||
|
||||
### Docs
|
||||
`swagger.yaml`
|
||||
|
||||
### License
|
||||
**Apache-2.0**
|
||||
|
||||
Copyright 2016 Mozilla Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "The Coral Project: Talk",
|
||||
"env": {
|
||||
"TALK_SESSION_SECRET": {
|
||||
"description": "The session secret",
|
||||
"generator": "secret"
|
||||
},
|
||||
"TALK_FACEBOOK_APP_ID": {
|
||||
"value": "",
|
||||
"required": true
|
||||
},
|
||||
"TALK_FACEBOOK_APP_SECRET": {
|
||||
"value": "",
|
||||
"required": true
|
||||
},
|
||||
"NODE_ENV": "production",
|
||||
"TALK_SMTP_PORT": "2525",
|
||||
"REWRITE_ENV": "TALK_PORT:PORT,TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:POSTMARK_SMTP_SERVER,TALK_SMTP_USERNAME:POSTMARK_API_TOKEN,TALK_SMTP_PASSWORD:POSTMARK_API_TOKEN"
|
||||
},
|
||||
"addons": [{
|
||||
"plan": "mongolab:sandbox",
|
||||
"as": "MONGO"
|
||||
}, {
|
||||
"plan": "rediscloud:30",
|
||||
"as": "REDIS"
|
||||
}, {
|
||||
"plan": "postmark:10k",
|
||||
"as": "POSTMARK"
|
||||
}],
|
||||
"image": "heroku/nodejs",
|
||||
"success_url": "/admin/setup"
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Setup the debug paramater.
|
||||
*/
|
||||
|
||||
process.env.DEBUG = process.env.TALK_DEBUG;
|
||||
// Perform rewrites to the runtime environment variables based on the contents
|
||||
// of the process.env.REWRITE_ENV if it exists. This is done here as it is the
|
||||
// entrypoint for the entire application.
|
||||
require('env-rewrite').rewrite();
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Setup the debug paramater.
|
||||
*/
|
||||
|
||||
process.env.DEBUG = process.env.TALK_DEBUG;
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Setup the debug paramater.
|
||||
*/
|
||||
|
||||
process.env.DEBUG = process.env.TALK_DEBUG;
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Setup the debug paramater.
|
||||
*/
|
||||
|
||||
process.env.DEBUG = process.env.TALK_DEBUG;
|
||||
|
||||
const app = require('../app');
|
||||
const debug = require('debug')('talk:server');
|
||||
const http = require('http');
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Setup the debug paramater.
|
||||
*/
|
||||
|
||||
process.env.DEBUG = process.env.TALK_DEBUG;
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Setup the debug paramater.
|
||||
*/
|
||||
|
||||
process.env.DEBUG = process.env.TALK_DEBUG;
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ import {Router, Route, IndexRoute, browserHistory} from 'react-router';
|
||||
import ModerationQueue from 'containers/ModerationQueue/ModerationQueue';
|
||||
import CommentStream from 'containers/CommentStream/CommentStream';
|
||||
import Configure from 'containers/Configure/Configure';
|
||||
import Streams from 'containers/Streams/Streams';
|
||||
import CommunityContainer from 'containers/Community/CommunityContainer';
|
||||
import LayoutContainer from 'containers/LayoutContainer';
|
||||
|
||||
@@ -13,6 +14,7 @@ const routes = (
|
||||
<Route path='embed' component={CommentStream} />
|
||||
<Route path='community' component={CommunityContainer} />
|
||||
<Route path='configure' component={Configure} />
|
||||
<Route path='streams' component={Streams} />
|
||||
</Route>
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
FETCH_ASSETS_REQUEST,
|
||||
FETCH_ASSETS_SUCCESS,
|
||||
FETCH_ASSETS_FAILURE,
|
||||
UPDATE_ASSET_STATE_REQUEST,
|
||||
UPDATE_ASSET_STATE_SUCCESS,
|
||||
UPDATE_ASSET_STATE_FAILURE
|
||||
} from '../constants/assets';
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
|
||||
/**
|
||||
* Action disptacher related to assets
|
||||
*/
|
||||
|
||||
// Fetch a page of assets
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch) => {
|
||||
dispatch({type: FETCH_ASSETS_REQUEST});
|
||||
return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`)
|
||||
.then(({result, count}) =>
|
||||
dispatch({type: FETCH_ASSETS_SUCCESS,
|
||||
assets: result,
|
||||
count
|
||||
}))
|
||||
.catch(error => dispatch({type: FETCH_ASSETS_FAILURE, error}));
|
||||
};
|
||||
|
||||
// Update an asset state
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const updateAssetState = (id, closedAt) => (dispatch) => {
|
||||
dispatch({type: UPDATE_ASSET_STATE_REQUEST});
|
||||
return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
|
||||
.then(() =>
|
||||
dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
|
||||
.catch(error => dispatch({type: UPDATE_ASSET_STATE_FAILURE, error}));
|
||||
};
|
||||
@@ -1,30 +1,69 @@
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
import * as commentActions from '../constants/comments';
|
||||
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const fetchModerationQueueComments = () => {
|
||||
return dispatch => {
|
||||
dispatch({type: commentActions.COMMENTS_MODERATION_QUEUE_FETCH});
|
||||
return Promise.all([
|
||||
coralApi('/queue/comments/pending'),
|
||||
coralApi('/comments?status=rejected'),
|
||||
coralApi('/comments?action_type=flag')
|
||||
])
|
||||
.then(([pending, rejected, flagged]) => {
|
||||
|
||||
/* Combine seperate calls into a single object */
|
||||
flagged.comments.forEach(comment => comment.flagged = true);
|
||||
return {
|
||||
comments: [...pending.comments, ...rejected.comments, ...flagged.comments],
|
||||
users: [...pending.users, ...rejected.users, ...flagged.users],
|
||||
actions: [...pending.actions, ...rejected.actions, ...flagged.actions]
|
||||
};
|
||||
})
|
||||
.then(({comments, users}) => {
|
||||
|
||||
/* Post comments and users to redux store. Actions will be posted when they are needed. */
|
||||
dispatch({type: commentActions.USERS_MODERATION_QUEUE_FETCH_SUCCESS, users});
|
||||
dispatch({type: commentActions.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS, comments});
|
||||
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// Create a new comment
|
||||
export const createComment = (name, body) => {
|
||||
return dispatch => {
|
||||
const comment = {body, name};
|
||||
return coralApi('/comments', {method: 'POST', comment})
|
||||
.then(res => dispatch({type: commentActions.COMMENT_CREATE_SUCCESS, comment: res}))
|
||||
.catch(error => dispatch({type: commentActions.COMMENT_CREATE_FAILED, error}));
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Action disptacher related to comments
|
||||
*/
|
||||
|
||||
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)});
|
||||
// Update a comment. Now to update a comment we need to send back the whole object
|
||||
export const updateStatus = (status, comment) => {
|
||||
return dispatch => {
|
||||
dispatch({type: commentActions.COMMENT_STATUS_UPDATE, id: comment.id, status});
|
||||
return coralApi(`/comments/${comment.id}/status`, {method: 'PUT', body: {status}})
|
||||
.then(res => dispatch({type: commentActions.COMMENT_UPDATE_SUCCESS, res}))
|
||||
.catch(error => dispatch({type: commentActions.COMMENT_UPDATE_FAILED, error}));
|
||||
};
|
||||
};
|
||||
|
||||
export const flagComment = id => (dispatch, getState) => {
|
||||
dispatch({type: 'COMMENT_FLAG', id});
|
||||
dispatch({type: commentActions.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});
|
||||
};
|
||||
|
||||
// Dialog Actions
|
||||
export const showBanUserDialog = (userId, userName, commentId) => {
|
||||
return dispatch => {
|
||||
dispatch({type: 'SHOW_BANUSER_DIALOG', userId, userName, commentId});
|
||||
};
|
||||
return {type: commentActions.SHOW_BANUSER_DIALOG, userId, userName, commentId};
|
||||
};
|
||||
|
||||
export const hideBanUserDialog = (showDialog) => {
|
||||
return dispatch => {
|
||||
dispatch({type: 'HIDE_BANUSER_DIALOG', showDialog});
|
||||
};
|
||||
return {type: commentActions.HIDE_BANUSER_DIALOG, showDialog};
|
||||
};
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
import * as actions from '../constants/user';
|
||||
|
||||
/**
|
||||
* Action disptacher related to users
|
||||
*/
|
||||
//
|
||||
// export const banUser = (status, author_id) => (dispatch) => {
|
||||
// dispatch({type: 'USER_STATUS_UPDATE', author_id, status});
|
||||
// };
|
||||
export const banUser = (status, userId, commentId) => {
|
||||
// change status of a user
|
||||
export const userStatusUpdate = (status, userId, commentId) => {
|
||||
return dispatch => {
|
||||
dispatch({type: 'USER_BAN', status, userId, commentId});
|
||||
dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH'});
|
||||
dispatch({type: actions.UPDATE_STATUS_REQUEST});
|
||||
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
|
||||
.then(res => dispatch({type: actions.UPDATE_STATUS_SUCCESS, res}))
|
||||
.catch(error => dispatch({type: actions.UPDATE_STATUS_FAILURE, error}));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ export default props => {
|
||||
<div>
|
||||
{links ?
|
||||
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
|
||||
<div className={styles.actions}>
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{props.actions.map((action, i) => getActionButton(action, i, props))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,21 +63,23 @@ const getActionButton = (action, i, props) => {
|
||||
if (action === 'ban') {
|
||||
return (
|
||||
<Button
|
||||
disabled={banned ? 'disabled' : ''}
|
||||
className='ban'
|
||||
cStyle='black'
|
||||
disabled={banned ? 'disabled' : ''}
|
||||
onClick={() => props.onClickShowBanDialog(author.id, author.displayName, comment.id)}
|
||||
key={i} >
|
||||
key={i}
|
||||
>
|
||||
{lang.t('comment.ban_user')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FabButton
|
||||
className={styles.actionButton}
|
||||
icon={props.actionsMap[action].icon}
|
||||
className={`${action} ${styles.actionButton}`}
|
||||
cStyle={action}
|
||||
icon={props.actionsMap[action].icon}
|
||||
key={i}
|
||||
onClick={() => props.onClickAction(props.actionsMap[action].status, comment.id)}
|
||||
onClick={() => props.onClickAction(props.actionsMap[action].status, comment)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -126,7 +126,10 @@ export default class CommentList extends React.Component {
|
||||
const {active} = this.state;
|
||||
|
||||
return (
|
||||
<ul className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}>
|
||||
<ul
|
||||
className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}
|
||||
id='commentList'
|
||||
>
|
||||
{commentIds.map((commentId, index) => {
|
||||
const comment = comments[commentId];
|
||||
const author = users[comment.author_id];
|
||||
|
||||
@@ -16,6 +16,8 @@ export default ({handleLogout}) => (
|
||||
activeClassName={styles.active}>{lang.t('configure.community')}</Link>
|
||||
<Link className={styles.navLink} to="/admin/configure"
|
||||
activeClassName={styles.active}>{lang.t('configure.configure')}</Link>
|
||||
<Link className={styles.navLink} to="/admin/streams"
|
||||
activeClassName={styles.active}>{lang.t('configure.streams')}</Link>
|
||||
</Navigation>
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export const FETCH_ASSETS_REQUEST = 'FETCH_ASSETS_REQUEST';
|
||||
export const FETCH_ASSETS_SUCCESS = 'FETCH_ASSETS_SUCCESS';
|
||||
export const FETCH_ASSETS_FAILURE = 'FETCH_ASSETS_FAILURE';
|
||||
export const UPDATE_ASSET_STATE_REQUEST = 'UPDATE_ASSET_STATE_REQUEST';
|
||||
export const UPDATE_ASSET_STATE_SUCCESS = 'UPDATE_ASSET_STATE_SUCCESS';
|
||||
export const UPDATE_ASSET_STATE_FAILURE = 'UPDATE_ASSET_STATE_FAILURE';
|
||||
@@ -1,3 +1,12 @@
|
||||
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
|
||||
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
|
||||
export const USER_BAN_SUCESS = 'USER_BAN_SUCESS';
|
||||
export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS';
|
||||
export const COMMENTS_MODERATION_QUEUE_FETCH = 'COMMENTS_MODERATION_QUEUE_FETCH';
|
||||
export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS = 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS';
|
||||
export const COMMENT_CREATE_SUCCESS = 'COMMENT_CREATE_SUCCESS';
|
||||
export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED';
|
||||
export const COMMENT_STREAM_FETCH_SUCCESS = 'COMMENT_STREAM_FETCH_SUCCESS';
|
||||
export const COMMENT_UPDATE_SUCCESS = 'COMMENT_UPDATE_SUCCESS';
|
||||
export const COMMENT_UPDATE_FAILED = 'COMMENT_UPDATE_FAILED';
|
||||
export const COMMENT_STATUS_UPDATE = 'COMMENT_STATUS_UPDATE';
|
||||
export const COMMENT_FLAG = 'COMMENT_FLAG';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const UPDATE_STATUS_REQUEST = 'UPDATE_STATUS_REQUEST';
|
||||
export const UPDATE_STATUS_SUCCESS = 'UPDATE_STATUS_SUCCESS';
|
||||
export const UPDATE_STATUS_FAILURE = 'UPDATE_STATUS_FAILURE';
|
||||
@@ -7,7 +7,7 @@ import styles from './Community.css';
|
||||
import Table from './Table';
|
||||
import Loading from './Loading';
|
||||
import NoResults from './NoResults';
|
||||
import Pager from './Pager';
|
||||
import Pager from 'coral-ui/components/Pager';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => {
|
||||
|
||||
const CommentSettings = ({fetchingSettings, updateSettings, settingsError, settings, errors}) => {
|
||||
if (fetchingSettings) {
|
||||
|
||||
/* maybe a spinner here at some point */
|
||||
return <p>Loading settings...</p>;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {Icon} from 'react-mdl';
|
||||
import key from 'keymaster';
|
||||
|
||||
import ModerationKeysModal from 'components/ModerationKeysModal';
|
||||
import CommentList from 'components/CommentList';
|
||||
import BanUserDialog from 'components/BanUserDialog';
|
||||
|
||||
import {updateStatus, showBanUserDialog, hideBanUserDialog} from 'actions/comments';
|
||||
import {banUser} from 'actions/users';
|
||||
import {
|
||||
updateStatus,
|
||||
showBanUserDialog,
|
||||
hideBanUserDialog,
|
||||
fetchModerationQueueComments
|
||||
} from 'actions/comments';
|
||||
import {userStatusUpdate} from 'actions/users';
|
||||
import styles from './ModerationQueue.css';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
@@ -32,7 +36,7 @@ class ModerationQueue extends React.Component {
|
||||
|
||||
// Fetch comments and bind singleView key before render
|
||||
componentWillMount () {
|
||||
this.props.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH'});
|
||||
this.props.dispatch(fetchModerationQueueComments());
|
||||
key('s', () => this.setState({singleView: !this.state.singleView}));
|
||||
key('shift+/', () => this.setState({modalOpen: true}));
|
||||
key('esc', () => this.setState({modalOpen: false}));
|
||||
@@ -55,10 +59,10 @@ class ModerationQueue extends React.Component {
|
||||
}
|
||||
|
||||
// Dispatch the update status action
|
||||
onCommentAction (action, id) {
|
||||
onCommentAction (action, comment) {
|
||||
|
||||
// If not banning then change the status to approved or flagged as action = status
|
||||
this.props.dispatch(updateStatus(action, id));
|
||||
this.props.dispatch(updateStatus(action, comment));
|
||||
}
|
||||
|
||||
showBanUserDialog (userId, userName, commentId) {
|
||||
@@ -70,11 +74,10 @@ class ModerationQueue extends React.Component {
|
||||
}
|
||||
|
||||
banUser (userId, commentId) {
|
||||
this.props.dispatch(banUser('banned', userId, commentId));
|
||||
}
|
||||
|
||||
showShortcuts = () => {
|
||||
this.setState({modalOpen: true});
|
||||
this.props.dispatch(userStatusUpdate('banned', userId, commentId))
|
||||
.then(() => {
|
||||
this.props.dispatch(fetchModerationQueueComments());
|
||||
});
|
||||
}
|
||||
|
||||
onTabClick (activeTab) {
|
||||
@@ -100,11 +103,6 @@ class ModerationQueue extends React.Component {
|
||||
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.rejected')}</a>
|
||||
<a href='#flagged' onClick={() => this.onTabClick('flagged')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.flagged')}</a>
|
||||
<a href='#shortcuts' onClick={this.showShortcuts}
|
||||
className={`mdl-tabs__tab ${styles.tab} ${styles.showShortcuts}`}>
|
||||
<Icon name='keyboard' />
|
||||
<span>{lang.t('modqueue.showshortcuts')}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='pending'>
|
||||
<CommentList
|
||||
@@ -113,7 +111,7 @@ class ModerationQueue extends React.Component {
|
||||
commentIds={premodIds}
|
||||
comments={comments.byId}
|
||||
users={users.byId}
|
||||
onClickAction={(action, commentId) => this.onCommentAction(action, commentId)}
|
||||
onClickAction={(action, comment) => this.onCommentAction(action, comment)}
|
||||
onClickShowBanDialog={(userId, userName, commentId) => this.showBanUserDialog(userId, userName, commentId)}
|
||||
actions={['reject', 'approve', 'ban']}
|
||||
loading={comments.loading} />
|
||||
@@ -122,7 +120,7 @@ class ModerationQueue extends React.Component {
|
||||
handleClose={() => this.hideBanUserDialog()}
|
||||
onClickBanUser={(userId, commentId) => this.banUser(userId, commentId)}
|
||||
user={comments.banUser}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
|
||||
<CommentList
|
||||
isActive={activeTab === 'rejected'}
|
||||
@@ -130,29 +128,26 @@ class ModerationQueue extends React.Component {
|
||||
commentIds={rejectedIds}
|
||||
comments={comments.byId}
|
||||
users={users.byId}
|
||||
onClickAction={(action, id) => this.onCommentAction(action, id)}
|
||||
onClickAction={(action, comment) => this.onCommentAction(action, comment)}
|
||||
actions={['approve']}
|
||||
loading={comments.loading} />
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='flagged'>
|
||||
<CommentList
|
||||
isActive={activeTab === 'flagged'}
|
||||
isActive={activeTab === 'rejected'}
|
||||
singleView={singleView}
|
||||
commentIds={flaggedIds}
|
||||
comments={comments.byId}
|
||||
users={users.byId}
|
||||
onClickAction={(action, id) => this.onCommentAction(action, id)}
|
||||
onClickAction={(action, comment) => this.onCommentAction(action, comment)}
|
||||
actions={['reject', 'approve']}
|
||||
loading={comments.loading} />
|
||||
</div>
|
||||
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='shortcuts'>
|
||||
<ModerationKeysModal open={modalOpen}
|
||||
onClose={() => this.setState({modalOpen: false})} />
|
||||
</div>
|
||||
<ModerationKeysModal open={modalOpen}
|
||||
onClose={() => this.setState({modalOpen: false})} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
.container {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.leftColumn {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
width: calc(90% - 200px);
|
||||
}
|
||||
|
||||
.searchIcon {
|
||||
vertical-align: middle;
|
||||
font-size: 18px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
padding: 3px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
width: 90%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.searchBoxInput {
|
||||
border: none;
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.searchBoxInput:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.optionHeader {
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.optionDetail {
|
||||
font-size: 16px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.streamsTable {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.radio {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.statusMenu {
|
||||
border-radius: 3px;
|
||||
width: 10em;
|
||||
text-align: center;
|
||||
float: right;
|
||||
border: 1px solid #ccc;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.statusMenuOpen {
|
||||
padding: 10px;
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.statusMenuIcon {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.statusMenuClosed {
|
||||
padding: 10px;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import React, {Component} from 'react';
|
||||
import styles from './Streams.css';
|
||||
import {connect} from 'react-redux';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import {fetchAssets, updateAssetState} from '../../actions/assets';
|
||||
import translations from '../../translations.json';
|
||||
import {
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Icon,
|
||||
DataTable,
|
||||
TableHeader
|
||||
} from 'react-mdl';
|
||||
import Pager from 'coral-ui/components/Pager';
|
||||
|
||||
const limit = 25;
|
||||
|
||||
class Streams extends Component {
|
||||
|
||||
state = {
|
||||
search: '',
|
||||
sort: 'desc',
|
||||
filter: 'all',
|
||||
statusMenus: {},
|
||||
timer: null,
|
||||
page: 0
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.props.fetchAssets(0, limit, '', this.state.sortBy);
|
||||
}
|
||||
|
||||
onSettingChange = (setting) => (e) => {
|
||||
let options = this.state;
|
||||
this.setState({[setting]: e.target.value});
|
||||
options[setting] = e.target.value;
|
||||
this.props.fetchAssets(0, limit, options.search, options.sort, options.filter);
|
||||
}
|
||||
|
||||
onSearchChange = (e) => {
|
||||
this.setState((prevState) => {
|
||||
prevState.search = e.target.value;
|
||||
clearTimeout(prevState.timer);
|
||||
const fetchAssets = this.props.fetchAssets;
|
||||
prevState.timer = setTimeout(() => {
|
||||
fetchAssets(0, limit, e.target.value, this.state.sort, this.state.filter);
|
||||
}, 350);
|
||||
return prevState;
|
||||
});
|
||||
}
|
||||
|
||||
renderDate = (date) => {
|
||||
const d = new Date(date);
|
||||
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
onStatusClick = (closeStream, id, statusMenuOpen) => () => {
|
||||
if (statusMenuOpen) {
|
||||
this.setState(prev => {
|
||||
prev.statusMenus[id] = false;
|
||||
return prev;
|
||||
});
|
||||
this.props.updateAssetState(id, closeStream ? Date.now() : null)
|
||||
.then(() => {
|
||||
const {search, sort, filter, page} = this.state;
|
||||
this.props.fetchAssets(page, limit, search, sort, filter);
|
||||
});
|
||||
} else {
|
||||
this.setState(prev => {
|
||||
prev.statusMenus[id] = true;
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderStatus = (closedAt, {id}) => {
|
||||
const closed = closedAt && new Date(closedAt).getTime() < Date.now();
|
||||
const statusMenuOpen = this.state.statusMenus[id];
|
||||
return <div className={styles.statusMenu}>
|
||||
<div
|
||||
className={closed ? styles.statusMenuClosed : styles.statusMenuOpen}
|
||||
onClick={this.onStatusClick(closed, id, statusMenuOpen)}>
|
||||
{closed ? lang.t('streams.closed') : lang.t('streams.open')}
|
||||
{!statusMenuOpen && <Icon className={styles.statusMenuIcon} name='keyboard_arrow_down'/>}
|
||||
</div>
|
||||
{
|
||||
statusMenuOpen &&
|
||||
<div
|
||||
className={!closed ? styles.statusMenuClosed : styles.statusMenuOpen}
|
||||
onClick={this.onStatusClick(!closed, id, statusMenuOpen)}>
|
||||
{!closed ? lang.t('streams.closed') : lang.t('streams.open')}
|
||||
</div>
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
|
||||
onPageClick = (page) => {
|
||||
this.setState({page});
|
||||
const {search, sort, filter} = this.state;
|
||||
this.props.fetchAssets((page - 1) * limit, limit, search, sort, filter);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {search, sort, filter} = this.state;
|
||||
const {assets} = this.props;
|
||||
|
||||
return <div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
|
||||
<div className={styles.searchBox}>
|
||||
<Icon name='search' className={styles.searchIcon}/>
|
||||
<input
|
||||
type='text'
|
||||
value={search}
|
||||
className={styles.searchBoxInput}
|
||||
onChange={this.onSearchChange}
|
||||
placeholder={lang.t('streams.search')}/>
|
||||
</div>
|
||||
|
||||
<div className={styles.optionHeader}>{lang.t('streams.filter-streams')}</div>
|
||||
<div className={styles.optionDetail}>{lang.t('streams.stream-status')}</div>
|
||||
<RadioGroup
|
||||
name='status filter'
|
||||
value={filter}
|
||||
childContainer='div'
|
||||
onChange={this.onSettingChange('filter')}>
|
||||
<Radio value='all'>{lang.t('streams.all')}</Radio>
|
||||
<Radio value='open'>{lang.t('streams.open')}</Radio>
|
||||
<Radio value='closed'>{lang.t('streams.closed')}</Radio>
|
||||
</RadioGroup>
|
||||
<div className={styles.optionHeader}>{lang.t('streams.sort-by')}</div>
|
||||
<RadioGroup
|
||||
name='sort by'
|
||||
value={sort}
|
||||
childContainer='div'
|
||||
onChange={this.onSettingChange('sort')}>
|
||||
<Radio value='desc'>{lang.t('streams.newest')}</Radio>
|
||||
<Radio value='asc'>{lang.t('streams.oldest')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div className={styles.mainContent}>
|
||||
<DataTable
|
||||
className={styles.streamsTable}
|
||||
rows={assets.ids.map((id) => assets.byId[id])}>
|
||||
<TableHeader name="title">{lang.t('streams.article')}</TableHeader>
|
||||
<TableHeader numeric name="publication_date" cellFormatter={this.renderDate}>
|
||||
{lang.t('streams.pubdate')}
|
||||
</TableHeader>
|
||||
<TableHeader numeric name="closedAt" cellFormatter={this.renderStatus}>
|
||||
{lang.t('streams.status')}
|
||||
</TableHeader>
|
||||
</DataTable>
|
||||
<Pager
|
||||
totalPages={Math.ceil((assets.count || 0) / limit)}
|
||||
page={this.state.page}
|
||||
onNewPageHandler={this.onPageClick}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({assets}) => {
|
||||
return {
|
||||
assets: assets.toJS()
|
||||
};
|
||||
};
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
fetchAssets: (...args) => {
|
||||
dispatch(fetchAssets.apply(this, args));
|
||||
},
|
||||
updateAssetState: (...args) => dispatch(updateAssetState.apply(this, args))
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Streams);
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -0,0 +1,24 @@
|
||||
import {Map, List, fromJS} from 'immutable';
|
||||
import {FETCH_ASSETS_SUCCESS, UPDATE_ASSET_STATE_REQUEST} from '../constants/assets';
|
||||
|
||||
const initialState = Map({
|
||||
byId: Map(),
|
||||
ids: List()
|
||||
});
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case FETCH_ASSETS_SUCCESS:
|
||||
return replaceAssets(action, state);
|
||||
case UPDATE_ASSET_STATE_REQUEST:
|
||||
return state.setIn(['byId', action.id, 'closedAt'], action.closedAt);
|
||||
default: return state;
|
||||
}
|
||||
};
|
||||
|
||||
const replaceAssets = (action, state) => {
|
||||
const assets = fromJS(action.assets.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {}));
|
||||
return state.set('byId', assets)
|
||||
.set('count', action.count)
|
||||
.set('ids', List(assets.keys()));
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as actions from '../constants/comments';
|
||||
import * as userActions from '../constants/user';
|
||||
import {Map, List, fromJS} from 'immutable';
|
||||
|
||||
/**
|
||||
@@ -23,16 +24,17 @@ const initialState = Map({
|
||||
// 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);
|
||||
case actions.COMMENTS_MODERATION_QUEUE_FETCH: return state.set('loading', true);
|
||||
case actions.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS: return replaceComments(action, state);
|
||||
case actions.COMMENTS_MODERATION_QUEUE_FAILED: return state.set('loading', false);
|
||||
case actions.COMMENT_STATUS_UPDATE: return updateStatus(state, action);
|
||||
case actions.COMMENT_FLAG: return flag(state, action);
|
||||
case actions.COMMENT_CREATE_SUCCESS: return addComment(state, action);
|
||||
case actions.COMMENT_STREAM_FETCH_SUCCESS: return replaceComments(action, state);
|
||||
case actions.SHOW_BANUSER_DIALOG: return setBanUser(state, true, action);
|
||||
case actions.HIDE_BANUSER_DIALOG: return setBanUser(state, false, action);
|
||||
case actions.USER_BAN_SUCESS: return setBanUser(state, false, action);
|
||||
case actions.USER_BAN_SUCCESS: return setBanUser(state, false, action);
|
||||
case userActions.UPDATE_STATUS_SUCCESS: return setBanUser(state, false, action);
|
||||
default: return state;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import settings from 'reducers/settings';
|
||||
import community from 'reducers/community';
|
||||
import users from 'reducers/users';
|
||||
import auth from 'reducers/auth';
|
||||
import assets from 'reducers/assets';
|
||||
|
||||
// Combine all reducers into a main one
|
||||
export default combineReducers({
|
||||
@@ -11,5 +12,6 @@ export default combineReducers({
|
||||
comments,
|
||||
community,
|
||||
auth,
|
||||
users
|
||||
users,
|
||||
assets
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
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
|
||||
@@ -14,5 +13,5 @@ import talkAdapter from 'services/talk-adapter';
|
||||
export default createStore(
|
||||
mainReducer,
|
||||
window.devToolsExtension && window.devToolsExtension(),
|
||||
applyMiddleware(thunk, talkAdapter)
|
||||
applyMiddleware(thunk)
|
||||
);
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
|
||||
/**
|
||||
* The adapter is a redux middleware that interecepts the actions that need
|
||||
* to interface with the backend, do the job and return the results.
|
||||
* The idea is that if we expose the required actions to handle to devs, the
|
||||
* moderation app can be platform agnostic. This same client could work not only
|
||||
* for the coral but also for wordpress comments, disqus and many more.
|
||||
*/
|
||||
|
||||
// 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_UPDATE':
|
||||
updateComment(store, action.comment);
|
||||
break;
|
||||
case 'COMMENT_CREATE':
|
||||
createComment(store, action.name, action.body);
|
||||
break;
|
||||
case 'USER_BAN':
|
||||
userStatusUpdate(store, action.status, action.userId, action.commentId);
|
||||
break;
|
||||
}
|
||||
|
||||
next(action);
|
||||
};
|
||||
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
const fetchModerationQueueComments = store =>
|
||||
|
||||
Promise.all([
|
||||
coralApi('/queue/comments/pending'),
|
||||
coralApi('/comments?status=rejected'),
|
||||
coralApi('/comments?action_type=flag')
|
||||
])
|
||||
.then(([pending, rejected, flagged]) => {
|
||||
|
||||
/* Combine seperate calls into a single object */
|
||||
let all = {};
|
||||
all.comments = pending.comments
|
||||
.concat(rejected.comments)
|
||||
.concat(flagged.comments.map(comment => {
|
||||
comment.flagged = true;
|
||||
return comment;
|
||||
}));
|
||||
all.users = pending.users
|
||||
.concat(rejected.users)
|
||||
.concat(flagged.users);
|
||||
all.actions = pending.actions
|
||||
.concat(rejected.actions)
|
||||
.concat(flagged.actions);
|
||||
return all;
|
||||
})
|
||||
.then(all => {
|
||||
|
||||
/* Post comments and users to redux store. Actions will be posted when they are needed. */
|
||||
store.dispatch({type: 'USERS_MODERATION_QUEUE_FETCH_SUCCESS',
|
||||
users: all.users});
|
||||
store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS',
|
||||
comments: all.comments});
|
||||
|
||||
});
|
||||
|
||||
// .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) => {
|
||||
coralApi(`/comments/${comment.get('id')}/status`, {method: 'PUT', body: {status: comment.get('status')}})
|
||||
.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) => {
|
||||
const body = {
|
||||
status: 'Untouched',
|
||||
body: comment,
|
||||
name: name,
|
||||
createdAt: Date.now()
|
||||
};
|
||||
return coralApi('/comments', {method: 'POST', body})
|
||||
.then(res => store.dispatch({type: 'COMMENT_CREATE_SUCCESS', comment: res}))
|
||||
.catch(error => store.dispatch({type: 'COMMENT_CREATE_FAILED', error}));
|
||||
};
|
||||
|
||||
// Ban a user
|
||||
const userStatusUpdate = (store, status, userId, commentId) => {
|
||||
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
|
||||
.then(res => store.dispatch({type: 'USER_BAN_SUCESS', res}))
|
||||
.catch(error => store.dispatch({type: 'USER_BAN_FAILED', error}));
|
||||
};
|
||||
@@ -56,6 +56,7 @@
|
||||
"moderate": "Moderate",
|
||||
"configure": "Configure",
|
||||
"community": "Community",
|
||||
"streams": "Streams",
|
||||
"closed-comments-desc": "Write a message for closed threads",
|
||||
"closed-comments-label": "Write a message...",
|
||||
"hours": "Hours",
|
||||
@@ -73,6 +74,22 @@
|
||||
"note": "Note: Banning this user will also place this comment in the Rejected queue.",
|
||||
"cancel": "Cancel",
|
||||
"yes_ban_user": "Yes, Ban User"
|
||||
},
|
||||
"streams": {
|
||||
"search": "Search",
|
||||
"filter-streams": "Filter Streams",
|
||||
"stream-status": "Stream Status",
|
||||
"all": "All",
|
||||
"open": "Open",
|
||||
"closed": "Closed",
|
||||
"newest": "Newest",
|
||||
"oldest": "Oldest",
|
||||
"sort-by": "Sort By",
|
||||
"open": "Open",
|
||||
"closed": "Closed",
|
||||
"article": "Article",
|
||||
"pubdate": "Publication Date",
|
||||
"status": "Status"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
@@ -139,6 +156,22 @@
|
||||
"note": "Nota: Suspender este usuario también va a colocar este comentario en la cola de Rechazados.",
|
||||
"cancel": "Cancelar",
|
||||
"yes_ban_user": "Si, Suspendan el usuario"
|
||||
},
|
||||
"streams": {
|
||||
"search": "",
|
||||
"filter-streams": "",
|
||||
"stream-status": "",
|
||||
"all": "",
|
||||
"open": "",
|
||||
"closed": "",
|
||||
"newest": "",
|
||||
"oldest": "",
|
||||
"sort-by": "",
|
||||
"open": "",
|
||||
"closed": "",
|
||||
"article": "",
|
||||
"pubdate": "",
|
||||
"status": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
import translations from './translations.json';
|
||||
|
||||
const name = 'coral-plugin-flags';
|
||||
const name = 'coral-plugin-likes';
|
||||
|
||||
const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, currentUser, banned}) => {
|
||||
const liked = like && like.current_user;
|
||||
|
||||
@@ -9,10 +9,11 @@ import ForgotContent from './ForgotContent';
|
||||
const SignDialog = ({open, view, handleClose, offset, ...props}) => (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
id="signInDialog"
|
||||
open={open}
|
||||
style={{
|
||||
position: 'relative',
|
||||
top: offset !== 0 && offset
|
||||
top: offset !== 0 && offset
|
||||
}}>
|
||||
<span className={styles.close} onClick={handleClose}>×</span>
|
||||
{view === 'SIGNIN' && <SignInContent {...props} />}
|
||||
|
||||
@@ -9,7 +9,9 @@ const UserBox = ({className, user, logout, ...props}) => (
|
||||
className={`${styles.userBox} ${className ? className : ''}`}
|
||||
{...props}
|
||||
>
|
||||
{lang.t('signIn.loggedInAs')} <a>{user.displayName}</a>. {lang.t('signIn.notYou')} <a onClick={logout}>{lang.t('signIn.logout')}</a>
|
||||
{lang.t('signIn.loggedInAs')}
|
||||
<a>{user.displayName}</a>. {lang.t('signIn.notYou')}
|
||||
<a onClick={logout} id='logout'>{lang.t('signIn.logout')}</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ input.error{
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
margin-left: 4px;
|
||||
padding-bottom: 2px;
|
||||
border-bottom: solid 1px black;
|
||||
}
|
||||
|
||||
+2
-3
@@ -12,7 +12,7 @@ const Pager = ({totalPages, page, onNewPageHandler}) => (
|
||||
<div className="pager">
|
||||
<ul>
|
||||
{
|
||||
(totalPages > page) ?
|
||||
(totalPages > page && totalPages > 1) ?
|
||||
<li
|
||||
className={`mdl-button mdl-js-button ${styles.li}`}
|
||||
onClick={() => onNewPageHandler(page - 1)}>
|
||||
@@ -23,7 +23,7 @@ const Pager = ({totalPages, page, onNewPageHandler}) => (
|
||||
}
|
||||
{Rows(page, totalPages, onNewPageHandler)}
|
||||
{
|
||||
(page < totalPages) ?
|
||||
(page < totalPages && totalPages > 1) ?
|
||||
<li
|
||||
className={`mdl-button mdl-js-button ${styles.li}`}
|
||||
onClick={() => onNewPageHandler(page + 1)}>
|
||||
@@ -42,4 +42,3 @@ Pager.propTypes = {
|
||||
};
|
||||
|
||||
export default Pager;
|
||||
|
||||
+17
-8
@@ -19,6 +19,7 @@ module.exports = {
|
||||
},
|
||||
'test_settings': {
|
||||
'default': {
|
||||
'launch_url' : 'http://localhost:3000',
|
||||
'selenium_port': 6666,
|
||||
'selenium_host': 'localhost',
|
||||
'silent': true,
|
||||
@@ -26,19 +27,27 @@ module.exports = {
|
||||
'browserName': 'chrome',
|
||||
'javascriptEnabled': true,
|
||||
'acceptSslCerts': true,
|
||||
'webStorageEnabled' : true,
|
||||
'databaseEnabled' : true,
|
||||
'applicationCacheEnabled' : false,
|
||||
'nativeEvents' : true
|
||||
'webStorageEnabled': true,
|
||||
'databaseEnabled': true,
|
||||
'applicationCacheEnabled': false,
|
||||
'nativeEvents': true
|
||||
},
|
||||
'screenshots' : {
|
||||
'enabled' : true,
|
||||
'on_failure' : true,
|
||||
'on_error' : true,
|
||||
'path' : './tests/e2e/reports'
|
||||
'enabled': true,
|
||||
'on_failure': true,
|
||||
'on_error': true,
|
||||
'path': './tests/e2e/reports'
|
||||
},
|
||||
'exclude': [
|
||||
'./tests/e2e/tests/EmbedStreamTests.js'
|
||||
]
|
||||
},
|
||||
'integration': {
|
||||
'launch_url': 'http://localhost:3000'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// "chromeOptions" : {
|
||||
// "args" : ["start-fullscreen"]
|
||||
// }
|
||||
|
||||
+15
-12
@@ -5,15 +5,16 @@
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "./bin/cli serve --jobs",
|
||||
"build": "NODE_ENV=production ./node_modules/.bin/webpack --config webpack.config.js --bail",
|
||||
"build-watch": "NODE_ENV=development ./node_modules/.bin/webpack --config webpack.config.dev.js --watch",
|
||||
"lint": "./node_modules/.bin/eslint bin/* .",
|
||||
"lint-fix": "./node_modules/.bin/eslint bin/* . --fix",
|
||||
"test": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-core/register tests/helpers/*.js --require ignore-styles --recursive tests",
|
||||
"test-watch": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-core/register --recursive -w tests",
|
||||
"pree2e": "NODE_ENV=test ./scripts/pree2e.sh",
|
||||
"e2e": "NODE_ENV=test ./node_modules/.bin/nightwatch",
|
||||
"embed-start": "NODE_ENV=development npm run build && ./bin/cli serve --jobs"
|
||||
"build": "NODE_ENV=production webpack --config webpack.config.js --bail",
|
||||
"build-watch": "NODE_ENV=development webpack --config webpack.config.dev.js --watch",
|
||||
"lint": "eslint bin/* .",
|
||||
"lint-fix": "eslint bin/* . --fix",
|
||||
"test": "NODE_ENV=test mocha --compilers js:babel-core/register tests/helpers/*.js --require ignore-styles --recursive tests",
|
||||
"test-watch": "NODE_ENV=test mocha --compilers js:babel-core/register --recursive -w tests",
|
||||
"pree2e": "NODE_ENV=test scripts/pree2e.sh",
|
||||
"e2e": "NODE_ENV=test nightwatch",
|
||||
"embed-start": "NODE_ENV=development npm run build && ./bin/cli serve --jobs",
|
||||
"postinstall": "npm run build"
|
||||
},
|
||||
"config": {
|
||||
"pre-git": {
|
||||
@@ -53,6 +54,7 @@
|
||||
"connect-redis": "^3.1.0",
|
||||
"debug": "^2.2.0",
|
||||
"ejs": "^2.5.2",
|
||||
"env-rewrite": "^1.0.2",
|
||||
"express": "^4.14.0",
|
||||
"express-session": "^1.14.2",
|
||||
"helmet": "^3.1.0",
|
||||
@@ -74,10 +76,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^6.5.2",
|
||||
"babel-core": "^6.18.2",
|
||||
"babel-core": "^6.21.0",
|
||||
"babel-eslint": "^7.1.0",
|
||||
"babel-jest": "^15.0.0",
|
||||
"babel-loader": "^6.2.7",
|
||||
"babel-plugin-add-module-exports": "^0.2.1",
|
||||
"babel-plugin-transform-async-to-generator": "^6.16.0",
|
||||
"babel-plugin-transform-class-properties": "^6.18.0",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
@@ -115,7 +118,7 @@
|
||||
"material-design-lite": "^1.2.1",
|
||||
"mocha": "^3.1.2",
|
||||
"mocha-junit-reporter": "^1.12.1",
|
||||
"nightwatch": "^0.9.9",
|
||||
"nightwatch": "^0.9.11",
|
||||
"node-fetch": "^1.6.3",
|
||||
"postcss-loader": "^1.1.0",
|
||||
"postcss-modules": "^0.5.2",
|
||||
@@ -136,7 +139,7 @@
|
||||
"redux-mock-store": "^1.2.1",
|
||||
"redux-thunk": "^2.1.0",
|
||||
"regenerator": "^0.8.46",
|
||||
"selenium-standalone": "^5.8.0",
|
||||
"selenium-standalone": "latest",
|
||||
"style-loader": "^0.13.1",
|
||||
"supertest": "^2.0.1",
|
||||
"timeago.js": "^2.0.3",
|
||||
|
||||
@@ -12,18 +12,47 @@ router.get('/', (req, res, next) => {
|
||||
skip = 0,
|
||||
sort = 'asc',
|
||||
field = 'created_at',
|
||||
filter = 'all',
|
||||
search = ''
|
||||
} = req.query;
|
||||
|
||||
const FilterOpenAssets = (query, filter) => {
|
||||
switch(filter) {
|
||||
case 'open':
|
||||
return query.merge({
|
||||
$or: [
|
||||
{
|
||||
closedAt: null
|
||||
},
|
||||
{
|
||||
closedAt: {
|
||||
$gt: Date.now()
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
case 'closed':
|
||||
return query.merge({
|
||||
closedAt: {
|
||||
$lt: Date.now()
|
||||
}
|
||||
});
|
||||
default:
|
||||
return query;
|
||||
}
|
||||
};
|
||||
|
||||
// Find all the assets.
|
||||
Promise.all([
|
||||
Asset
|
||||
.search(search)
|
||||
|
||||
// Find the actuall assets.
|
||||
FilterOpenAssets(Asset.search(search), filter)
|
||||
.sort({[field]: (sort === 'asc') ? 1 : -1})
|
||||
.skip(skip)
|
||||
.limit(limit),
|
||||
Asset
|
||||
.search(search)
|
||||
.skip(parseInt(skip))
|
||||
.limit(parseInt(limit)),
|
||||
|
||||
// Get the count of actual assets.
|
||||
FilterOpenAssets(Asset.search(search), filter)
|
||||
.count()
|
||||
])
|
||||
.then(([result, count]) => {
|
||||
@@ -107,7 +136,6 @@ router.put('/:asset_id/status', (req, res, next) => {
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
res.status(204).json();
|
||||
})
|
||||
.catch((err) => {
|
||||
|
||||
+11
-3
@@ -1,7 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# install selenium
|
||||
../node_modules/selenium-standalone/bin/selenium-standalone install
|
||||
selenium-standalone install
|
||||
|
||||
# start the app server
|
||||
npm start &
|
||||
# Creating Admin Test User
|
||||
{ echo admin@test.com; echo test; echo test; echo Admin Test User; echo admin;} | dotenv ./bin/cli-users create
|
||||
|
||||
# Creating Moderator Test User
|
||||
{ echo moderator@test.com; echo test; echo test; echo Moderator Test User; echo moderator;} | dotenv ./bin/cli-users create
|
||||
|
||||
# Creating Commenter Test User
|
||||
{ echo commenter@test.com; echo test; echo test; echo Commenter Test User; echo ;} | dotenv ./bin/cli-users create
|
||||
|
||||
npm start
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'react';
|
||||
import 'redux';
|
||||
import {expect} from 'chai';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import * as actions from '../../../../client/coral-admin/src/actions/assets';
|
||||
import {Map} from 'immutable';
|
||||
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('Asset actions', () => {
|
||||
let store;
|
||||
|
||||
const assets = [
|
||||
{
|
||||
url: 'http://test.com',
|
||||
id: '123',
|
||||
status: 'closed'
|
||||
},
|
||||
{
|
||||
url: 'http://test.org',
|
||||
id: '456',
|
||||
status: 'open'
|
||||
}
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(new Map({}));
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
describe('FETCH_ASSETS_REQUEST', () => {
|
||||
|
||||
it('should fetch a list of assets', () => {
|
||||
|
||||
fetchMock.get('*', JSON.stringify({
|
||||
result: assets,
|
||||
count: 2
|
||||
}));
|
||||
|
||||
return actions.fetchAssets(2, 20)(store.dispatch)
|
||||
.then(() => {
|
||||
expect(store.getActions()[0]).to.have.property('type', 'FETCH_ASSETS_REQUEST');
|
||||
expect(store.getActions()[1]).to.have.property('type', 'FETCH_ASSETS_SUCCESS');
|
||||
expect(store.getActions()[1]).to.have.property('count', 2);
|
||||
expect(store.getActions()[1]).to.have.property('assets').
|
||||
and.to.deep.equal(assets);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error appropriatly', () => {
|
||||
|
||||
fetchMock.get('*', 404);
|
||||
|
||||
return actions.fetchAssets(2, 20)(store.dispatch)
|
||||
.then(() => {
|
||||
expect(store.getActions()[0]).to.have.property('type', 'FETCH_ASSETS_REQUEST');
|
||||
expect(store.getActions()[1]).to.have.property('type', 'FETCH_ASSETS_FAILURE');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('UPDATE_ASSET_STATE_REQUEST', () => {
|
||||
|
||||
it('should update an asset', () => {
|
||||
|
||||
fetchMock.put('*', JSON.stringify(assets[0]));
|
||||
|
||||
return actions.updateAssetState('123', 'status', 'open')(store.dispatch)
|
||||
.then(() => {
|
||||
expect(store.getActions()[0]).to.have.property('type', 'UPDATE_ASSET_STATE_REQUEST');
|
||||
expect(store.getActions()[1]).to.have.property('type', 'UPDATE_ASSET_STATE_SUCCESS');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should return an error appropriately', () => {
|
||||
|
||||
fetchMock.put('*', 404);
|
||||
|
||||
return actions.updateAssetState('123', 'status', 'open')(store.dispatch)
|
||||
.then(() => {
|
||||
expect(store.getActions()[0]).to.have.property('type', 'UPDATE_ASSET_STATE_REQUEST');
|
||||
expect(store.getActions()[1]).to.have.property('type', 'UPDATE_ASSET_STATE_FAILURE');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import {Map, fromJS} from 'immutable';
|
||||
import {expect} from 'chai';
|
||||
import assetsReducer from '../../../../client/coral-admin/src/reducers/assets';
|
||||
|
||||
describe ('assetsReducer', () => {
|
||||
describe('FETCH_ASSETS_SUCCESS', () => {
|
||||
it('should replace the existing assets', () => {
|
||||
const action = {
|
||||
type: 'FETCH_ASSETS_SUCCESS',
|
||||
count: 200,
|
||||
assets: [
|
||||
{
|
||||
id: '123',
|
||||
url: 'http://test.com',
|
||||
closedAt: 'tomorrow'
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
url: 'http://test2.com',
|
||||
closedAt: 'thursday'
|
||||
},
|
||||
]
|
||||
};
|
||||
const store = new Map({});
|
||||
const result = assetsReducer(store, action);
|
||||
expect(result.getIn(['byId', '123']).toJS()).to.deep.equal({
|
||||
url: 'http://test.com',
|
||||
closedAt: 'tomorrow',
|
||||
id: '123'
|
||||
});
|
||||
expect(result.getIn(['ids']).toJS()).to.deep.equal([
|
||||
'123',
|
||||
'456'
|
||||
]);
|
||||
expect(result.getIn(['count'])).to.equal(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UPDATE_ASSET_STATE_REQUEST', () => {
|
||||
it('should update the state of a particular asset', () => {
|
||||
const action = {
|
||||
type: 'UPDATE_ASSET_STATE_REQUEST',
|
||||
id: '123',
|
||||
closedAt: null
|
||||
};
|
||||
const store = new fromJS({
|
||||
byId: {
|
||||
'123': {
|
||||
id: '123',
|
||||
url: 'http://test.com',
|
||||
closedAt: Date.now()
|
||||
},
|
||||
'456': {
|
||||
id: '456',
|
||||
url: 'http://test2.com',
|
||||
closedAt: 'thursday'
|
||||
}
|
||||
}
|
||||
});
|
||||
const result = assetsReducer(store, action);
|
||||
expect(result.getIn(['byId', '123', 'closedAt'])).to.equal.null;
|
||||
});
|
||||
});
|
||||
});
|
||||
+16
-2
@@ -1,4 +1,18 @@
|
||||
module.exports = {
|
||||
waitForConditionTimeout: 20000,
|
||||
baseUrl: 'localhost:3011/'
|
||||
waitForConditionTimeout: 8000,
|
||||
baseUrl: 'http://localhost:3000',
|
||||
users: {
|
||||
admin: {
|
||||
email: 'admin@test.com',
|
||||
pass: 'test'
|
||||
},
|
||||
moderator: {
|
||||
email: 'moderator@test.com',
|
||||
pass: 'test'
|
||||
},
|
||||
commenter: {
|
||||
email: 'commenter@test.com',
|
||||
pass: 'test'
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const embedStreamCommands = {
|
||||
url: function () {
|
||||
return `${this.api.launchUrl}/admin`;
|
||||
},
|
||||
ready() {
|
||||
return this
|
||||
.waitForElementVisible('body', 2000);
|
||||
},
|
||||
approveComment() {
|
||||
return this
|
||||
.waitForElementVisible('@commentList')
|
||||
.waitForElementVisible('@approveButton')
|
||||
.click('@approveButton');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
commands: [embedStreamCommands],
|
||||
elements: {
|
||||
commentList: {
|
||||
selector: '#commentList'
|
||||
},
|
||||
banButton: {
|
||||
selector: '#commentList .actions:first-child .ban'
|
||||
},
|
||||
rejectButton: {
|
||||
selector: '#commentList .actions:first-child .reject'
|
||||
},
|
||||
approveButton: {
|
||||
selector: '#commentList .actions:first-child .approve'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const embedCommands = {
|
||||
ready() {
|
||||
return this.resizeWindow(1200, 800)
|
||||
.url(client.globals.baseUrl)
|
||||
.waitForElementVisible('body', 2000)
|
||||
.frame('coralStreamIframe');
|
||||
},
|
||||
setConfig(config, baseUrl) {
|
||||
return fetch(`${baseUrl}/api/v1/settings`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
},
|
||||
enterComment() {
|
||||
const comment = 'This is a test comment';
|
||||
return this
|
||||
.waitForElementVisible('@commentBox')
|
||||
.setValue('@commentBox', comment)
|
||||
.click('@postButton')
|
||||
.waitForElementVisible('.comment', 1000);
|
||||
},
|
||||
validateComment(comment) {
|
||||
return this
|
||||
.assert.equal(comment, '.comment');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
commands: [embedCommands],
|
||||
elements: {
|
||||
|
||||
commentBox: {
|
||||
selector: '#commentBox'
|
||||
},
|
||||
postButton: {
|
||||
selector: '#commentBox .coral-plugin-commentbox-button'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
const embedStreamCommands = {
|
||||
url: function () {
|
||||
return this
|
||||
.api.launchUrl;
|
||||
},
|
||||
ready() {
|
||||
return this
|
||||
.waitForElementVisible('body', 4000)
|
||||
.waitForElementVisible('iframe#coralStreamIframe')
|
||||
.api.frame('coralStreamIframe');
|
||||
},
|
||||
signUp(user) {
|
||||
return this
|
||||
.waitForElementVisible('@signInButton', 2000)
|
||||
.click('@signInButton')
|
||||
.waitForElementVisible('@signInDialog')
|
||||
.waitForElementVisible('@registerButton')
|
||||
.click('@registerButton')
|
||||
.setValue('@signInDialogEmail', user.email)
|
||||
.setValue('@signInDialogPassword', user.pass)
|
||||
.setValue('@signUpDialogConfirmPassword', user.pass)
|
||||
.setValue('@signUpDialogDisplayName', user.displayName)
|
||||
.waitForElementVisible('@signUpButton')
|
||||
.click('@signUpButton')
|
||||
.waitForElementVisible('@logInButton')
|
||||
.click('@logInButton')
|
||||
.waitForElementVisible('@logoutButton', 5000);
|
||||
},
|
||||
login(user) {
|
||||
return this
|
||||
.waitForElementVisible('@signInButton', 2000)
|
||||
.click('@signInButton')
|
||||
.waitForElementVisible('@signInDialog')
|
||||
.waitForElementVisible('@signInDialogEmail')
|
||||
.waitForElementVisible('@signInDialogPassword')
|
||||
.setValue('@signInDialogEmail', user.email)
|
||||
.setValue('@signInDialogPassword', user.pass)
|
||||
.waitForElementVisible('@logInButton')
|
||||
.click('@logInButton')
|
||||
.waitForElementVisible('@logoutButton', 5000);
|
||||
},
|
||||
logout() {
|
||||
return this
|
||||
.waitForElementVisible('@logoutButton')
|
||||
.click('@logoutButton')
|
||||
.waitForElementVisible('@signInButton', 2000);
|
||||
},
|
||||
postComment(comment = 'Test Comment') {
|
||||
return this
|
||||
.waitForElementVisible('@commentBox', 2000)
|
||||
.setValue('@commentBox', comment)
|
||||
.click('@postButton');
|
||||
},
|
||||
likeComment() {
|
||||
return this
|
||||
.waitForElementVisible('@likeButton')
|
||||
.click('@likeButton');
|
||||
},
|
||||
flagComment() {
|
||||
return this
|
||||
.waitForElementVisible('@flagButton')
|
||||
.click('@flagButton');
|
||||
},
|
||||
flagUsername() {
|
||||
return this
|
||||
.waitForElementVisible('@flagButton')
|
||||
.click('@flagButton');
|
||||
},
|
||||
getPermalink(fn) {
|
||||
return this
|
||||
.waitForElementVisible('@permalinkButton')
|
||||
.click('@permalinkButton')
|
||||
.waitForElementVisible('@permalinkPopUp')
|
||||
.getValue('@permalinkInput', result => fn(result.value));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
commands: [embedStreamCommands],
|
||||
elements: {
|
||||
signInButton: {
|
||||
selector: '#coralSignInButton'
|
||||
},
|
||||
signInDialog:{
|
||||
selector: '#signInDialog'
|
||||
},
|
||||
signInDialogEmail: {
|
||||
selector: '#signInDialog #email'
|
||||
},
|
||||
signInDialogPassword: {
|
||||
selector: '#signInDialog #password'
|
||||
},
|
||||
signUpDialogConfirmPassword: {
|
||||
selector: '#signInDialog #confirmPassword'
|
||||
},
|
||||
signUpDialogDisplayName: {
|
||||
selector: '#signInDialog #displayName'
|
||||
},
|
||||
logInButton: {
|
||||
selector: '#coralLogInButton'
|
||||
},
|
||||
signUpButton: {
|
||||
selector: '#coralSignUpButton'
|
||||
},
|
||||
logoutButton: {
|
||||
selector: '.commentStream #logout'
|
||||
},
|
||||
commentBox: {
|
||||
selector: '.coral-plugin-commentbox-textarea'
|
||||
},
|
||||
postButton: {
|
||||
selector: '#commentBox .coral-plugin-commentbox-button'
|
||||
},
|
||||
likeButton: {
|
||||
selector: '.comment .coral-plugin-likes-container .coral-plugin-likes-button'
|
||||
},
|
||||
likeText: {
|
||||
selector: '.comment .coral-plugin-likes-container .coral-plugin-likes-button .coral-plugin-likes-button-text'
|
||||
},
|
||||
likesCount: {
|
||||
selector: '.comment .coral-plugin-likes-container .coral-plugin-likes-button .coral-plugin-likes-like-count'
|
||||
},
|
||||
flagButton: {
|
||||
selector: '.comment .coral-plugin-flags-container .coral-plugin-flags-button'
|
||||
},
|
||||
flagPopUp: {
|
||||
selector: '.comment .coral-plugin-flags-popup'
|
||||
},
|
||||
flagCommentOption: {
|
||||
selector: '.comment .coral-plugin-flags-popup .coral-plugin-flags-popup-radio-label[for="comments"]'
|
||||
},
|
||||
flagUsernameOption: {
|
||||
selector: '.comment .coral-plugin-flags-popup .coral-plugin-flags-popup-radio-label[for="user"]'
|
||||
},
|
||||
flagOtherOption: {
|
||||
selector: '.comment .coral-plugin-flags-popup .coral-plugin-flags-popup-radio-label[for="other"]'
|
||||
},
|
||||
flagHeaderMessage: {
|
||||
selector: '.comment .coral-plugin-flags-popup .coral-plugin-flags-popup-header'
|
||||
},
|
||||
flagButtonText: {
|
||||
selector: '.comment .coral-plugin-flags-button-text'
|
||||
},
|
||||
flagDoneButton: {
|
||||
selector: '.comment .coral-plugin-flags-popup .coral-plugin-flags-popup-button'
|
||||
},
|
||||
permalinkButton: {
|
||||
selector: '.comment .coral-plugin-permalinks-button'
|
||||
},
|
||||
permalinkPopUp: {
|
||||
selector: '.comment .coral-plugin-permalinks-popover.active'
|
||||
},
|
||||
permalinkInput: {
|
||||
selector: '.comment .coral-plugin-permalinks-popover.active input'
|
||||
},
|
||||
registerButton: {
|
||||
selector: '#signInDialog #coralRegister'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -6,8 +6,5 @@ module.exports = {
|
||||
.url(baseUrl)
|
||||
.assert.title('Coral Talk')
|
||||
.waitForElementPresent('body', 1000);
|
||||
},
|
||||
after: client => {
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
module.exports = {
|
||||
'@tags': ['embedStream'],
|
||||
before: client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
embedStreamPage
|
||||
.navigate()
|
||||
.ready();
|
||||
},
|
||||
'Login as commenter': client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
const {users} = client.globals;
|
||||
embedStreamPage
|
||||
.login(users.commenter);
|
||||
},
|
||||
'Add test comment': client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
embedStreamPage
|
||||
.postComment('Test Comment');
|
||||
},
|
||||
'Logout': client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
embedStreamPage
|
||||
.logout();
|
||||
},
|
||||
'Login as admin': client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
const {users} = client.globals;
|
||||
embedStreamPage
|
||||
.login(users.admin);
|
||||
},
|
||||
'Approve test comment': client => {
|
||||
const adminPage = client.page.adminPage();
|
||||
|
||||
adminPage
|
||||
.navigate()
|
||||
.ready();
|
||||
|
||||
adminPage
|
||||
.approveComment();
|
||||
},
|
||||
after: client => {
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
module.exports = {
|
||||
'@tags': ['login', 'admin'],
|
||||
before(client) {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
const {launchUrl} = client;
|
||||
|
||||
client
|
||||
.url(launchUrl);
|
||||
|
||||
embedStreamPage
|
||||
.ready();
|
||||
},
|
||||
'Admin logs in': client => {
|
||||
const {users} = client.globals;
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
|
||||
embedStreamPage
|
||||
.login(users.admin);
|
||||
},
|
||||
after: client => {
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
module.exports = {
|
||||
'@tags': ['flag', 'comments', 'commenter'],
|
||||
before: client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
const {users} = client.globals;
|
||||
|
||||
embedStreamPage
|
||||
.navigate()
|
||||
.ready();
|
||||
|
||||
embedStreamPage
|
||||
.login(users.commenter);
|
||||
},
|
||||
'Commenter flags a comment': client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
|
||||
embedStreamPage
|
||||
.flagComment()
|
||||
.waitForElementVisible('@flagPopUp')
|
||||
.waitForElementVisible('@flagCommentOption')
|
||||
.click('@flagCommentOption')
|
||||
.waitForElementVisible('@flagDoneButton')
|
||||
.click('@flagDoneButton')
|
||||
.waitForElementVisible('@flagOtherOption')
|
||||
.click('@flagOtherOption')
|
||||
.waitForElementVisible('@flagDoneButton')
|
||||
.click('@flagDoneButton')
|
||||
.click('@flagDoneButton')
|
||||
.expect.element('@flagButtonText').text.to.equal('Reported');
|
||||
},
|
||||
after: client => {
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
module.exports = {
|
||||
'@tags': ['flag', 'commenter'],
|
||||
before: client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
const {users} = client.globals;
|
||||
|
||||
embedStreamPage
|
||||
.navigate()
|
||||
.ready();
|
||||
|
||||
embedStreamPage
|
||||
.login(users.commenter);
|
||||
},
|
||||
'Commenter flags a username': client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
|
||||
embedStreamPage
|
||||
.flagUsername()
|
||||
.waitForElementVisible('@flagPopUp')
|
||||
.waitForElementVisible('@flagUsernameOption')
|
||||
.click('@flagUsernameOption')
|
||||
.waitForElementVisible('@flagDoneButton')
|
||||
.click('@flagDoneButton')
|
||||
.waitForElementVisible('@flagOtherOption')
|
||||
.click('@flagOtherOption')
|
||||
.waitForElementVisible('@flagDoneButton')
|
||||
.click('@flagDoneButton')
|
||||
.click('@flagDoneButton');
|
||||
},
|
||||
after: client => {
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
module.exports = {
|
||||
'@tags': ['like', 'comments', 'commenter'],
|
||||
before: client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
const {users} = client.globals;
|
||||
|
||||
embedStreamPage
|
||||
.navigate()
|
||||
.ready();
|
||||
|
||||
embedStreamPage
|
||||
.login(users.commenter);
|
||||
},
|
||||
'Commenter likes a comment': client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
|
||||
embedStreamPage
|
||||
.likeComment()
|
||||
.waitForElementVisible('@likesCount', 2000)
|
||||
.expect.element('@likeText').text.to.equal('Liked');
|
||||
|
||||
},
|
||||
after: client => {
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
'@tags': ['login', 'commenter'],
|
||||
before: client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
|
||||
embedStreamPage
|
||||
.navigate()
|
||||
.ready();
|
||||
},
|
||||
'Commenter logs in': client => {
|
||||
const {users} = client.globals;
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
|
||||
embedStreamPage
|
||||
.login(users.commenter);
|
||||
},
|
||||
after: client => {
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
let permalink = '';
|
||||
|
||||
module.exports = {
|
||||
'@tags': ['permalink', 'commenter'],
|
||||
before: client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
const {users} = client.globals;
|
||||
|
||||
embedStreamPage
|
||||
.navigate()
|
||||
.ready();
|
||||
|
||||
embedStreamPage
|
||||
.login(users.commenter);
|
||||
},
|
||||
'Commenter gets the permalink of a comment': client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
embedStreamPage
|
||||
.getPermalink(value => {
|
||||
permalink = value;
|
||||
});
|
||||
},
|
||||
'Commenter navigates to the permalink': client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
embedStreamPage
|
||||
.navigate(permalink);
|
||||
|
||||
client.assert.urlContains(permalink);
|
||||
},
|
||||
after: client => {
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
module.exports = {
|
||||
'@tags': ['write', 'commenter'],
|
||||
before: client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
const {users} = client.globals;
|
||||
|
||||
embedStreamPage
|
||||
.navigate()
|
||||
.ready();
|
||||
|
||||
embedStreamPage
|
||||
.login(users.commenter);
|
||||
},
|
||||
'Commenter posts a comment': client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
|
||||
embedStreamPage
|
||||
.postComment('I read the comments');
|
||||
},
|
||||
after: client => {
|
||||
const adminPage = client.page.adminPage();
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
const {users} = client.globals;
|
||||
|
||||
embedStreamPage
|
||||
.logout()
|
||||
.login(users.admin);
|
||||
|
||||
adminPage
|
||||
.navigate()
|
||||
.ready();
|
||||
|
||||
adminPage
|
||||
.approveComment();
|
||||
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
module.exports = {
|
||||
'@tags': ['login', 'moderator'],
|
||||
before: client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
const {launchUrl} = client;
|
||||
|
||||
client
|
||||
.url(launchUrl);
|
||||
|
||||
embedStreamPage
|
||||
.ready();
|
||||
},
|
||||
'Moderator logs in': client => {
|
||||
const {users} = client.globals;
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
|
||||
embedStreamPage
|
||||
.login(users.moderator);
|
||||
},
|
||||
after: client => {
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
'@tags': ['flag', 'comments', 'visitor'],
|
||||
before: client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
|
||||
embedStreamPage
|
||||
.navigate()
|
||||
.ready();
|
||||
},
|
||||
'Visitor tries to flag a comment': client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
|
||||
embedStreamPage
|
||||
.flagComment()
|
||||
.waitForElementVisible('@signInDialog', 2000);
|
||||
},
|
||||
after: client => {
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
'@tags': ['like', 'comments', 'visitor'],
|
||||
before: client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
|
||||
embedStreamPage
|
||||
.navigate()
|
||||
.ready();
|
||||
},
|
||||
'Visitor tries to like a comment': client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
|
||||
embedStreamPage
|
||||
.likeComment()
|
||||
.waitForElementVisible('@signInDialog', 2000);
|
||||
},
|
||||
after: client => {
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
'@tags': ['signup', 'visitor'],
|
||||
before: client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
|
||||
embedStreamPage
|
||||
.navigate()
|
||||
.ready();
|
||||
},
|
||||
'Visitor signs up': client => {
|
||||
const embedStreamPage = client.page.embedStreamPage();
|
||||
const hash = Math.floor(Math.random() * (999 - 0));
|
||||
|
||||
embedStreamPage
|
||||
.signUp({
|
||||
email: `visitor_${hash}@test.com`,
|
||||
displayName: 'Visitor',
|
||||
pass: 'testtest'
|
||||
});
|
||||
},
|
||||
after: client => {
|
||||
client.end();
|
||||
}
|
||||
};
|
||||
@@ -18,12 +18,13 @@ describe('/api/v1/assets', () => {
|
||||
url: 'https://coralproject.net/news/asset1',
|
||||
title: 'Asset 1',
|
||||
description: 'term1',
|
||||
id: '1'
|
||||
closedAt: Date.now()
|
||||
},
|
||||
{
|
||||
url: 'https://coralproject.net/news/asset2',
|
||||
title: 'Asset 2',
|
||||
description: 'term2'
|
||||
description: 'term2',
|
||||
closedAt: null
|
||||
}
|
||||
]);
|
||||
});
|
||||
@@ -81,6 +82,38 @@ describe('/api/v1/assets', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return only closed assets', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/assets?filter=closed')
|
||||
.set(passport.inject({roles: ['admin']}))
|
||||
.then((res) => {
|
||||
const body = res.body;
|
||||
|
||||
expect(body).to.have.property('count', 1);
|
||||
expect(body).to.have.property('result');
|
||||
|
||||
const assets = body.result;
|
||||
|
||||
expect(assets[0]).to.have.property('title', 'Asset 1');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return only opened assets', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/assets?filter=open')
|
||||
.set(passport.inject({roles: ['admin']}))
|
||||
.then((res) => {
|
||||
const body = res.body;
|
||||
|
||||
expect(body).to.have.property('count', 1);
|
||||
expect(body).to.have.property('result');
|
||||
|
||||
const assets = body.result;
|
||||
|
||||
expect(assets[0]).to.have.property('title', 'Asset 2');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#put', () => {
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@
|
||||
<script type='text/javascript' src='<%= basePath %>/pym.v1.min.js'></script>
|
||||
<script>
|
||||
var ready = false;
|
||||
var pymParent = new pym.Parent('coralStreamEmbed', '/embed/stream', {title: 'Talk Comments', id:'coralStreamIframe'});
|
||||
var pymParent = new pym.Parent('coralStreamEmbed', '/embed/stream', {title: 'Talk Comments', id:'coralStreamIframe', name: 'coralStreamIframe'});
|
||||
pymParent.onMessage('height', function(height) {document.querySelector('#coralStreamEmbed iframe').height = height + 'px'})
|
||||
pymParent.onMessage('childReady', function () {
|
||||
var interval = setInterval(function () {
|
||||
|
||||
Reference in New Issue
Block a user