Merge branch 'master' of github.com:coralproject/talk into moderate-bio

Integrating graphql, updating tests, and preserving transition to ModerationList from CommentList.
This commit is contained in:
David Jay
2017-01-25 17:15:12 -05:00
212 changed files with 5589 additions and 3845 deletions
+1
View File
@@ -1 +1,2 @@
node_modules
test
+1 -1
View File
@@ -17,7 +17,7 @@
"no-template-curly-in-string": [1],
"no-unsafe-negation": [1],
"array-callback-return": [1],
"eqeqeq": [2],
"eqeqeq": [2, "smart"],
"no-eval": [2],
"no-global-assign": [2],
"no-implied-eval": [2],
+2 -2
View File
@@ -3,13 +3,13 @@ npm-debug.log*
dist
!dist/coral-admin
dist/coral-admin/bundle.js
tests/e2e/reports
test/e2e/reports
.DS_Store
*.iml
*.swp
dump.rdb
.env
gaba.cfg
*.cfg
.idea/
coverage/
yarn.lock
+4
View File
@@ -0,0 +1,4 @@
{
"verbose": true,
"ignore": ["test/*", "client/*", "dist/*"]
}
-3
View File
@@ -1,3 +0,0 @@
language: node_js
node_js:
- 4
+20 -1
View File
@@ -10,6 +10,8 @@ const RedisStore = require('connect-redis')(session);
const redis = require('./services/redis');
const csrf = require('csurf');
const errors = require('./errors');
const graph = require('./graph');
const apollo = require('graphql-server-express');
const app = express();
@@ -49,7 +51,7 @@ const session_opts = {
name: 'talk.sid',
cookie: {
secure: false,
maxAge: 18000000, // 30 minutes for expiry.
maxAge: 36000000, // 1 hour for expiry.
},
store: new RedisStore({
ttl: 1800,
@@ -77,6 +79,23 @@ app.use(session(session_opts));
app.use(passport.initialize());
app.use(passport.session());
//==============================================================================
// GraphQL Router
//==============================================================================
// GraphQL endpoint.
app.use('/api/v1/graph/ql', apollo.graphqlExpress(graph.createGraphOptions));
// Only include the graphiql tool if we aren't in production mode.
if (app.get('env') !== 'production') {
// Interactive graphiql interface.
app.use('/api/v1/graph/iql', apollo.graphiqlExpress({
endpointURL: '/api/v1/graph/ql'
}));
}
//==============================================================================
// CSRF MIDDLEWARE
//==============================================================================
+3 -3
View File
@@ -8,7 +8,7 @@ const program = require('commander');
const pkg = require('../package.json');
const parseDuration = require('parse-duration');
const Table = require('cli-table');
const Asset = require('../models/asset');
const AssetModel = require('../models/asset');
const mongoose = require('../services/mongoose');
const scraper = require('../services/scraper');
const util = require('../util');
@@ -22,7 +22,7 @@ util.onshutdown([
* Lists all the assets registered in the database.
*/
function listAssets() {
Asset
AssetModel
.find({})
.sort({'created_at': 1})
.then((asset) => {
@@ -56,7 +56,7 @@ function refreshAssets(ageString) {
const ageMs = parseDuration(ageString);
const age = new Date(now - ageMs);
Asset.find({
AssetModel.find({
$or: [
{
scraped: {
+10 -4
View File
@@ -6,7 +6,7 @@
const program = require('commander');
const mongoose = require('../services/mongoose');
const Setting = require('../models/setting');
const SettingsService = require('../services/settings');
const util = require('../util');
// Register the shutdown criteria.
@@ -22,10 +22,16 @@ program
.command('init')
.description('initilizes the talk settings')
.action(() => {
const defaults = {id: '1', moderation: 'pre'};
const defaults = {
moderation: 'PRE',
wordlist: {
banned: [],
suspect: []
}
};
Setting
.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true})
SettingsService
.init(defaults)
.then(() => {
console.log('Created settings object.');
util.shutdown();
+26 -19
View File
@@ -7,7 +7,8 @@
const program = require('commander');
const pkg = require('../package.json');
const prompt = require('prompt');
const User = require('../models/user');
const UsersService = require('../services/users');
const UserModel = require('../models/user');
const mongoose = require('../services/mongoose');
const util = require('../util');
const Table = require('cli-table');
@@ -76,16 +77,22 @@ function createUser(options) {
});
})
.then((result) => {
return User.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim())
return UsersService.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim())
.then((user) => {
console.log(`Created user ${user.id}.`);
if (result.role && result.role.length > 0) {
return User
.addRoleToUser(user.id, result.role.trim())
let role = result.role ? result.role.trim() : null;
if (role && role.length > 0) {
return UsersService
.addRoleToUser(user.id, role)
.then(() => {
console.log(`Added the admin ${result.role.trim()} to User ${user.id}.`);
util.shutdown();
})
.catch((err) => {
console.error(err);
util.shutdown();
});
} else {
util.shutdown();
@@ -102,7 +109,7 @@ function createUser(options) {
* Deletes a user.
*/
function deleteUser(userID) {
User
UserModel
.findOneAndRemove({
id: userID
})
@@ -148,7 +155,7 @@ function passwd(userID) {
return;
}
User
UsersService
.changePassword(userID, result.password)
.then(() => {
console.log('Password changed.');
@@ -168,7 +175,7 @@ function updateUser(userID, options) {
const updates = [];
if (options.email && typeof options.email === 'string' && options.email.length > 0) {
let q = User.update({
let q = UserModel.update({
'id': userID,
'profiles.provider': 'local'
}, {
@@ -181,7 +188,7 @@ function updateUser(userID, options) {
}
if (options.name && typeof options.name === 'string' && options.name.length > 0) {
let q = User.update({
let q = UserModel.update({
'id': userID
}, {
$set: {
@@ -208,7 +215,7 @@ function updateUser(userID, options) {
* Lists all the users registered in the database.
*/
function listUsers() {
User
UsersService
.all()
.then((users) => {
let table = new Table({
@@ -248,7 +255,7 @@ function listUsers() {
* @param {String} srcUserID id of the user to which is the source of the merge
*/
function mergeUsers(dstUserID, srcUserID) {
User
UsersService
.mergeUsers(dstUserID, srcUserID)
.then(() => {
console.log(`User ${srcUserID} was merged into user ${dstUserID}.`);
@@ -266,7 +273,7 @@ function mergeUsers(dstUserID, srcUserID) {
* @param {String} role the role to add
*/
function addRole(userID, role) {
User
UsersService
.addRoleToUser(userID, role)
.then(() => {
console.log(`Added the ${role} role to User ${userID}.`);
@@ -284,7 +291,7 @@ function addRole(userID, role) {
* @param {String} role the role to remove
*/
function removeRole(userID, role) {
User
UsersService
.removeRoleFromUser(userID, role)
.then(() => {
console.log(`Removed the ${role} role from User ${userID}.`);
@@ -301,8 +308,8 @@ function removeRole(userID, role) {
* @param {String} userID id of the user to ban
*/
function ban(userID) {
User
.setStatus(userID, 'banned', '')
UsersService
.setStatus(userID, 'BANNED')
.then(() => {
console.log(`Banned the User ${userID}.`);
util.shutdown();
@@ -318,8 +325,8 @@ function ban(userID) {
* @param {String} userUD id of the user to remove the role from
*/
function unban(userID) {
User
.setStatus(userID, 'active', '')
UsersService
.setStatus(userID, 'ACTIVE')
.then(() => {
console.log(`Unban the User ${userID}.`);
util.shutdown();
@@ -335,7 +342,7 @@ function unban(userID) {
* @param {String} userID the ID of a user to disable
*/
function disableUser(userID) {
User
UsersService
.disableUser(userID)
.then(() => {
console.log(`User ${userID} was disabled.`);
@@ -352,7 +359,7 @@ function disableUser(userID) {
* @param {String} userID the ID of a user to enable
*/
function enableUser(userID) {
User
UsersService
.enableUser(userID)
.then(() => {
console.log(`User ${userID} was enabled.`);
+15 -3
View File
@@ -3,12 +3,24 @@ machine:
version: 7
services:
- docker
- redis
dependencies:
post:
# Lint the project here, before tests are ran.
- npm run lint
database:
post:
# Initialize the settings in the database, this will create indicies for the
# database.
- ./bin/cli settings init
- sleep 2
test:
override:
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml ./node_modules/.bin/mocha tests --reporter mocha-junit-reporter
- npm run lint
- npm run build
# Run the tests using the junit reporter.
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml NPM_PACKAGE_CONFIG_MOCHA_REPORTER=mocha-junit-reporter npm run test
deployment:
release:
+1 -1
View File
@@ -11,7 +11,7 @@ export const checkLogin = () => dispatch => {
dispatch(checkLoginRequest());
coralApi('/auth')
.then(result => {
const isAdmin = !!result.user.roles.filter(i => i === 'admin').length;
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
dispatch(checkLoginSuccess(result.user, isAdmin));
})
.catch(error => {
+3 -3
View File
@@ -65,9 +65,9 @@ export const fetchFlaggedQueue = () => {
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
return coralApi('/queue/comments/flagged')
.then(comments => {
comments.forEach(comment => comment.flagged = true);
return comments;
.then(results => {
results.comments.forEach(comment => comment.flagged = true);
return results;
})
.then(addUsersCommentsActions.bind(this, dispatch));
};
@@ -1,17 +1,25 @@
.dialog {
border: none;
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
width: 280px;
top: 10px;
}
border: none;
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
width: 500px;
top: 50%;
transform: translateY(-50%);
height: 184px;
padding: 20px;
.header {
margin-bottom: 20px;
}
h2 {
color: black;
font-size: 1.76em;
font-weight: 500;
margin: 0;
}
.header h1, .separator h1{
text-align: center;
font-size: 1.2em;
h3 {
color: black;
font-size: 1.4em;
font-weight: 500;
margin: 0;
}
}
.formField {
@@ -143,5 +151,14 @@ input.error{
}
.cancel {
margin: 10px 0;
margin-right: 10px;
width: 47%;
}
.ban {
width: 47%;
}
.buttons {
margin: 20px 0;
}
@@ -19,23 +19,23 @@ const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => (
<span className={styles.close} onClick={handleClose}>×</span>
<div>
<div className={styles.header}>
<h3>
<h2>
{lang.t('bandialog.ban_user')}
</h3>
</h2>
</div>
<div className={styles.separator}>
<h4>
<h3>
{lang.t('bandialog.are_you_sure', user.userName)}
</h4>
</h3>
<i>
{lang.t('bandialog.note')}
</i>
</div>
<div className={styles.buttons}>
<Button cStyle="cancel" className={styles.cancel} onClick={() => handleClose()} full>
<Button cStyle="cancel" className={styles.cancel} onClick={() => handleClose()} raised>
{lang.t('bandialog.cancel')}
</Button>
<Button cStyle="black" onClick={() => onClickBanUser('banned', user.userId, user.commentId)} full>
<Button cStyle="black" className={styles.ban} onClick={() => onClickBanUser(user.userId, user.commentId)} raised>
{lang.t('bandialog.yes_ban_user')}
</Button>
</div>
+16 -16
View File
@@ -7,9 +7,8 @@ import styles from './ModerationList.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
import {Icon} from 'react-mdl';
import Highlighter from 'react-highlight-words';
import {FabButton, Button} from 'coral-ui';
import {FabButton, Button, Icon} from 'coral-ui';
const linkify = new Linkify();
@@ -20,22 +19,19 @@ const Comment = props => {
const links = linkify.getMatches(comment.body);
return (
<li tabIndex={props.index} className={`${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
<li tabIndex={props.index} className={`mdl-card mdl-shadow--2dp ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<i className={`material-icons ${styles.avatar}`}>person</i>
<span>{author.displayName || lang.t('comment.anon')}</span>
<span className={styles.created}>{timeago().format(comment.createdAt || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}</span>
{comment.flagged ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
</div>
<div>
<div className={styles.sideActions}>
{links ?
<span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
<div className={`actions ${styles.actions}`}>
{props.modActions.map((action, i) => getActionButton(action, i, props))}
</div>
</div>
<div>
{authorStatus === 'banned' ?
<span className={styles.banned}><Icon name='error_outline'/> {lang.t('comment.banned_user')}</span> : null}
</div>
@@ -65,15 +61,19 @@ const getActionButton = (option, i, props) => {
}
if (option === 'ban') {
return (
<Button
className='ban'
cStyle='black'
disabled={banned ? 'disabled' : ''}
onClick={() => props.onClickShowBanDialog(author.id, author.displayName, comment.id)}
key={i}
>
{lang.t('comment.ban_user')}
</Button>
<div className={styles.ban}>
<Button
className={`ban ${styles.banButton}`}
cStyle='darkGrey'
disabled={banned ? 'disabled' : ''}
onClick={() => props.onClickShowBanDialog(author.id, author.displayName, comment.id)}
key={i}
raised
>
<Icon name='not_interested' className={styles.banIcon} />
{lang.t('comment.ban_user')}
</Button>
</div>
);
}
const menuOption = menuOptionsMap[option];
@@ -36,13 +36,33 @@
.listItem {
border-bottom: 1px solid #e0e0e0;
padding: 16px;
font-size: 16px;
width: 100%;
max-width: 660px;
min-width: 400px;
margin: 0 auto;
padding: 16px 14px;
position: relative;
transition: box-shadow 200ms;
&:hover {
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
&:last-child {
border-bottom: none;
}
.sideActions {
position: absolute;
right: 0;
height: 100%;
top: 0;
padding: 40px 18px;
box-sizing: border-box;
}
.itemHeader {
display: flex;
align-items: center;
@@ -72,25 +92,20 @@
.created {
color: #666;
font-size: 10px;
margin-left: 10px;
font-size: 13px;
margin-left: 40px;
}
.actionButton {
transform: scale(.7);
transform: scale(.8);
margin: 0;
}
.body {
margin-top: 20px;
flex: 1;
font-size: 1em;
color: rgba(0,0,0,.54);
}
.actions {
margin-left: 10px;
display: flex;
font-size: 0.88em;
color: black;
}
.flagged {
@@ -150,3 +165,20 @@
margin-right: 5px;
}
}
.ban {
display: block;
text-align: center;
margin-top: 5px;
}
.banButton {
width: 114px;
letter-spacing: 1px;
i {
vertical-align: middle;
margin-right: 10px;
font-size: 14px;
}
}
+41 -17
View File
@@ -1,40 +1,42 @@
.header {
background: #505050;
background-color: transparent;
box-shadow: none;
min-height: 58px;
}
.header > div {
position: relative;
padding: 0;
width: 1170px;
margin: 0 auto;
}
.active {
background: #232323;
background-color: #696969;
position: relative;
padding: 0;
min-width: 1280px;
margin: 0 auto;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
height: 58px;
}
.rightPanel {
position: absolute;
right: 0;
width: 170px;
position: absolute;
right: 0;
width: 170px;
height: 100%;
}
.rightPanel ul {
list-style: none;
line-height: 38px;
margin-right: 20px;
}
.rightPanel li {
display: inline-block;
float: right;
margin-left: 15px;
font-size: 15px;
font-weight: 500;
line-height: 33px;
}
.rightPanel .settings {
vertical-align: middle;
border-radius: 3px;
border: solid 1px #9e9e9e;
line-height: 10px;
line-height: 0;
}
.rightPanel .settings > div {
@@ -45,3 +47,25 @@
background: rgba(158, 158, 158, 0.69);
cursor: pointer;
}
.navLink {
padding: 0 20px;
font-size: 15px;
font-weight: 500;
background-color: transparent;
transition: background-color 200ms;
&.active {
background-color: #232323;
}
}
.nav {
overflow: hidden;
height: 58px !important;
}
.nav .navLink {
padding: 0 20px;
letter-spacing: 0.4px;
}
@@ -9,7 +9,7 @@ import {Logo} from './Logo';
export default ({handleLogout}) => (
<Header className={styles.header}>
<Logo />
<Navigation>
<Navigation className={styles.nav}>
<IndexLink className={styles.navLink} to="/admin"
activeClassName={styles.active}>{lang.t('configure.moderate')}</IndexLink>
<Link className={styles.navLink} to="/admin/community"
@@ -1,4 +1,6 @@
.layout {
max-width: 1170px;
max-width: 1280px;
margin: 0 auto;
}
overflow: hidden;
background-color: #FAFAFA;
}
+19 -12
View File
@@ -1,21 +1,28 @@
.logo h1 {
color: #272727;
font-size: 20px;
margin: 0;
line-height: 60px;
padding: 0 20px;
color: #272727;
font-size: 20px;
margin: 0;
line-height: 60px;
padding-left: 13px;
margin-top: -4px;
}
.logo span {
display: inline-block;
margin-left: 10px;
font-size: 18px;
vertical-align: middle;
display: inline-block;
margin-left: 10px;
font-size: 18px;
vertical-align: middle;
font-weight: 500;
}
.logo {
background: #E5E5E5;
height: 100%;
background: #E5E5E5;
height: 100%;
width: 128px;
}
.base {
stroke: #E5E5E5;
height: 35px;
width: 35px;
}
+1 -1
View File
@@ -5,7 +5,7 @@ import {CoralLogo} from 'coral-ui';
export const Logo = () => (
<div className={styles.logo}>
<h1>
<CoralLogo stroke="#E5E5E5" />
<CoralLogo className={styles.base} />
<span>Talk</span>
</h1>
</div>
@@ -1,17 +1,96 @@
.roleButton {
display: block;
.container {
padding: 10px;
display: flex;
padding-bottom: 200px;
}
.searchInput {
display: block;
padding-left: 40px;
width: auto;
.leftColumn {
padding: 42px 56px;
width: 234px;
}
.mainContent {
width: calc(100% - 300px);
padding: 34px 14px;
box-sizing: border-box;
}
.roleButton {
display: block;
}
.searchBox {
background: white;
width: 100%;
padding: 9px;
border: 1px solid #ccc;
border-radius: 2px;
display: flex;
background: white;
box-sizing: border-box;
height: 40px;
i {
color: #A1A1A1
}
input {
display: block;
width: 100%;
height: 100%;
border: none;
font-size: 16px;
padding: 0 2px 0 15px;
box-sizing: border-box;
}
}
.email {
display: block;
display: block;
}
.dataTable {
width: 100%;
border-left: none;
border-right: none;
th {
font-size: 1.1em;
}
th:nth-child(2), th:nth-child(3) {
width: 100px;
}
}
.selectField {
position: relative;
width: 150px;
height: 36px;
background: #2c2c2c;
padding: 10px 15px;
box-sizing: border-box;
color: white;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
> div {
padding: 0;
}
i {
position: absolute;
top: 7px;
right: 7px;
}
input {
padding: 0;
font-size: 13px;
letter-spacing: 0.7px;
font-weight: 400;
}
&:hover {
cursor: pointer;
}
}
@@ -1,13 +1,12 @@
import React from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import {Grid, Cell} from 'react-mdl';
import styles from './Community.css';
import Table from './Table';
import Loading from './Loading';
import NoResults from './NoResults';
import Pager from 'coral-ui/components/Pager';
import {Pager} from 'coral-ui';
const lang = new I18n(translations);
@@ -33,17 +32,17 @@ const tableHeaders = [
const Community = ({isFetching, commenters, ...props}) => {
const hasResults = !isFetching && !!commenters.length;
return (
<Grid>
<Cell col={2}>
<div className={styles.container}>
<div className={styles.leftColumn}>
<form action="">
<div className={`mdl-textfield ${styles.searchBox}`}>
<label className="mdl-button mdl-js-button mdl-button--icon" htmlFor="commenters-search">
<div className={`${styles.searchBox}`}>
<label htmlFor="commenters-search">
<i className="material-icons">search</i>
</label>
<div className="">
<input
id="commenters-search"
className={`mdl-textfield__input ${styles.searchInput}`}
className={`${styles.searchInput}`}
type="text"
value={props.searchValue}
onKeyDown={props.onKeyDownHandler}
@@ -52,8 +51,8 @@ const Community = ({isFetching, commenters, ...props}) => {
</div>
</div>
</form>
</Cell>
<Cell col={6}>
</div>
<div className={styles.mainContent}>
{ isFetching && <Loading /> }
{ !hasResults && <NoResults /> }
{ hasResults &&
@@ -68,8 +67,8 @@ const Community = ({isFetching, commenters, ...props}) => {
page={props.page}
onNewPageHandler={props.onNewPageHandler}
/>
</Cell>
</Grid>
</div>
</div>
);
};
@@ -52,19 +52,21 @@ class Table extends Component {
</td>
<td className="mdl-data-table__cell--non-numeric">
<SelectField label={'Select me'} value={row.status || ''}
className={styles.selectField}
label={lang.t('community.status')}
onChange={status => this.onCommenterStatusChange(row.id, status)}>
<Option value={'active'}>{lang.t('community.active')}</Option>
<Option value={'banned'}>{lang.t('community.banned')}</Option>
<Option value={'ACTIVE'}>{lang.t('community.active')}</Option>
<Option value={'BANNED'}>{lang.t('community.banned')}</Option>
</SelectField>
</td>
<td className="mdl-data-table__cell--non-numeric">
<SelectField label={'Select me'} value={row.roles[0] || ''}
className={styles.selectField}
label={lang.t('community.role')}
onChange={role => this.onRoleChange(row.id, role)}>
<Option value={''}>.</Option>
<Option value={'moderator'}>{lang.t('community.moderator')}</Option>
<Option value={'admin'}>{lang.t('community.admin')}</Option>
<Option value={'MODERATOR'}>{lang.t('community.moderator')}</Option>
<Option value={'ADMIN'}>{lang.t('community.admin')}</Option>
</SelectField>
</td>
</tr>
@@ -3,15 +3,8 @@ import {SelectField, Option} from 'react-mdl-selectfield';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import styles from './Configure.css';
import {
List,
ListItem,
ListItemContent,
ListItemAction,
Textfield,
Checkbox,
Icon
} from 'react-mdl';
import {Textfield, Checkbox} from 'react-mdl';
import {Card, Icon, Spinner} from 'coral-ui';
const TIMESTAMPS = {
weeks: 60 * 60 * 24 * 7,
@@ -35,7 +28,7 @@ const updateCharCount = (updateSettings, settingsError) => (event) => {
};
const updateModeration = (updateSettings, mod) => () => {
const moderation = mod === 'pre' ? 'post' : 'pre';
const moderation = mod === 'PRE' ? 'POST' : 'PRE';
updateSettings({moderation});
};
@@ -71,35 +64,32 @@ const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => {
const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError, settings, errors}) => {
if (fetchingSettings) {
/* maybe a spinner here at some point */
return <p>Loading settings...</p>;
return <Card shadow="4"><Spinner/>Loading settings...</Card>;
}
return (
<div>
<div className={styles.commentSettingsSection}>
<h3>{title}</h3>
<List>
<ListItem className={`${styles.configSetting} ${settings.moderation === 'pre' ? styles.enabledSetting : styles.disabledSetting}`}>
<ListItemAction>
<Card className={`${styles.configSetting} ${settings.moderation === 'PRE' ? styles.enabledSetting : styles.disabledSetting}`}>
<div className={styles.action}>
<Checkbox
onChange={updateModeration(updateSettings, settings.moderation)}
checked={settings.moderation === 'pre'} />
</ListItemAction>
<ListItemContent>
checked={settings.moderation === 'PRE'} />
</div>
<div className={styles.content}>
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
<p className={settings.moderation === 'pre' ? '' : styles.disabledSettingText}>
<p className={settings.moderation === 'PRE' ? '' : styles.disabledSettingText}>
{lang.t('configure.enable-pre-moderation-text')}
</p>
</ListItemContent>
</ListItem>
<ListItem className={`${styles.configSetting} ${settings.charCountEnable ? styles.enabledSetting : styles.disabledSetting}`}>
<ListItemAction>
</div>
</Card>
<Card className={`${styles.configSetting} ${settings.charCountEnable ? styles.enabledSetting : styles.disabledSetting}`}>
<div className={styles.action}>
<Checkbox
onChange={updateCharCountEnable(updateSettings, settings.charCountEnable)}
checked={settings.charCountEnable} />
</ListItemAction>
<ListItemContent>
</div>
<div className={styles.content}>
<div className={styles.settingsHeader}>{lang.t('configure.comment-count-header')}</div>
<p className={settings.charCountEnable ? '' : styles.disabledSettingText}>
<span>{lang.t('configure.comment-count-text-pre')}</span>
@@ -118,32 +108,44 @@ const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError
</span>
}
</p>
</ListItemContent>
</ListItem>
<ListItem threeLine className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? styles.enabledSetting : styles.disabledSetting}`}>
<ListItemAction>
</div>
</Card>
<Card className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? styles.enabledSetting : styles.disabledSetting}`}>
<div className={styles.action}>
<Checkbox
onChange={updateInfoBoxEnable(updateSettings, settings.infoBoxEnable)}
checked={settings.infoBoxEnable} />
</ListItemAction>
<ListItemContent>
</div>
<div className={styles.content}>
{lang.t('configure.include-comment-stream')}
<p>
{lang.t('configure.include-comment-stream-desc')}
</p>
</ListItemContent>
</ListItem>
<ListItem className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? null : styles.hidden}`} >
<ListItemContent>
<div className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? null : styles.hidden}`} >
<div className={styles.content}>
<Textfield
onChange={updateInfoBoxContent(updateSettings)}
value={settings.infoBoxContent}
label={lang.t('configure.include-text')}
rows={3}/>
</div>
</div>
</div>
</Card>
<Card className={styles.configSettingInfoBox}>
<div className={styles.content}>
{lang.t('configure.closed-comments-desc')}
<div>
<Textfield
onChange={updateInfoBoxContent(updateSettings)}
value={settings.infoBoxContent}
label={lang.t('configure.include-text')}
onChange={updateClosedMessage(updateSettings)}
value={settings.closedMessage}
label={lang.t('configure.closed-comments-label')}
rows={3}/>
</ListItemContent>
</ListItem>
<ListItem className={styles.configSettingInfoBox}>
<ListItemContent>
</div>
</div>
</Card>
<Card className={`${styles.configSettingInfoBox}`}>
<div className={styles.content}>
{lang.t('configure.close-after')}
<br />
<Textfield
@@ -163,19 +165,8 @@ const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError
<Option value={'weeks'}>{lang.t('configure.weeks')}</Option>
</SelectField>
</div>
</ListItemContent>
</ListItem>
<ListItem className={styles.configSettingInfoBox}>
<ListItemContent>
{lang.t('configure.closed-comments-desc')}
<Textfield
onChange={updateClosedMessage(updateSettings)}
value={settings.closedMessage}
label={lang.t('configure.closed-comments-label')}
rows={3}/>
</ListItemContent>
</ListItem>
</List>
</div>
</Card>
</div>
);
};
@@ -1,26 +1,29 @@
.container {
padding: 10px;
display: flex;
h3 {
color: black;
font-size: 1.76em;
font-weight: 500;
}
}
.leftColumn {
width: 300px;
padding: 42px 56px;
width: 234px;
}
.mainContent {
width: calc(70% - 300px)
}
.settingOption {
cursor: pointer;
width: calc(100% - 300px);
padding: 10px 14px;
box-sizing: border-box;
max-width: 718px;
}
.configSetting {
border: 1px solid #ccc;
border-radius: 4px;
height: 95px;
margin-bottom: 10px;
margin-bottom: 20px;
align-items: flex-start;
min-height: 100px;
}
.settingsError {
@@ -42,13 +45,13 @@
}
.configSettingInfoBox {
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 10px;
min-height: 100px;
margin-bottom: 20px;
cursor: pointer;
width: auto;
height: auto;
text-align: left;
overflow: visible;
}
.configSettingInfoBox p {
@@ -77,7 +80,7 @@
}
.charCountTexfieldEnabled {
border-color: #4caf50;
border-color: #00796b;
}
.charCountTexfield:focus {
@@ -85,11 +88,12 @@
}
.changedSave {
background-color:#4caf50;
background-color: #00796B;
color: white;
}
.copiedText {
color: #008000;
color: #00796b;
float: right;
padding: 12px;
font-size: 14px;
@@ -97,6 +101,7 @@
.copyButton {
float: right;
width: 200px;
}
.embedInput {
@@ -113,8 +118,12 @@
}
#bannedWordlist, #suspectWordlist {
width: 100%;
padding: 10px;
input {
width: 100%;
padding: 10px;
}
}
.wordlistHeader {
@@ -124,7 +133,7 @@
}
.enabledSetting {
border-left-color: #4caf50;
border-left-color: #00796b;
border-left-style: solid;
border-left-width: 7px;
}
@@ -136,3 +145,23 @@
.hidden {
display: none;
}
.saveBox {
margin-top: 38px;
}
.commentSettingsSection {
padding-bottom: 200px;
.action {
display: inline-block;
position: absolute;
top: 0;
left: 0;
padding: 20px;
}
.content {
display: inline-block;
padding-left: 30px;
}
}
@@ -1,4 +1,4 @@
import React from 'react';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {
fetchSettings,
@@ -6,13 +6,8 @@ import {
saveSettingsToServer,
updateWordlist,
} from '../../actions/settings';
import {
List,
ListItem,
ListItemContent,
Button,
Icon
} from 'react-mdl';
import {Button, List, Item} from 'coral-ui';
import styles from './Configure.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
@@ -21,7 +16,7 @@ import CommentSettings from './CommentSettings';
import Wordlist from './Wordlist';
import has from 'lodash/has';
class Configure extends React.Component {
class Configure extends Component {
constructor (props) {
super(props);
@@ -30,6 +25,8 @@ class Configure extends React.Component {
changed: false,
errors: {}
};
this.changeSection = this.changeSection.bind(this);
}
componentWillMount = () => {
@@ -41,7 +38,7 @@ class Configure extends React.Component {
this.setState({changed: false});
}
changeSection = (activeSection) => () => {
changeSection(activeSection) {
this.setState({activeSection});
}
@@ -99,7 +96,8 @@ class Configure extends React.Component {
}
render () {
const section = this.getSection(this.state.activeSection);
const {activeSection} = this.state;
const section = this.getSection(activeSection);
const showSave = Object.keys(this.state.errors).reduce(
(bool, error) => this.state.errors[error] ? false : bool, this.state.changed);
@@ -107,37 +105,40 @@ class Configure extends React.Component {
return (
<div className={styles.container}>
<div className={styles.leftColumn}>
<List>
<ListItem className={styles.settingOption}>
<ListItemContent
onClick={this.changeSection('comments')}
icon='settings'>{lang.t('configure.comment-settings')}</ListItemContent>
</ListItem>
<ListItem className={styles.settingOption}>
<ListItemContent
onClick={this.changeSection('embed')}
icon='code'>{lang.t('configure.embed-comment-stream')}</ListItemContent>
</ListItem>
<ListItem className={styles.settingOption}>
<ListItemContent
onClick={this.changeSection('wordlist')}
icon='settings'>{lang.t('configure.wordlist')}</ListItemContent>
</ListItem>
<List onChange={this.changeSection} activeItem={activeSection}>
<Item itemId='comments' icon="settings">
{lang.t('configure.comment-settings')}
</Item>
<Item itemId='embed' icon='code'>
{lang.t('configure.embed-comment-stream')}
</Item>
<Item itemId='wordlist' icon='settings'>
{lang.t('configure.wordlist')}
</Item>
</List>
<div className={styles.saveBox}>
{
showSave ?
<Button
raised
onClick={this.saveSettings}
className={styles.changedSave}>
<Icon name='check' /> {lang.t('configure.save-changes')}
</Button>
: <Button
raised
disabled>
{lang.t('configure.save-changes')}
</Button>
<Button
raised
onClick={this.saveSettings}
className={styles.changedSave}
icon='check'
full
>
{lang.t('configure.save-changes')}
</Button>
:
<Button
raised
disabled
icon='check'
full
>
{lang.t('configure.save-changes')}
</Button>
}
</div>
</div>
<div className={styles.mainContent}>
@@ -2,11 +2,7 @@ import React, {Component} from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import styles from './Configure.css';
import {
List,
ListItem,
Button
} from 'react-mdl';
import {Button, Card} from 'coral-ui';
class EmbedLink extends Component {
@@ -34,16 +30,16 @@ class EmbedLink extends Component {
return (
<div>
<h3>{this.props.title}</h3>
<List>
<ListItem className={styles.configSettingEmbed}>
<div>
<Card shadow="2">
<p>{lang.t('configure.copy-and-paste')}</p>
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
<Button raised colored className={styles.copyButton} onClick={this.copyToClipBoard}>
<Button raised className={styles.copyButton} onClick={this.copyToClipBoard} cStyle="black">
{lang.t('embedlink.copy')}
</Button>
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
</ListItem>
</List>
</Card>
</div>
</div>
);
}
@@ -2,15 +2,13 @@ import React from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import TagsInput from 'react-tagsinput';
import styles from './Configure.css';
import {Card} from 'react-mdl';
import {Card} from 'coral-ui';
const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
<div>
<h3>{lang.t('configure.banned-words-title')}</h3>
<Card id={styles.bannedWordlist} shadow={2}>
<Card id={styles.bannedWordlist}>
<p className={styles.wordlistHeader}>{lang.t('configure.banned-word-header')}</p>
<p className={styles.wordlistDesc}>{lang.t('configure.banned-word-text')}</p>
<TagsInput
@@ -18,10 +16,11 @@ const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
inputProps={{placeholder: 'word or phrase'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeWordlist('banned', tags)} />
onChange={tags => onChangeWordlist('banned', tags)}
/>
</Card>
<h3>{lang.t('configure.suspect-words-title')}</h3>
<Card id={styles.suspectWordlist} shadow={2}>
<Card id={styles.suspectWordlist}>
<p className={styles.wordlistHeader}>{lang.t('configure.suspect-word-header')}</p>
<p className={styles.wordlistDesc}>{lang.t('configure.suspect-word-text')}</p>
<TagsInput
@@ -75,11 +75,14 @@ class ModerationContainer extends React.Component {
render () {
const {comments, actions} = this.props;
const premodIds = comments.ids.filter(id => comments.byId[id].status === 'premod');
const rejectedIds = comments.ids.filter(id => comments.byId[id].status === 'rejected');
const flaggedIds = comments.ids.filter(id => comments.byId[id].flagged === true);
const userActionIds = actions.ids.filter(id => actions.byId[id].item_type === 'users');
const premodIds = comments.ids.filter(id => comments.byId[id].status === 'PREMOD');
const rejectedIds = comments.ids.filter(id => comments.byId[id].status === 'REJECTED');
const flaggedIds = comments.ids.filter(id =>
comments.byId[id].flagged === true &&
comments.byId[id].status !== 'REJECTED' &&
comments.byId[id].status !== 'ACCEPTED'
);
const userActionIds = actions.ids.filter(id => actions.byId[id].item_type === 'USERS');
return (
<ModerationQueue
@@ -6,12 +6,32 @@
}
.tabBar {
background: #9E9E9E;
background: #262626;
z-index: 5;
}
.tab {
flex: 1;
color: white;
text-transform: capitalize;
font-weight: 500;
font-size: 15px;
letter-spacing: 1px;
transition: border-bottom 200ms;
}
.active {
color: white;
box-sizing: border-box;
border-bottom: solid 5px #F36451;
}
.active > span {
color: white;
}
.active:after {
background: transparent !important;
}
.showShortcuts {
@@ -12,114 +12,177 @@ const lang = new I18n(translations);
export default (props) => (
<div>
<div className='mdl-tabs mdl-js-tabs mdl-js-ripple-effect'>
<div className='mdl-tabs'>
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<a href='#all' onClick={() => props.onTabClick('all')}
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.all')}</a>
<a href='#pending' onClick={() => props.onTabClick('pending')}
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.pending')}</a>
<a href='#flagged' onClick={() => props.onTabClick('flagged')}
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.flagged')}</a>
<a href='#account' onClick={() => props.onTabClick('account')}
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.account')}</a>
<a href='#rejected' onClick={() => props.onTabClick('rejected')}
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.rejected')}</a>
<a href='#pending'
onClick={(e) => {
e.preventDefault();
props.onTabClick('pending');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'pending' ? styles.active : ''}`}
>
{lang.t('modqueue.pending')}
</a>
<a href='#account'
onClick={(e) => {
e.preventDefault();
props.onTabClick('account');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'account' ? styles.active : ''}`}>
{lang.t('modqueue.account')}
</a>
<a href='#rejected'
onClick={(e) => {
e.preventDefault();
props.onTabClick('rejected');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'rejected' ? styles.active : ''}`}
>
{lang.t('modqueue.rejected')}
</a>
<a href='#flagged'
onClick={(e) => {
e.preventDefault();
props.onTabClick('flagged');
}}
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'flagged' ? styles.active : ''}`}
>
{lang.t('modqueue.flagged')}
</a>
</div>
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='all'>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'all'}
singleView={props.singleView}
commentIds={[...props.premodIds, ...props.flaggedIds]}
comments={props.comments.byId}
users={props.users.byId}
actionIds={props.userActionIds}
actions={props.actions.byId}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
updateCommentStatus={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
{
props.activeTab === 'all' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'all'}
singleView={props.singleView}
commentIds={[...props.premodIds, ...props.flaggedIds]}
comments={props.comments.byId}
users={props.users.byId}
actionIds={props.userActionIds}
actions={props.actions.byId}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
updateCommentStatus={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='pending'>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'pending'}
singleView={props.singleView}
commentIds={props.premodIds}
comments={props.comments.byId}
users={props.users.byId}
actions={props.actions.byId}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
updateCommentStatus={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'rejected'}
singleView={props.singleView}
commentIds={props.rejectedIds}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
comments={props.comments.byId}
users={props.users.byId}
updateCommentStatus={props.updateStatus}
modActions={['approve']}
loading={props.comments.loading}
/>
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='pending'>
{
props.activeTab === 'pending' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'pending'}
singleView={props.singleView}
commentIds={props.premodIds}
comments={props.comments.byId}
users={props.users.byId}
actions={props.actions.byId}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
updateCommentStatus={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='account'>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'account'}
singleView={props.singleView}
users={props.users.byId}
actionIds={props.userActionIds}
actions={props.actions.byId}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
updateCommentStatus={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
{
props.activeTab === 'account' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'account'}
singleView={props.singleView}
users={props.users.byId}
actionIds={props.userActionIds}
actions={props.actions.byId}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
updateCommentStatus={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='flagged'>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'flagged'}
singleView={props.singleView}
commentIds={props.flaggedIds}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
comments={props.comments.byId}
users={props.users.byId}
updateCommentStatus={props.updateStatus}
modActions={['reject', 'approve']}
loading={props.comments.loading}/>
</div>
{
props.activeTab === 'flagged' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'flagged'}
singleView={props.singleView}
commentIds={props.flaggedIds}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
comments={props.comments.byId}
users={props.users.byId}
updateCommentStatus={props.updateStatus}
modActions={['reject', 'approve']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
{
props.activeTab === 'rejected' &&
<div>
<ModerationList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'rejected'}
singleView={props.singleView}
commentIds={props.rejectedIds}
userStatusUpdate={props.userStatusUpdate}
suspendUser={props.suspendUser}
comments={props.comments.byId}
users={props.users.byId}
updateCommentStatus={props.updateStatus}
modActions={['approve']}
loading={props.comments.loading}
/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.userStatusUpdate}
user={props.comments.banUser}
/>
</div>
}
</div>
<ModerationKeysModal open={props.modalOpen} onClose={props.closeModal} />
</div>
</div>
@@ -4,11 +4,14 @@
}
.leftColumn {
width: 200px;
padding: 42px 56px;
width: 234px;
}
.mainContent {
width: calc(90% - 200px);
width: calc(100% - 300px);
padding: 34px 14px;
box-sizing: border-box;
}
.searchIcon {
@@ -18,11 +21,12 @@
}
.searchBox {
padding: 3px;
width: 100%;
padding: 9px;
border: 1px solid #ccc;
border-radius: 3px;
width: 90%;
border-radius: 2px;
display: flex;
background: white;
}
.searchBoxInput {
@@ -48,6 +52,16 @@
.streamsTable {
width: 100%;
border-left: none;
border-right: none;
th {
font-size: 1.1em;
}
th.status {
width: 100px;
}
}
.radio {
@@ -55,18 +69,20 @@
}
.statusMenu {
border-radius: 3px;
border-radius: 2px;
width: 10em;
text-align: center;
float: right;
border: 1px solid #ccc;
color: #fff;
cursor: pointer;
letter-spacing: 0.7px;
font-weight: 400;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
}
.statusMenuOpen {
padding: 10px;
background-color: #4caf50;
background-color: #268D81;
}
.statusMenuIcon {
@@ -75,9 +91,17 @@
.statusMenuClosed {
padding: 10px;
background-color: #000;
background-color: #262626;
}
.hidden {
display: none;
}
.radioGroup {
margin-top: 5px;
span {
margin-bottom: 7px;
display: inline-block;
}
}
@@ -103,61 +103,63 @@ class Streams extends Component {
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>
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')}>
onChange={this.onSettingChange('filter')}
className={styles.radioGroup}
>
<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>
<RadioGroup
name='sort by'
value={sort}
childContainer='div'
onChange={this.onSettingChange('sort')}
className={styles.radioGroup}
>
<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 name="publication_date" cellFormatter={this.renderDate}>
{lang.t('streams.pubdate')}
</TableHeader>
<TableHeader name="closedAt" cellFormatter={this.renderStatus} className={styles.status}>
{lang.t('streams.status')}
</TableHeader>
</DataTable>
<Pager
totalPages={Math.ceil((assets.count || 0) / limit)}
page={this.state.page}
onNewPageHandler={this.onPageClick}
/>
</div>
</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>;
);
}
}
+3 -3
View File
@@ -53,13 +53,13 @@
"include-comment-stream": "Include Comment Stream Description for Readers.",
"include-comment-stream-desc": "Write a message to be added to the top of your comment stream. Pose a topic, include community guidelines, etc.",
"include-text": "Include your text here.",
"comment-settings": "Comment Settings",
"embed-comment-stream": "Embed Comment Stream",
"comment-settings": "Settings",
"embed-comment-stream": "Embed Stream",
"banned-word-header": "Write the banned words list",
"suspect-word-header": "Write the suspect words list",
"banned-word-text": "Comments which contain these words or phrases (not case-sensitive) will be automatically removed from the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
"suspect-word-text": "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
"wordlist": "Banned & Suspect Words",
"wordlist": "Banned Words",
"banned-words-title": "Banned words list",
"suspect-words-title": "Suspect words list",
"save-changes": "Save Changes",
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
import {connect} from 'react-redux';
import {I18n} from '../../coral-framework';
import {updateOpenStatus, updateConfiguration} from '../../coral-framework/actions/config';
import {updateOpenStatus, updateConfiguration} from '../../coral-framework/actions/asset';
import CloseCommentsInfo from '../components/CloseCommentsInfo';
import ConfigureCommentStream from '../components/ConfigureCommentStream';
@@ -13,8 +13,10 @@ class ConfigureStreamContainer extends Component {
constructor (props) {
super(props);
console.log('moderation', props.asset.settings.moderation);
this.state = {
premod: props.config.moderation === 'pre',
premod: props.asset.settings.moderation === 'PRE',
premodLinks: false
};
@@ -26,7 +28,7 @@ class ConfigureStreamContainer extends Component {
handleApply () {
const {premod, changed} = this.state;
const newConfig = {
moderation: premod ? 'pre' : 'post'
moderation: premod ? 'PRE' : 'POST'
};
if (changed) {
this.props.updateConfiguration(newConfig);
@@ -47,18 +49,17 @@ class ConfigureStreamContainer extends Component {
}
toggleStatus () {
this.props.updateStatus(this.props.config.status === 'open' ? 'closed' : 'open');
this.props.updateStatus(this.props.asset.closedAt === null ? 'closed' : 'open');
}
getClosedIn () {
const {closedTimeout} = this.props.config;
const {closedTimeout} = this.props.asset.settings;
const {created_at} = this.props.asset;
return lang.timeago(new Date(created_at).getTime() + (1000 * closedTimeout));
}
render () {
const {status} = this.props;
const status = this.props.asset.closedAt === null ? 'open' : 'closed';
return (
<div>
<ConfigureCommentStream
@@ -80,11 +81,7 @@ class ConfigureStreamContainer extends Component {
}
const mapStateToProps = (state) => ({
config: state.config.toJS(),
asset: state.items
.get('assets')
.first()
.toJS()
asset: state.asset.toJS()
});
const mapDispatchToProps = dispatch => ({
@@ -0,0 +1 @@
+171
View File
@@ -0,0 +1,171 @@
// this component will
// render its children
// render a like button
// render a permalink button
// render a reply button
// render a flag button
// translate things?
import React, {PropTypes} from 'react';
import PermalinkButton from 'coral-plugin-permalinks/PermalinkButton';
import AuthorName from 'coral-plugin-author-name/AuthorName';
import Content from 'coral-plugin-commentcontent/CommentContent';
import PubDate from 'coral-plugin-pubdate/PubDate';
import {ReplyBox, ReplyButton} from 'coral-plugin-replies';
import FlagComment from 'coral-plugin-flags/FlagComment';
import LikeButton from 'coral-plugin-likes/LikeButton';
const getAction = (type, comment) => comment.actions.filter((a) => a.type === type)[0];
class Comment extends React.Component {
constructor(props) {
super(props);
this.state = {replyBoxVisible: false};
}
static propTypes = {
refetch: PropTypes.func.isRequired,
showSignInDialog: PropTypes.func.isRequired,
postAction: PropTypes.func.isRequired,
deleteAction: PropTypes.func.isRequired,
parentId: PropTypes.string,
addNotification: PropTypes.func.isRequired,
postItem: PropTypes.func.isRequired,
depth: PropTypes.number.isRequired,
asset: PropTypes.shape({
id: PropTypes.string,
title: PropTypes.string,
url: PropTypes.string
}).isRequired,
currentUser: PropTypes.shape({
id: PropTypes.string.isRequired
}),
comment: PropTypes.shape({
depth: PropTypes.number,
actions: PropTypes.array.isRequired,
body: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
replies: PropTypes.arrayOf(
PropTypes.shape({
body: PropTypes.string.isRequired,
id: PropTypes.string.isRequired
})
),
user: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
}).isRequired
}).isRequired
}
onReplyBoxClick = () => {
if (this.props.currentUser) {
this.setState({replyBoxVisible: !this.state.replyBoxVisible});
} else {
const offset = document.getElementById(`c_${this.props.comment.id}`).getBoundingClientRect().top - 75;
this.props.showSignInDialog(offset);
}
}
render () {
const {
comment,
parentId,
currentUser,
asset,
depth,
postItem,
refetch,
addNotification,
showSignInDialog,
postAction,
deleteAction
} = this.props;
const like = getAction('LIKE', comment);
const flag = getAction('FLAG', comment);
return (
<div
className="comment"
id={`c_${comment.id}`}
style={{marginLeft: depth * 30}}>
<hr aria-hidden={true} />
<AuthorName
author={comment.user}
addNotification={this.props.addNotification}
id={comment.id}
author_id={comment.user.id}
postAction={this.props.postAction}
showSignInDialog={this.props.showSignInDialog}
deleteAction={this.props.deleteAction}
currentUser={currentUser}/>
<PubDate created_at={comment.created_at} />
<Content body={comment.body} />
<div className="commentActionsLeft">
<ReplyButton
onClick={this.onReplyBoxClick}
parentCommentId={parentId || comment.id}
currentUserId={currentUser && currentUser.id}
banned={false} />
<LikeButton
like={like}
id={comment.id}
postAction={postAction}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser} />
</div>
<div className="commentActionsRight">
<FlagComment
flag={flag}
id={comment.id}
author_id={comment.user.id}
postAction={postAction}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
currentUser={currentUser} />
<PermalinkButton articleURL={asset.url} commentId={comment.id} />
</div>
{
this.state.replyBoxVisible
? <ReplyBox
commentPostedHandler={() => {
console.log('replyPostedHandler');
this.setState({replyBoxVisible: false});
refetch();
}}
parentId={parentId || comment.id}
addNotification={addNotification}
authorId={currentUser.id}
postItem={postItem}
assetId={asset.id} />
: null
}
{
comment.replies &&
comment.replies.map(reply => {
return <Comment
refetch={refetch}
addNotification={addNotification}
parentId={comment.id}
postItem={postItem}
depth={depth + 1}
asset={asset}
currentUser={currentUser}
postAction={postAction}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={reply.id}
comment={reply} />;
})
}
</div>
);
}
}
export default Comment;
@@ -1,331 +0,0 @@
import React, {Component, PropTypes} from 'react';
import Pym from 'pym.js';
import {connect} from 'react-redux';
import {
itemActions,
Notification,
notificationActions,
authActions
} from '../../coral-framework';
import CommentBox from '../../coral-plugin-commentbox/CommentBox';
import InfoBox from '../../coral-plugin-infobox/InfoBox';
import Content from '../../coral-plugin-commentcontent/CommentContent';
import PubDate from '../../coral-plugin-pubdate/PubDate';
import Count from '../../coral-plugin-comment-count/CommentCount';
import AuthorName from '../../coral-plugin-author-name/AuthorName';
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies';
import FlagComment from '../../coral-plugin-flags/FlagComment';
import LikeButton from '../../coral-plugin-likes/LikeButton';
import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton';
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
import UserBox from '../../coral-sign-in/components/UserBox';
import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui';
import SettingsContainer from '../../coral-settings/containers/SettingsContainer';
import RestrictedContent from '../../coral-framework/components/RestrictedContent';
import SuspendedAccount from '../../coral-framework/components/SuspendedAccount';
import ConfigureStreamContainer from '../../coral-configure/containers/ConfigureStreamContainer';
const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions;
const {addNotification, clearNotification} = notificationActions;
const {logout, showSignInDialog} = authActions;
class CommentStream extends Component {
constructor (props) {
super(props);
this.state = {
activeTab: 0
};
this.changeTab = this.changeTab.bind(this);
}
changeTab (tab) {
this.setState({
activeTab: tab
});
}
static propTypes = {
items: PropTypes.object.isRequired,
addItem: PropTypes.func.isRequired,
updateItem: PropTypes.func.isRequired
}
componentDidMount () {
// Set up messaging between embedded Iframe an parent component
this.pym = new Pym.Child({polling: 100});
let path = this.pym.parentUrl.split('#')[0];
if (!path) {
path = window.location.href.split('#')[0];
}
this.props.getStream(path || window.location);
this.path = path;
this.pym.sendMessage('childReady');
this.pym.onMessage('DOMContentLoaded', hash => {
const commentId = hash.replace('#', 'c_');
let count = 0;
const interval = setInterval(() => {
if (document.getElementById(commentId)) {
window.clearInterval(interval);
this.pym.scrollParentToChildEl(commentId);
}
if (++count > 100) { // ~10 seconds
// give up waiting for the comments to load.
// it would be weird for the page to jump after that long.
window.clearInterval(interval);
}
}, 100);
});
}
render () {
const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0];
const rootItem = this.props.items.assets && this.props.items.assets[rootItemId];
const {actions, users, comments} = this.props.items;
const {status, moderation, closedMessage, charCount, charCountEnable} = this.props.config;
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
const {activeTab} = this.state;
const banned = (this.props.userData.status === 'banned');
const expandForLogin = showSignInDialog ? {
minHeight: document.body.scrollHeight + 150
} : {};
return <div style={expandForLogin}>
{
rootItem
? <div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count id={rootItemId} items={this.props.items}/></Tab>
<Tab>Settings</Tab>
<Tab restricted={!isAdmin}>Configure Stream</Tab>
</TabBar>
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
<TabContent show={activeTab === 0}>
{
status === 'open'
? <div id="commentBox">
<InfoBox
content={this.props.config.infoBoxContent}
enable={this.props.config.infoBoxEnable}
/>
<RestrictedContent restricted={banned} restrictedComp={<SuspendedAccount />}>
<CommentBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
premod={moderation}
reply={false}
currentUser={this.props.auth.user}
banned={banned}
author={user}
charCount={charCountEnable && charCount}/>
</RestrictedContent>
</div>
: <p>{closedMessage}</p>
}
{!loggedIn && <SignInContainer offset={signInOffset}/>}
{
rootItem.comments && rootItem.comments.map((commentId) => {
const comment = comments[commentId];
return <div className="comment" key={commentId} id={`c_${commentId}`}>
<hr aria-hidden={true}/>
<AuthorName
author={users[comment.author_id]}
addNotification={this.props.addNotification}
id={commentId}
author_id={comment.author_id}
postAction={this.props.postAction}
showSignInDialog={this.props.showSignInDialog}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PubDate created_at={comment.created_at}/>
<Content body={comment.body}/>
<div className="commentActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={commentId}
currentUser={this.props.auth.user}
showReply={comment.showReply}
banned={banned}/>
<LikeButton
addNotification={this.props.addNotification}
id={commentId}
like={actions[comment.like]}
showSignInDialog={this.props.showSignInDialog}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}
banned={banned}/>
</div>
<div className="commentActionsRight">
<FlagComment
addNotification={this.props.addNotification}
id={commentId}
author_id={comment.author_id}
flag={actions[comment.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
showSignInDialog={this.props.showSignInDialog}
updateItem={this.props.updateItem}
banned={banned}
currentUser={this.props.auth.user}/>
<PermalinkButton
commentId={commentId}
articleURL={this.path}/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
author={user}
parent_id={commentId}
premod={moderation}
currentUser={user}
charCount={charCountEnable && charCount}
showReply={comment.showReply}/>
{
comment.children &&
comment.children.map((replyId) => {
let reply = this.props.items.comments[replyId];
return <div className="reply" key={replyId} id={`c_${replyId}`}>
<hr aria-hidden={true}/>
<AuthorName author={users[reply.author_id]}/>
<PubDate created_at={reply.created_at}/>
<Content body={reply.body}/>
<div className="replyActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={replyId}
banned={banned}
currentUser={this.props.auth.user}
showReply={reply.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={replyId}
like={this.props.items.actions[reply.like]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
showSignInDialog={this.props.showSignInDialog}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}
banned={banned}/>
</div>
<div className="replyActionsRight">
<FlagComment
addNotification={this.props.addNotification}
id={replyId}
author_id={comment.author_id}
flag={actions[reply.flag]}
postAction={this.props.postAction}
showSignInDialog={this.props.showSignInDialog}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
banned={banned}
currentUser={this.props.auth.user}/>
<PermalinkButton
commentId={reply.parent_id}
articleURL={this.path}
/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
author={user}
parent_id={commentId}
child_id={replyId}
premod={moderation}
banned={banned}
currentUser={user}
charCount={charCountEnable && charCount}
showReply={reply.showReply}/>
</div>;
})
}
</div>;
})
}
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={this.props.notification}
/>
</TabContent>
<TabContent show={activeTab === 1}>
<SettingsContainer
loggedIn={loggedIn}
userData={this.props.userData}
showSignInDialog={this.props.handleSignInDialog}
/>
</TabContent>
<TabContent show={activeTab === 2}>
<RestrictedContent restricted={!loggedIn}>
<ConfigureStreamContainer
status={status}
onClick={this.toggleStatus}
/>
</RestrictedContent>
</TabContent>
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={this.props.notification}
/>
</div>
:
<Spinner/>
}
</div>;
}
}
const mapStateToProps = state => ({
config: state.config.toJS(),
items: state.items.toJS(),
notification: state.notification.toJS(),
auth: state.auth.toJS(),
userData: state.user.toJS()
});
const mapDispatchToProps = (dispatch) => ({
addItem: (item, item_id) => dispatch(addItem(item, item_id)),
updateItem: (id, property, value, itemType) => dispatch(updateItem(id, property, value, itemType)),
postItem: (data, type, id) => dispatch(postItem(data, type, id)),
getStream: (rootId) => dispatch(getStream(rootId)),
addNotification: (type, text) => dispatch(addNotification(type, text)),
clearNotification: () => dispatch(clearNotification()),
postAction: (item, itemType, action) => dispatch(postAction(item, itemType, action)),
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
deleteAction: (item, action, user, itemType) => dispatch(deleteAction(item, action, user, itemType)),
appendItemArray: (item, property, value, addToFront, itemType) => dispatch(appendItemArray(item, property, value, addToFront, itemType)),
handleSignInDialog: () => dispatch(authActions.showSignInDialog()),
logout: () => dispatch(logout())
});
export default connect(mapStateToProps, mapDispatchToProps)(CommentStream);
+205
View File
@@ -0,0 +1,205 @@
import React, {Component} from 'react';
import {compose} from 'react-apollo';
import {connect} from 'react-redux';
import {isEqual} from 'lodash';
import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui';
const {logout, showSignInDialog} = authActions;
const {addNotification, clearNotification} = notificationActions;
const {fetchAssetSuccess} = assetActions;
import {queryStream} from './graphql/queries';
import {postComment, postAction, deleteAction} from './graphql/mutations';
import {Notification, notificationActions, authActions, assetActions, pym} from 'coral-framework';
import Stream from './Stream';
import InfoBox from 'coral-plugin-infobox/InfoBox';
import Count from 'coral-plugin-comment-count/CommentCount';
import CommentBox from 'coral-plugin-commentbox/CommentBox';
import UserBox from '../../coral-sign-in/components/UserBox';
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
import SuspendedAccount from '../../coral-framework/components/SuspendedAccount';
import SettingsContainer from '../../coral-settings/containers/SettingsContainer';
import RestrictedContent from '../../coral-framework/components/RestrictedContent';
import ConfigureStreamContainer from '../../coral-configure/containers/ConfigureStreamContainer';
class Embed extends Component {
constructor (props) {
super(props);
this.state = {
activeTab: 0,
showSignInDialog: false
};
this.changeTab = this.changeTab.bind(this);
}
changeTab (tab) {
this.setState({
activeTab: tab
});
}
static propTypes = {
data: React.PropTypes.shape({
loading: React.PropTypes.bool,
error: React.PropTypes.object
}).isRequired
}
componentDidMount () {
// stream id, logged in user, settings
// Set up messaging between embedded Iframe an parent component
// this.props.getStream(path || window.location);
// this.path = window.location.href.split('#')[0];
//
pym.sendMessage('childReady');
pym.onMessage('DOMContentLoaded', hash => {
const commentId = hash.replace('#', 'c_');
let count = 0;
const interval = setInterval(() => {
if (document.getElementById(commentId)) {
window.clearInterval(interval);
pym.scrollParentToChildEl(commentId);
}
if (++count > 100) { // ~10 seconds
// give up waiting for the comments to load.
// it would be weird for the page to jump after that long.
window.clearInterval(interval);
}
}, 100);
});
}
componentWillReceiveProps (nextProps) {
const {loadAsset} = this.props;
if(!isEqual(nextProps.data.asset, this.props.data.asset)) {
loadAsset(nextProps.data.asset);
}
}
render () {
const {activeTab} = this.state;
const {loading, asset, refetch} = this.props.data;
const {loggedIn, isAdmin, user, showSignInDialog, signInOffset} = this.props.auth;
const expandForLogin = showSignInDialog ? {
minHeight: document.body.scrollHeight + 200
} : {};
return <div style={expandForLogin}>
{
loading ? <Spinner/>
: <div className="commentStream">
<TabBar onChange={this.changeTab} activeTab={activeTab}>
<Tab><Count count={asset.comments.length}/></Tab>
<Tab>Settings</Tab>
<Tab restricted={!isAdmin}>Configure Stream</Tab>
</TabBar>
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
<TabContent show={activeTab === 0}>
{
asset.closedAt === null
? <div id="commentBox">
<InfoBox
content={asset.settings.infoBoxContent}
enable={asset.settings.infoBoxEnable}
/>
<RestrictedContent restricted={false} restrictedComp={<SuspendedAccount />}>
{
user
? <CommentBox
commentPostedHandler={refetch}
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
assetId={asset.id}
premod={asset.settings.moderation}
isReply={false}
currentUser={this.props.auth.user}
banned={false}
authorId={user.id}
charCount={asset.settings.charCountEnable && asset.settings.charCount} />
: null
}
</RestrictedContent>
</div>
: <p>{asset.settings.closedMessage}</p>
}
{!loggedIn && <SignInContainer offset={signInOffset}/>}
<Stream
refetch={refetch}
addNotification={this.props.addNotification}
postItem={this.props.postItem}
asset={asset}
currentUser={user}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
showSignInDialog={this.props.showSignInDialog}
comments={asset.comments} />
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={{text: null}}
/>
</TabContent>
<TabContent show={activeTab === 1}>
<SettingsContainer
loggedIn={loggedIn}
userData={this.props.userData}
showSignInDialog={this.props.showSignInDialog}
/>
</TabContent>
<TabContent show={activeTab === 2}>
<RestrictedContent restricted={!loggedIn}>
<ConfigureStreamContainer
status={status}
onClick={this.toggleStatus}
/>
</RestrictedContent>
</TabContent>
<Notification
notifLength={4500}
clearNotification={this.props.clearNotification}
notification={this.props.notification}
/>
</div>
}
</div>;
}
}
const mapStateToProps = state => ({
items: state.items.toJS(),
notification: state.notification.toJS(),
auth: state.auth.toJS(),
userData: state.user.toJS()
});
const mapDispatchToProps = dispatch => ({
loadAsset: (asset) => dispatch(fetchAssetSuccess(asset)),
addNotification: (type, text) => dispatch(addNotification(type, text)),
clearNotification: () => dispatch(clearNotification()),
showSignInDialog: (offset) => dispatch(showSignInDialog(offset)),
logout: () => dispatch(logout()),
dispatch: d => dispatch(d)
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
postComment,
postAction,
deleteAction,
queryStream
)(Embed);
+49
View File
@@ -0,0 +1,49 @@
import React, {PropTypes} from 'react';
import Comment from './Comment';
const Stream = ({
comments,
currentUser,
asset,
postItem,
addNotification,
postAction,
deleteAction,
showSignInDialog,
refetch
}) => {
return (
<div>
{
comments.map(comment => {
return <Comment
refetch={refetch}
addNotification={addNotification}
depth={0}
postItem={postItem}
asset={asset}
currentUser={currentUser}
postAction={postAction}
deleteAction={deleteAction}
showSignInDialog={showSignInDialog}
key={comment.id}
comment={comment} />;
})
}
</div>
);
};
Stream.propTypes = {
refetch: PropTypes.func.isRequired,
addNotification: PropTypes.func.isRequired,
postItem: PropTypes.func.isRequired,
asset: PropTypes.object.isRequired,
comments: PropTypes.array.isRequired,
currentUser: PropTypes.shape({
displayName: PropTypes.string,
id: PropTypes.string
})
};
export default Stream;
@@ -0,0 +1,3 @@
mutation deleteAction ($id: ID!) {
deleteAction(id:$id)
}
@@ -0,0 +1,39 @@
import {graphql} from 'react-apollo';
import POST_COMMENT from './postComment.graphql';
import POST_ACTION from './postAction.graphql';
import DELETE_ACTION from './deleteAction.graphql';
export const postComment = graphql(POST_COMMENT, {
props: ({mutate}) => ({
postItem: ({asset_id, body, parent_id} /* , type */ ) => {
return mutate({
variables: {
asset_id,
body,
parent_id
}
});
}}),
});
export const postAction = graphql(POST_ACTION, {
props: ({mutate}) => ({
postAction: (action) => {
return mutate({
variables: {
action
}
});
}}),
});
export const deleteAction = graphql(DELETE_ACTION, {
props: ({mutate}) => ({
deleteAction: (id) => {
return mutate({
variables: {
id
}
});
}}),
});
@@ -0,0 +1,5 @@
mutation CreateAction ($action: CreateActionInput!) {
createAction(action:$action) {
id
}
}
@@ -0,0 +1,23 @@
fragment commentView on Comment {
id
body
status
user {
name: displayName
}
actions {
type: action_type
count
current: current_user {
id
created_at
}
}
}
mutation CreateComment ($asset_id: ID!, $parent_id: ID, $body: String!) {
createComment(asset_id:$asset_id, parent_id:$parent_id, body:$body) {
...commentView
}
}
@@ -0,0 +1,9 @@
import {graphql} from 'react-apollo';
import STREAM_QUERY from './streamQuery.graphql';
import pym from 'coral-framework/PymConnection';
let url = pym.parentUrl.split('#')[0] || 'http://localhost:3000/';
export const queryStream = graphql(STREAM_QUERY, {
options: {variables: {asset_url: url}}
});
@@ -0,0 +1,47 @@
fragment commentView on Comment {
id
body
created_at
user {
id
name: displayName
settings {
bio
}
}
actions {
type: action_type
count
current: current_user {
id
created_at
}
}
}
query AssetQuery($asset_url: String!) {
asset(url: $asset_url) {
id
title
url
closedAt
created_at
settings {
moderation
infoBoxEnable
infoBoxContent
closeTimeout
closedMessage
charCountEnable
charCount
requireEmailConfirmation
}
comments {
...commentView
replies {
...commentView
}
}
}
}
+11 -7
View File
@@ -1,11 +1,15 @@
import React from 'react';
import {render} from 'react-dom';
import CommentStream from './CommentStream';
import {Provider} from 'react-redux';
import {store} from '../../coral-framework';
import {ApolloProvider} from 'react-apollo';
import {client} from 'coral-framework/client';
import store from 'coral-framework/store';
import Embed from './Embed';
render(
<Provider store={store}>
<CommentStream />
</Provider>
, document.querySelector('#coralStream'));
<ApolloProvider client={client} store={store}>
<Embed />
</ApolloProvider>
, document.querySelector('#coralStream')
);
+9
View File
@@ -0,0 +1,9 @@
import Pym from '../../node_modules/pym.js';
const pym = new Pym.Child({polling: 100});
export default pym;
export const link = (url) => (e) => {
e.preventDefault();
pym.sendMessage('navigate', url);
};
@@ -1,20 +1,21 @@
import * as actions from '../constants/asset';
import coralApi from '../helpers/response';
import * as actions from '../constants/config';
import {addNotification} from '../actions/notification';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
export const fetchAssetRequest = () => ({type: actions.FETCH_ASSET_REQUEST});
export const fetchAssetSuccess = asset => ({type: actions.FETCH_ASSET_SUCCESS, asset});
export const fetchAssetFailure = error => ({type: actions.FETCH_ASSET_FAILURE, error});
const updateConfigRequest = () => ({type: actions.UPDATE_CONFIG_REQUEST});
const updateConfigSuccess = config => ({type: actions.UPDATE_CONFIG_SUCCESS, config});
const updateConfigFailure = () => ({type: actions.UPDATE_CONFIG_FAILURE});
export const updateConfiguration = newConfig => (dispatch, getState) => {
const assetId = getState().items.get('assets')
.keySeq()
.toArray()[0];
const assetId = getState().asset.toJS().id;
dispatch(updateConfigRequest());
coralApi(`/assets/${assetId}/settings`, {method: 'PUT', body: newConfig})
.then(() => {
@@ -25,12 +26,8 @@ export const updateConfiguration = newConfig => (dispatch, getState) => {
};
export const updateOpenStream = closedBody => (dispatch, getState) => {
const assetId = getState().items.get('assets')
.keySeq()
.toArray()[0];
const assetId = getState().asset.toJS().id;
dispatch(updateConfigRequest());
coralApi(`/assets/${assetId}/status`, {method: 'PUT', body: closedBody})
.then(() => {
dispatch(addNotification('success', lang.t('successUpdateSettings')));
+2 -2
View File
@@ -27,7 +27,7 @@ export const fetchSignIn = (formData) => (dispatch) => {
dispatch(signInRequest());
coralApi('/auth/local', {method: 'POST', body: formData})
.then(({user}) => {
const isAdmin = !!user.roles.filter(i => i === 'admin').length;
const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length;
dispatch(signInSuccess(user, isAdmin));
dispatch(hideSignInDialog());
dispatch(addItem(user, 'users'));
@@ -132,7 +132,7 @@ export const checkLogin = () => dispatch => {
throw new Error('Not logged in');
}
const isAdmin = !!result.user.roles.filter(i => i === 'admin').length;
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
dispatch(checkLoginSuccess(result.user, isAdmin));
})
.catch(error => {
+27 -10
View File
@@ -189,17 +189,34 @@ export function getItemsArray (ids) {
* The newly put item to the item store
*/
export function postItem (item, type, id) {
return (dispatch) => {
if (id) {
item.id = id;
export function postItem (item, type, id, mutate) {
console.log(
item,
type,
id,
mutate
);
mutate({
variables: {
asset_id: id,
body: item,
parent_id: null
}
return coralApi(`/${type}`, {method: 'POST', body: item})
.then((json) => {
dispatch(addItem({...item, id:json.id}, type));
return json;
});
};
}).then(({data}) => {
console.log('it workt');
console.log(data);
});
// return (dispatch) => {
// if (id) {
// item.id = id;
// }
// return coralApi(`/${type}`, {method: 'POST', body: item})
// .then((json) => {
// dispatch(addItem({...item, id:json.id}, type));
// return json;
// });
// };
}
/*
+13 -5
View File
@@ -30,19 +30,27 @@ export const saveBio = (user_id, formData) => dispatch => {
* @returns Promise
*/
export const fetchCommentsByUserId = userId => {
return (dispatch) => {
return (dispatch, getState) => {
dispatch({type: actions.COMMENTS_BY_USER_REQUEST});
return coralApi(`/comments?user_id=${userId}`)
.then(({comments, assets}) => {
const state = getState();
comments.forEach(comment => dispatch(addItem(comment, 'comments')));
assets.forEach(asset => {
const prevAsset = state.items.getIn(['assets', asset.id]);
assets.forEach(asset => dispatch(addItem(asset, 'assets')));
if (prevAsset) {
// Include data such as hydrated comments from assets already in the system.
dispatch(addItem({...prevAsset.toJS(), ...asset}, 'assets'));
} else {
dispatch(addItem(asset, 'assets'));
}
});
dispatch({type: actions.COMMENTS_BY_USER_SUCCESS, comments: comments.map(comment => comment.id)});
dispatch({type: assetActions.MULTIPLE_ASSETS_SUCCESS, assets: assets.map(asset => asset.id)});
})
.catch(error => {
dispatch({type: actions.COMMENTS_BY_USER_FAILURE, error});
});
.catch(error => dispatch({type: actions.COMMENTS_BY_USER_FAILURE, error}));
};
};
+13
View File
@@ -0,0 +1,13 @@
import ApolloClient, {addTypename} from 'apollo-client';
import getNetworkInterface from './transport';
export const client = new ApolloClient({
queryTransformer: addTypename,
dataIdFromObject: (result) => {
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
return result.__typename + result.id; // eslint-disable-line no-underscore-dangle
}
return null;
},
networkInterface: getNetworkInterface()
});
+13
View File
@@ -0,0 +1,13 @@
export const FETCH_ASSET_REQUEST = 'FETCH_ASSET_REQUEST';
export const FETCH_ASSET_FAILURE = 'FETCH_ASSET_FAILURE';
export const FETCH_ASSET_SUCCESS = 'FETCH_ASSET_SUCCESS';
export const UPDATE_CONFIG_REQUEST = 'UPDATE_CONFIG_REQUEST';
export const UPDATE_CONFIG_SUCCESS = 'UPDATE_CONFIG_SUCCESS';
export const UPDATE_CONFIG_FAILURE = 'UPDATE_CONFIG_FAILURE';
export const UPDATE_CONFIG = 'UPDATE_CONFIG';
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
export const ADD_ITEM = 'ADD_ITEM';
+1 -1
View File
@@ -2,5 +2,5 @@ export default {
email: email => (/^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)),
password: pass => (/^(?=.{8,}).*$/.test(pass)),
confirmPassword: () => true,
displayName: displayName => (/^[a-z0-9_]+$/.test(displayName))
displayName: displayName => (/^[a-zA-Z0-9_]+$/.test(displayName))
};
+4 -2
View File
@@ -4,7 +4,8 @@ import * as itemActions from './actions/items';
import I18n from './modules/i18n/i18n';
import * as notificationActions from './actions/notification';
import * as authActions from './actions/auth';
import * as configActions from './actions/config';
import * as assetActions from './actions/asset';
import pym from './PymConnection';
export {
Notification,
@@ -13,5 +14,6 @@ export {
I18n,
notificationActions,
authActions,
configActions
assetActions,
pym
};
+36
View File
@@ -0,0 +1,36 @@
import {Map} from 'immutable';
import * as actions from '../constants/asset';
const initialState = Map({
closedAt: null,
settings: null,
title: null,
url: null,
features: Map({}),
status: 'open',
moderation: null
});
export default function asset (state = initialState, action) {
switch (action.type) {
case actions.FETCH_ASSET_SUCCESS :
return state
.merge(action.asset);
case actions.UPDATE_CONFIG:
return state
.merge(action.config);
case actions.UPDATE_CONFIG_SUCCESS:
return state
.merge(action.config);
case actions.OPEN_COMMENTS:
return state
.set('status', 'open')
.set('closedAt', null);
case actions.CLOSE_COMMENTS:
return state
.set('status', 'closed')
.set('closedAt', Date.now());
default:
return state;
}
}
-29
View File
@@ -1,29 +0,0 @@
import {Map} from 'immutable';
import * as actions from '../constants/config';
const initialState = Map({
features: Map({}),
status: 'open',
moderation: null
});
export default (state = initialState, action) => {
switch(action.type) {
case actions.UPDATE_CONFIG:
return state
.merge(Map(action.config));
case actions.UPDATE_CONFIG_SUCCESS:
return state
.merge(Map(action.config));
case actions.OPEN_COMMENTS:
return state
.set('status', 'open');
case actions.CLOSE_COMMENTS:
return state
.set('status', 'closed');
case actions.ADD_ITEM:
return action.item_type === 'assets' ? state.set('status', (action.item && action.item.closedAt && new Date(action.item.closedAt).getTime() <= new Date().getTime()) ? 'closed' : 'open') : state;
default:
return state;
}
};
+9 -16
View File
@@ -1,20 +1,13 @@
/* @flow */
import {combineReducers} from 'redux';
import config from './config';
import items from './items';
import notification from './notification';
import auth from './auth';
import user from './user';
import asset from './asset';
import items from './items';
import notification from './notification';
/**
* Expose the combined main reducer
*/
export default combineReducers({
config,
items,
notification,
export default {
auth,
user
});
user,
asset,
items,
notification
};
+19 -4
View File
@@ -1,9 +1,24 @@
import {createStore, applyMiddleware} from 'redux';
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
import thunk from 'redux-thunk';
import mainReducer from './reducers';
import {client} from './client';
const middlewares = [
applyMiddleware(client.middleware()),
applyMiddleware(thunk)
];
if (window.devToolsExtension) {
// we can't have the last argument of compose() be undefined
middlewares.push(window.devToolsExtension());
}
export default createStore(
mainReducer,
window.devToolsExtension && window.devToolsExtension(),
applyMiddleware(thunk)
combineReducers({
...mainReducer,
apollo: client.reducer()
}),
{},
compose(...middlewares)
);
+14
View File
@@ -0,0 +1,14 @@
import {print} from 'graphql-tag/printer';
// quick way to add the subscribe and unsubscribe functions to the network interface
const addGraphQLSubscriptions = (networkInterface, wsClient) => Object.assign(networkInterface, {
subscribe: (request, handler) => wsClient.subscribe({
query: print(request.query),
variables: request.variables,
}, handler),
unsubscribe: (id) => {
wsClient.unsubscribe(id);
},
});
export default addGraphQLSubscriptions;
+4
View File
@@ -14,6 +14,8 @@
"PASSWORD_REQUIRED": "Must input a password",
"PASSWORD_LENGTH": "Password is too short",
"EMAIL_IN_USE": "Email address already in use",
"EMAIL_DISPLAY_NAME_IN_USE": "Email address or display name already in use",
"DISPLAYNAME_IN_USE": "Display name already in use",
"DISPLAY_NAME_REQUIRED": "Must input a display name",
"NO_SPECIAL_CHARACTERS": "Display names can contain letters, numbers and _ only",
"PROFANITY_ERROR": "Display names must not contain profanity. Please contact the administrator if you believe this to be in error."
@@ -34,6 +36,8 @@
"PASSWORD_REQUIRED": "Debe ingresar una contraseña",
"PASSWORD_LENGTH": "La contraseña es muy corta",
"EMAIL_IN_USE": "La dirección de correo electrónico se encuentra en uso",
"EMAIL_DISPLAY_NAME_IN_USE": "Correo o Nombre en uso.",
"DISPLAYNAME_IN_USE": "Nombre en uso.",
"DISPLAY_NAME_REQUIRED": "Debe ingresar un nombre",
"NO_SPECIAL_CHARACTERS": "Los nombres pueden contener letras, números y _",
"PROFANITY_ERROR": "Los nombres no pueden contener blasfemias. Por favor contacte al administrador si cree que esto es un error"
+11
View File
@@ -0,0 +1,11 @@
import {createNetworkInterface} from 'apollo-client';
export default function getNetworkInterface(apiUrl = '/api/v1/graph/ql', headers = {}) {
return new createNetworkInterface({
uri: apiUrl,
opts: {
credentials: 'same-origin',
headers,
},
});
}
@@ -36,8 +36,8 @@ export default class AuthorName extends Component {
onMouseOver={this.handleMouseOver}
onMouseLeave={this.handleMouseLeave}
>
{author && author.displayName}
{ showTooltip && <Tooltip>
{author && author.name}
{ showTooltip && author.settings.bio && <Tooltip>
<div className={`${packagename}-bio`}>
{author.settings.bio}
</div>
@@ -1,29 +1,18 @@
import React from 'react';
import React, {PropTypes} from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
import has from 'lodash/has';
import reduce from 'lodash/reduce';
const name = 'coral-plugin-comment-count';
const CommentCount = ({items, id}) => {
let count = 0;
if (has(items, `assets.${id}.comments`)) {
count += items.assets[id].comments.length;
}
// lodash reduce works on {}
count += reduce(items.comments, (accum, comment) => {
if (comment.children) {
accum += comment.children.length;
}
return accum;
}, 0);
const CommentCount = ({count}) => {
return <div className={`${name}-text`}>
{`${count} ${count === 1 ? lang.t('comment') : lang.t('comment-plural')}`}
</div>;
};
CommentCount.propTypes = {
count: PropTypes.number.isRequired
};
export default CommentCount;
const lang = new I18n(translations);
+51 -37
View File
@@ -8,11 +8,15 @@ const name = 'coral-plugin-commentbox';
class CommentBox extends Component {
static propTypes = {
postItem: PropTypes.func,
updateItem: PropTypes.func,
id: PropTypes.string,
comments: PropTypes.array,
reply: PropTypes.bool,
// updateItem: PropTypes.func,
// comments: PropTypes.array,
commentPostedHandler: PropTypes.func,
postItem: PropTypes.func.isRequired,
assetId: PropTypes.string.isRequired,
parentId: PropTypes.string,
authorId: PropTypes.string.isRequired,
isReply: PropTypes.bool.isRequired,
canPost: PropTypes.bool,
currentUser: PropTypes.object
}
@@ -24,73 +28,83 @@ class CommentBox extends Component {
postComment = () => {
const {
// child_id,
// updateItem,
// appendItemArray,
commentPostedHandler,
postItem,
updateItem,
id,
parent_id,
child_id,
assetId,
parentId,
addNotification,
appendItemArray,
premod,
author
authorId
} = this.props;
let comment = {
body: this.state.body,
asset_id: id,
author_id: author.id
asset_id: assetId,
author_id: authorId,
parent_id: parentId
};
let related;
let parent_type;
if (parent_id) {
comment.parent_id = parent_id;
related = 'children';
parent_type = 'comments';
} else {
related = 'comments';
parent_type = 'assets';
}
if (child_id || parent_id) {
updateItem(child_id || parent_id, 'showReply', false, 'comments');
}
// let related;
// let parent_type;
// if (parent_id) {
// comment.parent_id = parent_id;
// related = 'children';
// parent_type = 'comments';
// } else {
// related = 'comments';
// parent_type = 'assets';
// }
// if (child_id || parent_id) {
// updateItem(child_id || parent_id, 'showReply', false, 'comments');
// }
if (this.props.charCount && this.state.body.length > this.props.charCount) {
return;
}
postItem(comment, 'comments')
.then((postedComment) => {
const commentId = postedComment.id;
if (postedComment.status === 'rejected') {
.then(({data}) => {
const postedComment = data.createComment;
// const commentId = postedComment.id;
if (postedComment.status === 'REJECTED') {
addNotification('error', lang.t('comment-post-banned-word'));
} else if (premod === 'pre') {
} else if (postedComment.status === 'PREMOD') {
addNotification('success', lang.t('comment-post-notif-premod'));
} else {
appendItemArray(parent_id || id, related, commentId, !parent_id, parent_type);
// appendItemArray(parent_id || id, related, commentId, !parent_id, parent_type);
addNotification('success', 'Your comment has been posted.');
}
if (commentPostedHandler) {
commentPostedHandler();
}
})
.catch((err) => console.error(err));
this.setState({body: ''});
}
render () {
const {styles, reply, author, charCount} = this.props;
const {styles, isReply, authorId, charCount} = this.props;
const length = this.state.body.length;
return <div>
<div
className={`${name}-container`}>
<label
htmlFor={ reply ? 'replyText' : 'commentText'}
htmlFor={ isReply ? 'replyText' : 'commentText'}
className="screen-reader-text"
aria-hidden={true}>
{reply ? lang.t('reply') : lang.t('comment')}
{isReply ? lang.t('reply') : lang.t('comment')}
</label>
<textarea
className={`${name}-textarea`}
style={styles && styles.textarea}
value={this.state.body}
placeholder={lang.t('comment')}
id={reply ? 'replyText' : 'commentText'}
id={isReply ? 'replyText' : 'commentText'}
onChange={(e) => this.setState({body: e.target.value})}
rows={3}/>
</div>
@@ -101,7 +115,7 @@ class CommentBox extends Component {
}
</div>
<div className={`${name}-button-container`}>
{ author && (
{ authorId && (
<Button
cStyle={!length || (charCount && length > charCount) ? 'lightGrey' : 'darkGrey'}
className={`${name}-button`}
+1 -1
View File
@@ -9,7 +9,7 @@ const getPopupMenu = [
() => {
return {
header: lang.t('step-2-header'),
itemType: 'users',
itemType: 'USERS',
field: 'bio',
options: [
{val: 'This bio is offensive', text: lang.t('bio-offensive')},
+33 -24
View File
@@ -14,22 +14,31 @@ class FlagButton extends Component {
reason: '',
note: '',
step: 0,
posted: false
localPost: null,
localDelete: false
}
// When the "report" button is clicked expand the menu
onReportClick = () => {
if (!this.props.currentUser) {
const {currentUser, flag, deleteAction} = this.props;
const {localPost, localDelete} = this.state;
const flagged = (flag && flag.current && !localDelete) || localPost;
if (!currentUser) {
const offset = document.getElementById(`c_${this.props.id}`).getBoundingClientRect().top - 75;
this.props.showSignInDialog(offset);
return;
}
this.setState({showMenu: !this.state.showMenu});
if (flagged) {
this.setState((prev) => prev.localPost ? {...prev, localPost: null, step: 0} : {...prev, localDelete: true});
deleteAction(localPost || flag.current.id);
} else {
this.setState({showMenu: !this.state.showMenu});
}
}
onPopupContinue = () => {
const {postAction, addItem, updateItem, flag, id, author_id} = this.props;
const {itemType, field, reason, step, note, posted} = this.state;
const {postAction, id, author_id} = this.props;
const {itemType, reason, step, localPost} = this.state;
// Proceed to the next step or close the menu if we've reached the end
if (step + 1 >= this.props.getPopupMenu.length) {
@@ -39,33 +48,32 @@ class FlagButton extends Component {
}
// If itemType and reason are both set, post the action
if (itemType && reason && !posted) {
if (itemType && reason && !localPost) {
// Set the text from the "other" field if it exists.
let item_id;
switch(itemType) {
case 'comments':
case 'COMMENTS':
item_id = id;
break;
case 'users':
case 'USERS':
item_id = author_id;
break;
}
const action = {
action_type: `flag_${field}`,
metadata: {
field,
reason,
note
// Note: Action metadata has been temporarily removed.
if (itemType === 'COMMENTS') {
this.setState({localPost: 'temp'});
}
postAction({
item_id,
item_type: itemType,
action_type: 'FLAG'
}).then(({data}) => {
if (itemType === 'COMMENTS') {
this.setState({localPost: data.createAction.id});
}
};
postAction(item_id, itemType, action)
.then((action) => {
let id = `${action.action_type}_${action.item_id}`;
addItem({id, current_user: action, count: flag ? flag.count + 1 : 1}, 'actions');
updateItem(action.item_id, action.action_type, id, action.item_type);
this.setState({posted: true});
});
});
}
}
@@ -98,7 +106,8 @@ class FlagButton extends Component {
render () {
const {flag, getPopupMenu} = this.props;
const flagged = flag && flag.current_user;
const {localPost, localDelete} = this.state;
const flagged = (flag && flag.current && !localDelete) || localPost;
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
return <div className={`${name}-container`}>
@@ -106,7 +115,7 @@ class FlagButton extends Component {
{
flagged
? <span className={`${name}-button-text`}>{lang.t('reported')}</span>
: <span className={`${name}-button-text`}>{lang.t('report')}</span>
: <span className={`${name}-button-text`}>{lang.t('report')}</span>
}
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
style={flagged ? styles.flaggedIcon : {}}
+3 -3
View File
@@ -10,15 +10,15 @@ const getPopupMenu = [
return {
header: lang.t('step-1-header'),
options: [
{val: 'users', text: lang.t('flag-username')},
{val: 'comments', text: lang.t('flag-comment')}
{val: 'USERS', text: lang.t('flag-username')},
{val: 'COMMENTS', text: lang.t('flag-comment')}
],
button: lang.t('continue'),
sets: 'itemType'
};
},
(itemType) => {
const options = itemType === 'comments' ?
const options = itemType === 'COMMENTS' ?
[
{val: 'I don\'t agree with this comment', text: lang.t('no-agree-comment')},
{val: 'This comment is offensive', text: lang.t('comment-offensive')},
+1 -2
View File
@@ -1,12 +1,11 @@
import React, {PropTypes} from 'react';
import styles from './Comment.css';
const Comment = props => {
return (
<div className={styles.myComment}>
<p className="myCommentAsset">
<a className={`${styles.assetURL} myCommentAnchor`} href={`${props.asset.url}#${props.comment.id}`}>{props.asset.url}</a>
<a className={`${styles.assetURL} myCommentAnchor`} href='#' onClick={props.link(`${props.asset.url}#${props.comment.id}`)}>{props.asset.url}</a>
</p>
<p className={`${styles.commentBody} myCommentBody`}>{props.comment.body}</p>
</div>
@@ -11,6 +11,7 @@ const CommentHistory = props => {
return <Comment
key={i}
comment={comment}
link={props.link}
asset={asset} />;
})}
</div>
+66 -42
View File
@@ -1,52 +1,76 @@
import React from 'react';
import React, {Component, PropTypes} from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
const name = 'coral-plugin-likes';
const LikeButton = ({like, id, postAction, deleteAction, addItem, showSignInDialog, updateItem, currentUser, banned}) => {
const liked = like && like.current_user;
const onLikeClick = () => {
if (!currentUser) {
const offset = document.getElementById(`c_${id}`).getBoundingClientRect().top - 75;
showSignInDialog(offset);
return;
}
if (banned) {
return;
}
if (!liked) {
const action = {
action_type: 'like'
};
postAction(id, 'comments', action)
.then((action) => {
let id = `${action.action_type}_${action.item_id}`;
addItem({id, current_user: action, count: like ? like.count + 1 : 1}, 'actions');
updateItem(action.item_id, action.action_type, id, 'comments');
});
} else {
deleteAction(liked.id)
.then(() => {
updateItem(like.id, 'count', like.count - 1, 'actions');
updateItem(like.id, 'current_user', false, 'actions');
});
}
};
class LikeButton extends Component {
return <div className={`${name}-container`}>
<button onClick={onLikeClick} className={`${name}-button ${liked && 'likedButton'}`}>
{
liked
? <span className={`${name}-button-text`}>{lang.t('liked')}</span>
: <span className={`${name}-button-text`}>{lang.t('like')}</span>
static propTypes = {
like: PropTypes.shape({
current: PropTypes.obect,
count: PropTypes.number
}),
id: PropTypes.string,
postAction: PropTypes.func.isRequired,
deleteAction: PropTypes.func.isRequired,
showSignInDialog: PropTypes.func.isRequired,
currentUser: PropTypes.shape({
banned: PropTypes.boolean
}),
}
state = {
localPost: null, // Set to the ID of an action if one is posted
localDelete: false // Set to true is the user deletes an action, unless localPost is already set.
}
render() {
const {like, id, postAction, deleteAction, showSignInDialog, currentUser} = this.props;
const {localPost, localDelete} = this.state;
const liked = (like && like.current && !localDelete) || localPost;
let count = like ? like.count : 0;
if (localPost) {count += 1;}
if (localDelete) {count -= 1;}
const onLikeClick = () => {
if (!currentUser) {
const offset = document.getElementById(`c_${id}`).getBoundingClientRect().top - 75;
showSignInDialog(offset);
return;
}
<i className={`${name}-icon material-icons`}
aria-hidden={true}>thumb_up</i>
<span className={`${name}-like-count`}>{like && like.count > 0 && like.count}</span>
</button>
</div>;
};
if (currentUser.banned) {
return;
}
if (!liked) {
this.setState({localPost: 'temp', localDelete: false});
postAction({
item_id: id,
item_type: 'COMMENTS',
action_type: 'LIKE'
}).then(({data}) => {
this.setState({localPost: data.createAction.id});
});
} else {
this.setState((prev) => prev.localPost ? {...prev, localPost: null} : {...prev, localDelete: true});
deleteAction(localPost || like.current.id);
}
};
return <div className={`${name}-container`}>
<button onClick={onLikeClick} className={`${name}-button ${liked && 'likedButton'}`}>
{
liked
? <span className={`${name}-button-text`}>{lang.t('liked')}</span>
: <span className={`${name}-button-text`}>{lang.t('like')}</span>
}
<i className={`${name}-icon material-icons`}
aria-hidden={true}>thumb_up</i>
<span className={`${name}-like-count`}>{count > 0 && count}</span>
</button>
</div>;
}
}
export default LikeButton;
+22 -11
View File
@@ -1,17 +1,28 @@
import React from 'react';
import React, {PropTypes} 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
{...props}
comments = {props.child}
reply = {true}/>
}
</div>;
const ReplyBox = ({styles, postItem, assetId, authorId, addNotification, parentId, commentPostedHandler}) => (
<div className={`${name}-textarea`} style={styles && styles.container}>
<CommentBox
commentPostedHandler={commentPostedHandler}
parentId={parentId}
addNotification={addNotification}
authorId={authorId}
assetId={assetId}
postItem={postItem}
isReply={true} />
</div>
);
ReplyBox.propTypes = {
commentPostedHandler: PropTypes.func,
parentId: PropTypes.string,
addNotification: PropTypes.func.isRequired,
authorId: PropTypes.string.isRequired,
postItem: PropTypes.func.isRequired,
assetId: PropTypes.string.isRequired
};
export default ReplyBox;
+17 -13
View File
@@ -1,21 +1,25 @@
import React from 'react';
import React, {PropTypes} from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
const name = 'coral-plugin-replies';
const ReplyButton = (props) => <button
className={`${name}-reply-button`}
onClick={() => {
if (props.banned) {
return;
}
props.updateItem(props.id, 'showReply', !props.showReply, 'comments');
}}>
{lang.t('reply')}
<i className={`${name}-icon material-icons`}
aria-hidden={true}>reply</i>
</button>;
const ReplyButton = ({banned, onClick}) => {
return (
<button
className={`${name}-reply-button`}
onClick={onClick}>
{lang.t('reply')}
<i className={`${name}-icon material-icons`}
aria-hidden={true}>{banned ? 'BANNED' : 'reply'}</i>
</button>
);
};
ReplyButton.propTypes = {
onClick: PropTypes.func.isRequired,
banned: PropTypes.bool.isRequired
};
export default ReplyButton;
@@ -0,0 +1,58 @@
import React, {Component} from 'react';
import {graphql} from 'react-apollo';
import gql from 'graphql-tag';
export class RileysAwesomeCommentBox extends Component {
postComment() {
console.log(this.props);
console.log('postComment', this.props.asset_id);
this.props.mutate({
variables: {
asset_id: this.props.asset_id,
body: this.textarea.value,
parent_id: null
}
}).then(({data}) => {
console.log('it workt');
console.log(data);
});
}
render() {
return <div>
<textarea ref={textarea => this.textarea = textarea}></textarea>
<button onClick={this.postComment.bind(this)}>POST</button>
</div>;
}
}
const postComment = gql`
fragment commentView on Comment {
id
body
user {
name: displayName
}
actions {
type: action_type
count
current: current_user {
id
created_at
}
}
}
mutation CreateComment ($asset_id: ID!, $parent_id: ID, $body: String!) {
createComment(asset_id:$asset_id, parent_id:$parent_id, body:$body) {
...commentView
}
}
`;
const RileysAwesomeCommentBoxWithData = graphql(
postComment
)(RileysAwesomeCommentBox);
export default RileysAwesomeCommentBoxWithData;
+97
View File
@@ -0,0 +1,97 @@
import React, {Component} from 'react';
import {graphql} from 'react-apollo';
import gql from 'graphql-tag';
import {fetchSignIn} from 'coral-framework/actions/auth';
import RileysAwesomeCommentBox from 'coral-plugin-stream/RileysAwesomeCommentBox';
const assetID = '6187a94b-0b6d-4a96-ac6b-62b529cd8410';
// MyComponent is a "presentational" or apollo-unaware component,
// It could be a simple React class:
class Stream extends Component {
constructor(props) {
super(props);
}
logMeIn() {
fetchSignIn({email: 'your@example.com', password: 'dfasidfaisdufoiausdfoiuaspdoifas'})(() => {});
}
render() {
console.log(this.props);
const {data} = this.props;
return <div>
<button onClick={this.logMeIn.bind(this)}>Login or whatever</button>
{
data.loading
? 'loading!'
: <div>
<RileysAwesomeCommentBox asset_id={data.asset.id} />
<p>Asset ID: {data.asset.id}</p>
<ul>
{
data.asset.comments.map(comment => {
return <li key={comment.id}>
{comment.body} [{comment.id}]
<ul>
{
comment.replies.map(reply => {
return <li key={reply.id}>{reply.body}</li>;
})
}
</ul>
</li>;
})
}
</ul>
</div>
}
</div>;
}
}
// Initialize GraphQL queries or mutations with the `gql` tag
const StreamQuery = gql`fragment commentView on Comment {
id
body
user {
name: displayName
}
actions {
type: action_type
count
current: current_user {
id
created_at
}
}
}
query AssetQuery($asset_id: ID!) {
asset(id: $asset_id) {
id
title
url
comments {
...commentView
replies {
...commentView
}
}
}
}`;
// We then can use `graphql` to pass the query results returned by MyQuery
// to MyComponent as a prop (and update them as the results change)
const StreamWithData = graphql(
StreamQuery, {
options: {
variables: {
asset_id: assetID
}
}
}
)(Stream);
export default StreamWithData;
@@ -1,10 +1,15 @@
import React from 'react';
import styles from './NotLoggedIn.css';
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
export default ({showSignInDialog}) => (
<div className={styles.message}>
<SignInContainer noButton={true}/>
<div>
<a onClick={showSignInDialog}>Sign In</a> to access Settings
<a onClick={() => {
console.log('Signin click');
showSignInDialog();
}}>Sign In</a> to access Settings
</div>
<div>
From the Settings Page you can
@@ -4,7 +4,11 @@ import styles from './SettingsHeader.css';
export default ({userData}) => (
<div className={styles.header}>
<h1>{userData.displayName}</h1>
<h2>{userData.profiles.map(profile => profile.id)}</h2>
{
// Hiding display of users ID unless there's a use case for it.
// <h2>{userData.profiles.map(profile => profile.id)}</h2>
}
</div>
);
@@ -3,6 +3,7 @@ import {connect} from 'react-redux';
import {saveBio, fetchCommentsByUserId} from 'coral-framework/actions/user';
import {link} from 'coral-framework/PymConnection';
import BioContainer from './BioContainer';
import NotLoggedIn from '../components/NotLoggedIn';
import {TabBar, Tab, TabContent} from '../../coral-ui';
@@ -62,6 +63,7 @@ class SettingsContainer extends Component {
user.myComments.length && user.myAssets.length
? <CommentHistory
comments={commentsMostRecentFirst}
link={link}
assets={user.myAssets.map(id => items.assets[id])} />
: <p>{lang.t('user-no-comment')}</p>
}
@@ -3,7 +3,7 @@ import {Dialog} from 'coral-ui';
import styles from './styles.css';
import SignInContent from './SignInContent';
import SingUpContent from './SignUpContent';
import SignUpContent from './SignUpContent';
import ForgotContent from './ForgotContent';
const SignDialog = ({open, view, handleClose, offset, ...props}) => (
@@ -17,7 +17,7 @@ const SignDialog = ({open, view, handleClose, offset, ...props}) => (
}}>
<span className={styles.close} onClick={handleClose}>×</span>
{view === 'SIGNIN' && <SignInContent {...props} />}
{view === 'SIGNUP' && <SingUpContent {...props} />}
{view === 'SIGNUP' && <SignUpContent {...props} />}
{view === 'FORGOT' && <ForgotContent {...props} />}
</Dialog>
);
@@ -56,7 +56,7 @@ const SignUpContent = ({handleChange, formData, ...props}) => (
onChange={handleChange}
minLength="8"
/>
{ !props.errors.password && <span className={styles.hint}> Password must be at least 8 characters. </span> }
{ props.errors.password && <span className={styles.hint}> Password must be at least 8 characters. </span> }
<FormField
id="confirmPassword"
type="password"
+2
View File
@@ -19,6 +19,7 @@ export default {
alreadyHaveAnAccount: 'Already have an account?',
recoverPassword: 'Recover password',
emailInUse: 'Email address already in use',
emailORusernameInUse: 'Email address or Username already in use',
requiredField: 'This field is required',
passwordsDontMatch: 'Passwords don\'t match.',
specialCharacters: 'Display names can contain letters, numbers and _ only',
@@ -45,6 +46,7 @@ export default {
alreadyHaveAnAccount: 'Ya tienes una cuenta?',
recoverPassword: 'Recuperar contraseña',
emailInUse: 'Este email se encuentra en uso',
emailORusernameInUse: 'Este email ó nombre se encuentran en uso',
requiredField: 'Este campo es requerido',
passwordsDontMatch: 'Las contraseñas no coinciden',
specialCharacters: 'Los nombres pueden contener letras, números y _',
+9 -4
View File
@@ -11,8 +11,6 @@
display: inline-block;
font-family: 'Roboto','Helvetica','Arial',sans-serif;
font-size: 14px;
font-weight: 500;
letter-spacing: 0;
overflow: hidden;
will-change: box-shadow,transform;
-webkit-transition: box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);
@@ -24,6 +22,14 @@
line-height: 28px;
vertical-align: middle;
margin: 2px;
letter-spacing: 0.7px;
font-weight: 400;
i {
margin-right: 13px;
font-size: 18px;
vertical-align: middle;
}
}
.type--black {
@@ -69,7 +75,7 @@
.type--darkGrey {
color: white;
background: #696969;
background: #616161;
}
.type--darkGrey:hover {
@@ -104,6 +110,5 @@
}
.raised {
background: rgba(158,158,158,.2);
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
}
+5 -3
View File
@@ -1,17 +1,19 @@
import React from 'react';
import styles from './Button.css';
import Icon from './Icon';
const Button = ({cStyle = 'local', children, className, raised, full, ...props}) => (
const Button = ({cStyle = 'local', children, className, raised = false, full = false, icon = '', ...props}) => (
<button
className={`
${styles.button}
${styles[`type--${cStyle}`]}
${className}
${full && styles.full}
${raised && styles.button}
${full ? styles.full : ''}
${raised ? styles.raised : ''}
`}
{...props}
>
{icon && <Icon name={icon} />}
{children}
</button>
);
+35
View File
@@ -0,0 +1,35 @@
.base {
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
font-size: 16px;
font-weight: 400;
min-height: 200px;
overflow: hidden;
width: 330px;
z-index: 1;
position: relative;
background: #fff;
border-radius: 2px;
box-sizing: border-box;
width: 100%;
padding: 20px;
}
.shadow--4 {
box-shadow: 0 4px 5px 0 rgba(0,0,0,.14), 0 1px 10px 0 rgba(0,0,0,.12), 0 2px 4px -1px rgba(0,0,0,.2);
}
.shadow--2{
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
}
.shadow--3 {
box-shadow: 0 3px 4px 0 rgba(0,0,0,.14), 0 3px 3px -2px rgba(0,0,0,.2), 0 1px 8px 0 rgba(0,0,0,.12);
}
+8
View File
@@ -0,0 +1,8 @@
import React from 'react';
import styles from './Card.css';
export default ({children, className, shadow = 2, ...props}) => (
<div className={`${styles.base} ${className} ${styles[`shadow--${shadow}`]}`} {...props}>
{children}
</div>
);
+8
View File
@@ -0,0 +1,8 @@
.base {
height: 30px;
width : 30px;
}
.mark {
stroke: #FFFFFF;
}
+4 -3
View File
@@ -1,9 +1,10 @@
import React, {PropTypes} from 'react';
import styles from './CoralLogo.css';
const CoralLogo = ({height = '30px', width = '30px', stroke = '#FFFFFF'}) => (
<svg width={width} height={height} viewBox='0 0 381 391' version='1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink'>
const CoralLogo = ({className = ''}) => (
<svg className={`${styles.base} ${className}`} viewBox='0 0 381 391' version='1.1 xmlns=http://www.w3.org/2000/svg xmlns:xlink=http://www.w3.org/1999/xlink'>
<g stroke='none' strokeWidth='1' fill='none' fillRule='evenodd'>
<g id='Wordmark-Round' transform='translate(-1833.000000, -411.000000)' stroke={stroke} strokeWidth='22' strokeLinecap='round' strokeLinejoin='round'>
<g id='Wordmark-Round' className={styles.mark} transform='translate(-1833.000000, -411.000000)' strokeWidth='22' strokeLinecap='round' strokeLinejoin='round'>
<g id='coralProjectLogo-2-Copy-2' transform='translate(1842.000000, 421.000000)'>
<g id='Layer_2' transform='translate(2.268750, 1.133903)'>
<rect id='Rectangle-1' fill='#F47E6B' x='0' y='0' width='358.4625' height='368.518519' rx='40'>
+20 -4
View File
@@ -1,9 +1,25 @@
.base {
background: red;
i {
font-size: 30px;
transform: translate(-14px,-12px) !important;
}
}
.type--approve {
background: #00796b;
color: rgba(255, 255, 255, 0.901961);
background: #388E3C;
color: rgba(255, 255, 255, 0.901961);
}
.type--approve:hover {
background: #40a244;
}
.type--reject {
background: #d32f2f ;
color: rgba(255, 255, 255, 0.901961);
background: #D32F2F ;
color: rgba(255, 255, 255, 0.901961);
}
.type--reject:hover {
background: #e53333;
}
+1 -1
View File
@@ -3,7 +3,7 @@ import styles from './FabButton.css';
import {FABButton, Icon} from 'react-mdl';
const FabButton = ({cStyle = 'local', icon, className, ...props}) => (
<FABButton className={`${styles[`type--${cStyle}`]} ${className ? className : ''}`} {...props}>
<FABButton className={`${styles.base} ${styles[`type--${cStyle}`]} ${className ? className : ''}`} {...props}>
<Icon name={icon} />
</FABButton>
);
+8
View File
@@ -0,0 +1,8 @@
import React from 'react';
import {Icon as IconMDL} from 'react-mdl';
const Icon = ({className, name}) => (
<IconMDL className={className} name={name} />
);
export default Icon;
+24
View File
@@ -0,0 +1,24 @@
.base {
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
background-color: white;
width: 208px;
padding: 10px 14px;
font-size: 14px;
margin-bottom: 10px;
list-style: none;
&:hover {
cursor: pointer;
}
&.active {
color: white;
background-color: #262626;
}
i {
margin-right: 13px;
font-size: 18px;
vertical-align: middle;
}
}
+13
View File
@@ -0,0 +1,13 @@
import React from 'react';
import styles from './Item.css';
import Icon from './Icon';
export default ({children, itemId, active, onItemClick, className = '', icon}) => (
<li
className={`${styles.base} ${className} ${active ? styles.active : ''}`}
onClick={() => onItemClick(itemId)}
>
{icon && <Icon name={icon} />}
{children}
</li>
);
+4
View File
@@ -0,0 +1,4 @@
.base {
padding: 0;
margin: 0;
}
+32
View File
@@ -0,0 +1,32 @@
import React, {Component} from 'react';
import styles from './List.css';
export default class List extends Component {
constructor(props) {
super(props);
this.handleClickItem = this.handleClickItem.bind(this);
}
handleClickItem(itemId) {
if (this.props.onChange) {
this.props.onChange(itemId);
}
}
render() {
const {children, activeItem, className = ''} = this.props;
return (
<ul className={`${styles.base} ${className}`}>
{React.Children.toArray(children)
.filter(child => !child.props.restricted)
.map((child, i) =>
React.cloneElement(child, {
i,
active: child.props.itemId === activeItem,
onItemClick: this.handleClickItem,
})
)}
</ul>
);
}
}
+14 -5
View File
@@ -1,10 +1,19 @@
.li {
.pager {
text-align: center;
li {
display: inline-block;
margin-right: 5px;
padding: 0;
min-width: 30px;
color: white;
height: 30px;
text-align: center;
vertical-align: middle;
line-height: 30px;
width: 30px;
}
}
.current {
background: #e3edf3;
}
background: #696969;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
}
+2 -4
View File
@@ -2,19 +2,18 @@ import React, {PropTypes} from 'react';
import styles from './Pager.css';
const Rows = (curr, total, onClickHandler) => Array.from(Array(total)).map((e, i) =>
<li className={`mdl-button mdl-js-button ${styles.li} ${curr === i ? styles.current : ''}`}
<li className={curr === i ? styles.current : ''}
key={i} onClick={() => onClickHandler(i + 1)}>
{i + 1}
</li>
);
const Pager = ({totalPages, page, onNewPageHandler}) => (
<div className="pager">
<div className={styles.pager}>
<ul>
{
(totalPages > page && totalPages > 1) ?
<li
className={`mdl-button mdl-js-button ${styles.li}`}
onClick={() => onNewPageHandler(page - 1)}>
Prev
</li>
@@ -25,7 +24,6 @@ const Pager = ({totalPages, page, onNewPageHandler}) => (
{
(page < totalPages && totalPages > 1) ?
<li
className={`mdl-button mdl-js-button ${styles.li}`}
onClick={() => onNewPageHandler(page + 1)}>
Next
</li>
+1 -1
View File
@@ -1,7 +1,7 @@
import React from 'react';
import styles from './TabBar.css';
export class TabBar extends React.Component {
class TabBar extends React.Component {
constructor(props) {
super(props);
this.handleClickTab = this.handleClickTab.bind(this);
+5
View File
@@ -9,3 +9,8 @@ export {default as Spinner} from './components/Spinner';
export {default as Tooltip} from './components/Tooltip';
export {default as PopupMenu} from './components/PopupMenu';
export {default as Checkbox} from './components/Checkbox';
export {default as Icon} from './components/Icon';
export {default as List} from './components/List';
export {default as Item} from './components/Item';
export {default as Card} from './components/Card';
export {default as Pager} from './components/Pager';

Some files were not shown because too many files have changed in this diff Show More