mirror of
https://github.com/wassname/talk.git
synced 2026-08-13 12:40:11 +08:00
Merge branch 'graphql' of github.com:coralproject/talk into graphql
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -77,7 +79,22 @@ app.use(session(session_opts));
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
app.use('/api/v1/graph', require('./routes/api/graph'));
|
||||
//==============================================================================
|
||||
// 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
|
||||
|
||||
@@ -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(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>
|
||||
|
||||
@@ -7,9 +7,8 @@ import styles from './CommentList.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 @@ export default 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 = (action, i, props) => {
|
||||
}
|
||||
if (action === '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>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -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;
|
||||
@@ -71,25 +91,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 {
|
||||
@@ -143,3 +158,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,6 +52,7 @@ 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>
|
||||
@@ -60,6 +61,7 @@ class Table extends Component {
|
||||
</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>
|
||||
|
||||
@@ -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,
|
||||
@@ -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>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
|
||||
<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
|
||||
|
||||
@@ -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,14 +12,35 @@ const lang = new I18n(translations);
|
||||
|
||||
export default ({onTabClick, ...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='#pending' onClick={() => onTabClick('pending')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.pending')}</a>
|
||||
<a href='#rejected' onClick={() => onTabClick('rejected')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.rejected')}</a>
|
||||
<a href='#flagged' onClick={() => onTabClick('flagged')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.flagged')}</a>
|
||||
<a href='#pending'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onTabClick('pending');
|
||||
}}
|
||||
className={`mdl-tabs__tab ${styles.tab} ${props.activeTab === 'pending' ? styles.active : ''}`}
|
||||
>
|
||||
{lang.t('modqueue.pending')}
|
||||
</a>
|
||||
<a href='#rejected'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
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();
|
||||
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='pending'>
|
||||
{
|
||||
|
||||
@@ -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>;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,13 +46,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",
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import Pym from 'pym.js';
|
||||
import {graphql} from 'react-apollo';
|
||||
import {connect} from 'react-redux';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import {
|
||||
// itemActions,
|
||||
itemActions,
|
||||
|
||||
// Notification,
|
||||
// notificationActions,
|
||||
notificationActions,
|
||||
authActions
|
||||
} from '../../coral-framework';
|
||||
|
||||
// import CommentBox from '../../coral-plugin-commentbox/CommentBox';
|
||||
// import InfoBox from '../../coral-plugin-infobox/InfoBox';
|
||||
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';
|
||||
@@ -21,22 +23,22 @@ 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 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 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;
|
||||
const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions;
|
||||
const {addNotification, clearNotification} = notificationActions;
|
||||
const {logout, showSignInDialog} = authActions;
|
||||
|
||||
const assetID = 'bc7b4cef-1e14-46e1-9db4-66465192f168';
|
||||
const assetID = 'be97065d-f0eb-44a8-8efb-b416229afdc3';
|
||||
|
||||
class CommentStream extends Component {
|
||||
|
||||
@@ -44,7 +46,8 @@ class CommentStream extends Component {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
activeTab: 0
|
||||
activeTab: 0,
|
||||
showSignInDialog: false
|
||||
};
|
||||
|
||||
this.changeTab = this.changeTab.bind(this);
|
||||
@@ -99,17 +102,20 @@ class CommentStream extends Component {
|
||||
// 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 {isAdmin, showSignInDialog} = this.props.auth;
|
||||
const {activeTab} = this.state;
|
||||
|
||||
// const banned = (this.props.userData.status === 'banned');
|
||||
|
||||
const {loading, asset} = this.props.data;
|
||||
const {loading, asset, currentUser, refetch} = this.props.data;
|
||||
|
||||
// const {status, moderation, closedMessage, charCount, charCountEnable} = asset.settings;
|
||||
|
||||
const expandForLogin = showSignInDialog ? {
|
||||
minHeight: document.body.scrollHeight + 150
|
||||
} : {};
|
||||
|
||||
return <div style={expandForLogin}>
|
||||
{
|
||||
loading ? <Spinner/>
|
||||
@@ -117,36 +123,39 @@ class CommentStream extends Component {
|
||||
<TabBar onChange={this.changeTab} activeTab={activeTab}>
|
||||
<Tab><Count count={asset.comments.length}/></Tab>
|
||||
<Tab>Settings</Tab>
|
||||
<Tab restricted={false /* !isAdmin*/}>Configure Stream</Tab>
|
||||
<Tab restricted={!isAdmin}>Configure Stream</Tab>
|
||||
</TabBar>
|
||||
{/* loggedIn && <UserBox user={user} logout={this.props.logout} />*/}
|
||||
{currentUser && <UserBox user={currentUser} logout={this.props.logout} />}
|
||||
<TabContent show={activeTab === 0}>
|
||||
{
|
||||
|
||||
// status === 'open'
|
||||
// ? <div id="commentBox">
|
||||
// <InfoBox
|
||||
// content={''/*this.props.config.infoBoxContent*/}
|
||||
// enable={false/*this.props.config.infoBoxEnable*/}
|
||||
// />
|
||||
// <RestrictedContent restricted={false/*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={false/*banned*/}
|
||||
// author={user}
|
||||
// charCount={charCountEnable && charCount}/>
|
||||
// </RestrictedContent>
|
||||
// </div>
|
||||
// : <p>{closedMessage}</p>
|
||||
status === 'open'
|
||||
? <div id="commentBox">
|
||||
<InfoBox
|
||||
content={asset.settings.infoBoxContent}
|
||||
enable={asset.settings.infoBoxEnable}
|
||||
/>
|
||||
<RestrictedContent restricted={false} restrictedComp={<SuspendedAccount />}>
|
||||
<CommentBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
updateItem={this.props.updateItem}
|
||||
id={asset.settings.rootItemId}
|
||||
premod={asset.settings.moderation}
|
||||
reply={false}
|
||||
currentUser={this.props.auth.user}
|
||||
banned={false}
|
||||
author={currentUser}
|
||||
charCount={asset.settings.charCountEnable && asset.settings.charCount}/>
|
||||
</RestrictedContent>
|
||||
</div>
|
||||
: <p>{asset.settings.closedMessage}</p>
|
||||
}
|
||||
{
|
||||
!currentUser && <SignInContainer
|
||||
refetch={refetch}
|
||||
showSignInDialog={showSignInDialog}/>
|
||||
}
|
||||
{/* !loggedIn && <SignInContainer offset={signInOffset}/>*/}
|
||||
{
|
||||
asset.comments.map((comment) => {
|
||||
return <div className="comment" key={comment.id} id={`c_${comment.id}`}>
|
||||
@@ -161,14 +170,14 @@ class CommentStream extends Component {
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={null/* this.props.auth.user*/}/>
|
||||
currentUser={currentUser}/>
|
||||
<PubDate created_at={comment.created_at}/>
|
||||
<Content body={comment.body}/>
|
||||
<div className="commentActionsLeft">
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
id={comment.id}
|
||||
currentUser={null/* this.props.auth.user*/}
|
||||
currentUser={currentUser}
|
||||
showReply={comment.showReply}
|
||||
banned={false/* banned*/}/>
|
||||
<LikeButton
|
||||
@@ -180,7 +189,7 @@ class CommentStream extends Component {
|
||||
deleteAction={this.props.deleteAction}
|
||||
addItem={this.props.addItem}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={null/* this.props.auth.user*/}
|
||||
currentUser={currentUser}
|
||||
banned={false/* banned*/}/>
|
||||
</div>
|
||||
<div className="commentActionsRight">
|
||||
@@ -195,7 +204,7 @@ class CommentStream extends Component {
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
updateItem={this.props.updateItem}
|
||||
banned={false/* banned*/}
|
||||
currentUser={null/* this.props.auth.user*/}/>
|
||||
currentUser={currentUser}/>
|
||||
<PermalinkButton
|
||||
commentId={comment.id}
|
||||
articleURL={this.path}/>
|
||||
@@ -235,7 +244,7 @@ class CommentStream extends Component {
|
||||
addItem={this.props.addItem}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
updateItem={this.props.updateItem}
|
||||
currentUser={this.props.auth.user}
|
||||
currentUser={currentUser}
|
||||
banned={false/* banned*/}/>
|
||||
</div>
|
||||
<div className="replyActionsRight">
|
||||
@@ -278,11 +287,14 @@ class CommentStream extends Component {
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
<Notification
|
||||
notifLength={4500}
|
||||
clearNotification={this.props.clearNotification}
|
||||
notification={{text: null}}
|
||||
/>
|
||||
{
|
||||
|
||||
// <Notification
|
||||
// notifLength={4500}
|
||||
// clearNotification={this.props.clearNotification}
|
||||
// notification={{text: null}}
|
||||
// />
|
||||
}
|
||||
</TabContent>
|
||||
{
|
||||
|
||||
@@ -313,28 +325,23 @@ class CommentStream extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// 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())
|
||||
// });
|
||||
const mapStateToProps = state => (state);
|
||||
|
||||
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()),
|
||||
});
|
||||
|
||||
// Initialize GraphQL queries or mutations with the `gql` tag
|
||||
const StreamQuery = gql`fragment commentView on Comment {
|
||||
id
|
||||
@@ -358,16 +365,30 @@ query AssetQuery($asset_id: ID!) {
|
||||
id
|
||||
title
|
||||
url
|
||||
settings {
|
||||
moderation
|
||||
infoBoxEnable
|
||||
infoBoxContent
|
||||
closeTimeout
|
||||
closedMessage
|
||||
charCountEnable
|
||||
charCount
|
||||
requireEmailConfirmation
|
||||
}
|
||||
comments {
|
||||
...commentView
|
||||
replies {
|
||||
...commentView
|
||||
}
|
||||
}
|
||||
},
|
||||
currentUser: me {
|
||||
id,
|
||||
displayName
|
||||
}
|
||||
}`;
|
||||
|
||||
export default graphql(
|
||||
const CommentStreamWithData = graphql(
|
||||
StreamQuery, {
|
||||
options: {
|
||||
variables: {
|
||||
@@ -376,3 +397,8 @@ export default graphql(
|
||||
}
|
||||
}
|
||||
)(CommentStream);
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(CommentStreamWithData);
|
||||
|
||||
@@ -20,17 +20,21 @@ export const cleanState = () => ({type: actions.CLEAN_STATE});
|
||||
// Sign In Actions
|
||||
|
||||
const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST});
|
||||
|
||||
const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin});
|
||||
const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error});
|
||||
|
||||
export const fetchSignIn = (formData) => (dispatch) => {
|
||||
dispatch(signInRequest());
|
||||
coralApi('/auth/local', {method: 'POST', body: formData})
|
||||
.then(({user}) => {
|
||||
return coralApi('/auth/local', {method: 'POST', body: formData})
|
||||
.then((user) => {
|
||||
|
||||
const isAdmin = !!user.roles.filter(i => i === 'admin').length;
|
||||
dispatch(signInSuccess(user, isAdmin));
|
||||
dispatch(hideSignInDialog());
|
||||
dispatch(addItem(user, 'users'));
|
||||
|
||||
// dispatch(hideSignInDialog());
|
||||
|
||||
// dispatch(addItem(user, 'users'));
|
||||
})
|
||||
.catch(() => dispatch(signInFailure(lang.t('error.emailPasswordError'))));
|
||||
};
|
||||
@@ -75,7 +79,7 @@ const signUpFailure = error => ({type: actions.FETCH_SIGNUP_FAILURE, error});
|
||||
export const fetchSignUp = formData => (dispatch) => {
|
||||
dispatch(signUpRequest());
|
||||
|
||||
coralApi('/users', {method: 'POST', body: formData})
|
||||
return coralApi('/users', {method: 'POST', body: formData})
|
||||
.then(({user}) => {
|
||||
dispatch(signUpSuccess(user));
|
||||
setTimeout(() =>{
|
||||
@@ -95,7 +99,7 @@ const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILU
|
||||
|
||||
export const fetchForgotPassword = email => (dispatch) => {
|
||||
dispatch(forgotPassowordRequest(email));
|
||||
coralApi('/account/password/reset', {method: 'POST', body: {email}})
|
||||
return coralApi('/account/password/reset', {method: 'POST', body: {email}})
|
||||
.then(() => dispatch(forgotPassowordSuccess()))
|
||||
.catch(error => dispatch(forgotPassowordFailure(error)));
|
||||
};
|
||||
@@ -108,7 +112,7 @@ const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
|
||||
|
||||
export const logout = () => dispatch => {
|
||||
dispatch(logOutRequest());
|
||||
coralApi('/auth', {method: 'DELETE'})
|
||||
return coralApi('/auth', {method: 'DELETE'})
|
||||
.then(() => dispatch(logOutSuccess()))
|
||||
.catch(error => dispatch(logOutFailure(error)));
|
||||
};
|
||||
@@ -126,7 +130,7 @@ const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
|
||||
|
||||
export const checkLogin = () => dispatch => {
|
||||
dispatch(checkLoginRequest());
|
||||
coralApi('/auth')
|
||||
return coralApi('/auth')
|
||||
.then((result) => {
|
||||
if (!result.user) {
|
||||
throw new Error('Not logged in');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
email: email => (/^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)),
|
||||
password: pass => !(/^(?=.{8,}).*$/.test(pass)),
|
||||
password: pass => (/^(?=.{8,}).*$/.test(pass)),
|
||||
confirmPassword: () => true,
|
||||
displayName: displayName => (/^[a-z0-9_]+$/.test(displayName))
|
||||
displayName: displayName => (/^[a-zA-Z0-9_]+$/.test(displayName))
|
||||
};
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import {createStore, applyMiddleware, compose, combineReducers} from 'redux';
|
||||
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from './reducers';
|
||||
import authReducer from './reducers/auth';
|
||||
import {client} from './client';
|
||||
|
||||
export default createStore(
|
||||
combineReducers({
|
||||
...mainReducer,
|
||||
apollo: client.reducer(),
|
||||
auth: authReducer,
|
||||
apollo: client.reducer()
|
||||
}),
|
||||
{}, // Initial State. We need to set this
|
||||
compose(
|
||||
window.devToolsExtension && window.devToolsExtension(),
|
||||
applyMiddleware(thunk)
|
||||
applyMiddleware(thunk),
|
||||
window.devToolsExtension && window.devToolsExtension()
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -121,7 +121,12 @@ class SignInContainer extends Component {
|
||||
|
||||
handleSignIn(e) {
|
||||
e.preventDefault();
|
||||
this.props.fetchSignIn(this.state.formData);
|
||||
this.props.fetchSignIn(this.state.formData)
|
||||
|
||||
// Using refetch to get data after the user has logged in.
|
||||
// This is the equivalent of a page reload, we may want to use a more speficic mustation here.
|
||||
// Also, we may want to find a way for this logic to live elsewhere.
|
||||
.then(() => this.props.refetch());
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
.base {
|
||||
height: 30px;
|
||||
width : 30px;
|
||||
}
|
||||
|
||||
.mark {
|
||||
stroke: #FFFFFF;
|
||||
}
|
||||
@@ -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'>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
.base {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,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,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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
const loaders = require('./loaders');
|
||||
const mutators = require('./mutators');
|
||||
const schema = require('./schema');
|
||||
|
||||
module.exports = {
|
||||
createGraphOptions: (req) => {
|
||||
|
||||
let context = {};
|
||||
|
||||
// Load the current logged in user to `user`, otherwise this'll be null.
|
||||
context.user = req.user;
|
||||
|
||||
// Create the loaders.
|
||||
context.loaders = loaders(context);
|
||||
|
||||
// Create the mutators.
|
||||
context.mutators = mutators(context);
|
||||
|
||||
return {
|
||||
schema,
|
||||
context
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,11 +1,14 @@
|
||||
const DataLoader = require('dataloader');
|
||||
const _ = require('lodash');
|
||||
const url = require('url');
|
||||
const errors = require('../errors');
|
||||
const scraper = require('../services/scraper');
|
||||
|
||||
const Comment = require('../../../models/comment');
|
||||
const User = require('../../../models/user');
|
||||
const Action = require('../../../models/action');
|
||||
const Asset = require('../../../models/asset');
|
||||
const Settings = require('../../../models/setting');
|
||||
const Comment = require('../models/comment');
|
||||
const User = require('../models/user');
|
||||
const Action = require('../models/action');
|
||||
const Asset = require('../models/asset');
|
||||
const Settings = require('../models/setting');
|
||||
|
||||
/**
|
||||
* SingletonResolver is a cached loader for a single result.
|
||||
@@ -72,7 +75,7 @@ const singleJoinBy = (ids, key) => (items) => {
|
||||
* Retrieves assets by an array of ids.
|
||||
* @param {Array} ids array of ids to lookup
|
||||
*/
|
||||
const genAssetByID = (ids) => Asset.find({
|
||||
const genAssetsByID = (ids) => Asset.find({
|
||||
id: {
|
||||
$in: ids
|
||||
}
|
||||
@@ -111,6 +114,32 @@ const genCommentsByParentID = (ids) => Comment.find({
|
||||
}
|
||||
}).then(arrayJoinBy(ids, 'parent_id'));
|
||||
|
||||
/**
|
||||
* This endpoint find or creates an asset at the given url when it is loaded.
|
||||
* @param {String} asset_url the url passed in from the query
|
||||
* @returns {Promise} resolves to the asset
|
||||
*/
|
||||
const findOrCreateAssetByURL = (asset_url) => {
|
||||
|
||||
// Verify that the asset_url is parsable.
|
||||
let parsed_asset_url = url.parse(asset_url);
|
||||
if (!parsed_asset_url.protocol) {
|
||||
return Promise.reject(errors.ErrInvalidAssetURL);
|
||||
}
|
||||
|
||||
return Asset.findOrCreateByUrl(asset_url)
|
||||
.then((asset) => {
|
||||
|
||||
// If the asset wasn't scraped before, scrape it! Otherwise just return
|
||||
// the asset.
|
||||
if (!asset.scraped) {
|
||||
return scraper.create(asset).then(() => asset);
|
||||
}
|
||||
|
||||
return asset;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
@@ -122,13 +151,18 @@ const createLoaders = (context) => ({
|
||||
getByAssetID: new DataLoader((ids) => genCommentsByAssetID(ids)),
|
||||
},
|
||||
Actions: {
|
||||
getByID: new DataLoader((ids) => genActionsByID(ids, context.req.user)),
|
||||
getByID: new DataLoader((ids) => genActionsByID(ids, context.user)),
|
||||
},
|
||||
Users: {
|
||||
getByID: new DataLoader((ids) => User.findByIdArray(ids))
|
||||
},
|
||||
Assets: {
|
||||
getByID: new DataLoader((ids) => genAssetByID(ids)),
|
||||
|
||||
// TODO: decide whether we want to move these to mutators or not, as in fact
|
||||
// this operation create a new asset if one isn't found.
|
||||
getByURL: (url) => findOrCreateAssetByURL(url),
|
||||
|
||||
getByID: new DataLoader((ids) => genAssetsByID(ids)),
|
||||
getAll: new SingletonResolver(() => Asset.find({}))
|
||||
},
|
||||
Settings: new SingletonResolver(() => Settings.retrieve())
|
||||
@@ -0,0 +1,215 @@
|
||||
const errors = require('../errors');
|
||||
|
||||
const Action = require('../models/action');
|
||||
const Asset = require('../models/asset');
|
||||
const Comment = require('../models/comment');
|
||||
const User = require('../models/user');
|
||||
|
||||
const Wordlist = require('../services/wordlist');
|
||||
|
||||
/**
|
||||
* Creates a new comment.
|
||||
* @param {Object} user the user performing the request
|
||||
* @param {String} body body of the comment
|
||||
* @param {String} asset_id asset for the comment
|
||||
* @param {String} parent_id optional parent of the comment
|
||||
* @param {String} [status=null] the status of the new comment
|
||||
* @return {Promise} resolves to the created comment
|
||||
*/
|
||||
const createComment = ({user}, {body, asset_id, parent_id = null}, status = null) => {
|
||||
return Comment.publicCreate({
|
||||
body,
|
||||
asset_id,
|
||||
parent_id,
|
||||
status,
|
||||
author_id: user.id
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the comment object and outputs wordlist results.
|
||||
* @param {Object} context graphql context
|
||||
* @param {String} body body of a comment
|
||||
* @return {Object} resolves to the wordlist results
|
||||
*/
|
||||
const filterNewComment = (context, {body}) => {
|
||||
|
||||
// Create a new instance of the Wordlist.
|
||||
const wl = new Wordlist();
|
||||
|
||||
// Load the wordlist and filter the comment content.
|
||||
return wl.load().then(() => wl.scan('body', body));
|
||||
};
|
||||
|
||||
/**
|
||||
* This resolves a given comment's status to take into account moderator actions
|
||||
* are applied.
|
||||
* @param {Object} context graphql context
|
||||
* @param {String} body body of the comment
|
||||
* @param {String} asset_id asset for the comment
|
||||
* @param {Object} [wordlist={}] the results of the wordlist scan
|
||||
* @return {Promise} resolves to the comment's status
|
||||
*/
|
||||
const resolveNewCommentStatus = (context, {asset_id, body}, wordlist = {}) => {
|
||||
|
||||
// Decide the status based on whether or not the current asset/settings
|
||||
// has pre-mod enabled or not. If the comment was rejected based on the
|
||||
// wordlist, then reject it, otherwise if the moderation setting is
|
||||
// premod, set it to `premod`.
|
||||
let status;
|
||||
|
||||
if (wordlist.banned) {
|
||||
status = Promise.resolve('rejected');
|
||||
} else {
|
||||
status = Asset
|
||||
.rectifySettings(Asset.findById(asset_id).then((asset) => {
|
||||
if (!asset) {
|
||||
return Promise.reject(errors.ErrNotFound);
|
||||
}
|
||||
|
||||
// Check to see if the asset has closed commenting...
|
||||
if (asset.isClosed) {
|
||||
|
||||
// They have, ensure that we send back an error.
|
||||
return Promise.reject(new errors.ErrAssetCommentingClosed(asset.closedMessage));
|
||||
}
|
||||
|
||||
return asset;
|
||||
}))
|
||||
|
||||
// Return `premod` if pre-moderation is enabled and an empty "new" status
|
||||
// in the event that it is not in pre-moderation mode.
|
||||
.then(({moderation, charCountEnable, charCount}) => {
|
||||
|
||||
// Reject if the comment is too long
|
||||
if (charCountEnable && body.length > charCount) {
|
||||
return 'rejected';
|
||||
}
|
||||
return moderation === 'pre' ? 'premod' : null;
|
||||
});
|
||||
}
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
/**
|
||||
* createPublicComment is designed to create a comment from a public source. It
|
||||
* validates the comment, and performs some automated moderator actions based on
|
||||
* the settings.
|
||||
* @param {Object} context the graphql context
|
||||
* @param {Object} commentInput the new comment to be created
|
||||
* @return {Promise} resolves to a new comment
|
||||
*/
|
||||
const createPublicComment = (context, commentInput) => {
|
||||
|
||||
// First we filter the comment contents to ensure that we note any validation
|
||||
// issues.
|
||||
return filterNewComment(context, commentInput)
|
||||
|
||||
// We then take the wordlist and the comment into consideration when
|
||||
// considering what status to assign the new comment, and resolve the new
|
||||
// status to set the comment to.
|
||||
.then((wordlist) => resolveNewCommentStatus(context, commentInput, wordlist)
|
||||
|
||||
// Then we actually create the comment with the new status.
|
||||
.then((status) => createComment(context, commentInput, status))
|
||||
.then((comment) => {
|
||||
|
||||
// If the comment was flagged as being suspect, we need to add a
|
||||
// flag to it to indicate that it needs to be looked at.
|
||||
// Otherwise just return the new comment.
|
||||
if (wordlist.suspect) {
|
||||
|
||||
// TODO: this is kind of fragile, we should refactor this to resolve
|
||||
// all these const's that we're using like 'comments', 'flag' to be
|
||||
// defined in a checkable schema.
|
||||
return createAction(null, {
|
||||
item_id: comment.id,
|
||||
item_type: 'comments',
|
||||
action_type: 'flag',
|
||||
metadata: {
|
||||
field: 'body',
|
||||
details: 'Matched suspect word filters.'
|
||||
}
|
||||
}).then(() => comment);
|
||||
}
|
||||
|
||||
// Finally, we return the comment.
|
||||
return comment;
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an action on a item.
|
||||
* @param {Object} user the user performing the request
|
||||
* @param {String} item_id id of the item to add the action to
|
||||
* @param {String} item_type type of the item
|
||||
* @param {String} action_type type of the action
|
||||
* @return {Promise} resolves to the action created
|
||||
*/
|
||||
const createAction = ({user = {}}, {item_id, item_type, action_type, metadata = {}}) => {
|
||||
return Action.insertUserAction({
|
||||
item_id,
|
||||
item_type,
|
||||
user_id: user.id,
|
||||
action_type,
|
||||
metadata
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an action based on the user id if the user owns that action.
|
||||
* @param {Object} user the user performing the request
|
||||
* @param {[type]} id [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
const deleteAction = ({user}, {id}) => {
|
||||
return Action.remove({
|
||||
id,
|
||||
user_id: user.id
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates a users settings.
|
||||
* @param {Object} user the user performing the request
|
||||
* @param {String} bio the new user bio
|
||||
* @return {Promise}
|
||||
*/
|
||||
const updateUserSettings = ({user}, {bio}) => {
|
||||
return User.updateSettings(user.id, {bio});
|
||||
};
|
||||
|
||||
module.exports = (context) => {
|
||||
|
||||
// TODO: refactor to something that'll return an error in the event an attempt
|
||||
// is made to mutate state while not logged in. There's got to be a better way
|
||||
// to do this.
|
||||
if (context.user) {
|
||||
return {
|
||||
Comment: {
|
||||
create: (comment) => createPublicComment(context, comment)
|
||||
},
|
||||
Action: {
|
||||
create: (action) => createAction(context, action),
|
||||
delete: (action) => deleteAction(context, action)
|
||||
},
|
||||
User: {
|
||||
updateSettings: (settings) => updateUserSettings(context, settings)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
Comment: {
|
||||
create: () => {}
|
||||
},
|
||||
Action: {
|
||||
create: () => {},
|
||||
delete: () => {}
|
||||
},
|
||||
User: {
|
||||
updateSettings: () => {}
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
const Action = {
|
||||
action_type({action_type}) {
|
||||
|
||||
// TODO: remove once we cast the data model to have uppercase action
|
||||
// types.
|
||||
return action_type.toUpperCase();
|
||||
},
|
||||
item_type({item_type}) {
|
||||
|
||||
// TODO: remove once we cast the data model to have uppercase item
|
||||
// types.
|
||||
return item_type.toUpperCase();
|
||||
},
|
||||
user({user_id}, _, {loaders}) {
|
||||
return loaders.Users.getByID.load(user_id);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Action;
|
||||
@@ -0,0 +1,16 @@
|
||||
const ActionSummary = {
|
||||
action_type({action_type}) {
|
||||
|
||||
// TODO: remove once we cast the data model to have uppercase action
|
||||
// types.
|
||||
return action_type.toUpperCase();
|
||||
},
|
||||
item_type({item_type}) {
|
||||
|
||||
// TODO: remove once we cast the data model to have uppercase item
|
||||
// types.
|
||||
return item_type.toUpperCase();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ActionSummary;
|
||||
@@ -0,0 +1,20 @@
|
||||
const Asset = {
|
||||
comments({id}, _, {loaders}) {
|
||||
return loaders.Comments.getByAssetID.load(id);
|
||||
},
|
||||
settings({settings = null}, _, {loaders}) {
|
||||
return loaders.Settings.load()
|
||||
.then((globalSettings) => {
|
||||
|
||||
if (settings) {
|
||||
settings = Object.assign({}, settings, globalSettings);
|
||||
} else {
|
||||
settings = globalSettings;
|
||||
}
|
||||
|
||||
return settings;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Asset;
|
||||
@@ -0,0 +1,13 @@
|
||||
const Comment = {
|
||||
user({author_id}, _, {loaders}) {
|
||||
return loaders.Users.getByID.load(author_id);
|
||||
},
|
||||
replies({id}, _, {loaders}) {
|
||||
return loaders.Comments.getByParentID.load(id);
|
||||
},
|
||||
actions({id}, _, {loaders}) {
|
||||
return loaders.Actions.getByID.load(id);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Comment;
|
||||
@@ -0,0 +1,17 @@
|
||||
const Action = require('./action');
|
||||
const ActionSummary = require('./action_summary');
|
||||
const Asset = require('./asset');
|
||||
const Comment = require('./comment');
|
||||
const RootMutation = require('./root_mutation');
|
||||
const RootQuery = require('./root_query');
|
||||
const User = require('./user');
|
||||
|
||||
module.exports = {
|
||||
Action,
|
||||
ActionSummary,
|
||||
Asset,
|
||||
Comment,
|
||||
RootMutation,
|
||||
RootQuery,
|
||||
User
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
const RootMutation = {
|
||||
createComment(_, {asset_id, parent_id, body}, {mutators}) {
|
||||
return mutators.Comment.create({asset_id, parent_id, body});
|
||||
},
|
||||
createAction(_, {action}, {mutators}) {
|
||||
return mutators.Action.create(action);
|
||||
},
|
||||
deleteAction(_, {id}, {mutators}) {
|
||||
return mutators.Action.delete({id});
|
||||
},
|
||||
updateUserSettings(_, {settings}, {mutators}) {
|
||||
return mutators.User.updateSettings(settings);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = RootMutation;
|
||||
@@ -0,0 +1,25 @@
|
||||
const RootQuery = {
|
||||
assets(_, args, {loaders}) {
|
||||
return loaders.Assets.getAll.load();
|
||||
},
|
||||
asset(_, {id = null, url}, {loaders}) {
|
||||
if (id) {
|
||||
|
||||
// TODO: we may not always have a comment stream here, therefore, when we
|
||||
// load it, we may also need to create with the url. This may also have to
|
||||
// move the logic over to the mutators function as an upsert operation
|
||||
// possibly.
|
||||
return loaders.Assets.getByID.load(id);
|
||||
} else {
|
||||
return loaders.Assets.getByURL(url);
|
||||
}
|
||||
},
|
||||
settings(_, args, {loaders}) {
|
||||
return loaders.Settings.load();
|
||||
},
|
||||
me(_, args, {user}) {
|
||||
return user;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = RootQuery;
|
||||
@@ -0,0 +1,7 @@
|
||||
const User = {
|
||||
actions({id}, _, {loaders}) {
|
||||
return loaders.Actions.getByID.load(id);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = User;
|
||||
@@ -0,0 +1,123 @@
|
||||
// TODO: Adjust `RootQuery.asset(id: ID, url: URL)` to instead be
|
||||
// `RootQuery.asset(id: ID, url: URL!)` because we'll always need the url, if
|
||||
// this change is done now everything will likely break on the front end.
|
||||
|
||||
const typeDefs = [`
|
||||
type UserSettings {
|
||||
bio: String
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
displayName: String!
|
||||
actions: [ActionSummary]
|
||||
settings: UserSettings
|
||||
}
|
||||
|
||||
type Comment {
|
||||
id: ID!
|
||||
body: String!
|
||||
user: User
|
||||
replies(limit: Int = 3): [Comment]
|
||||
actions: [ActionSummary]
|
||||
}
|
||||
|
||||
enum ITEM_TYPE {
|
||||
ASSETS
|
||||
COMMENTS
|
||||
USERS
|
||||
}
|
||||
|
||||
enum ACTION_TYPE {
|
||||
LIKE
|
||||
FLAG
|
||||
}
|
||||
|
||||
interface ActionInterface {
|
||||
action_type: ACTION_TYPE!
|
||||
item_type: ITEM_TYPE!
|
||||
}
|
||||
|
||||
type Action implements ActionInterface {
|
||||
id: ID!
|
||||
item_id: ID!
|
||||
action_type: ACTION_TYPE!
|
||||
item_type: ITEM_TYPE!
|
||||
user: User!
|
||||
updated_at: String
|
||||
created_at: String
|
||||
}
|
||||
|
||||
type ActionSummary implements ActionInterface {
|
||||
action_type: ACTION_TYPE!
|
||||
item_type: ITEM_TYPE!
|
||||
count: Int
|
||||
current_user: Action
|
||||
}
|
||||
|
||||
type Settings {
|
||||
moderation: String
|
||||
infoBoxEnable: Boolean
|
||||
infoBoxContent: String
|
||||
closeTimeout: Int
|
||||
closedMessage: String
|
||||
charCountEnable: Boolean
|
||||
charCount: Int
|
||||
requireEmailConfirmation: Boolean
|
||||
}
|
||||
|
||||
type Asset {
|
||||
id: ID!
|
||||
title: String
|
||||
url: String
|
||||
comments: [Comment]
|
||||
settings: Settings!
|
||||
currentUser: User
|
||||
}
|
||||
|
||||
scalar URL
|
||||
|
||||
type RootQuery {
|
||||
settings: Settings
|
||||
assets: [Asset]
|
||||
asset(id: ID, url: URL): Asset
|
||||
me: User
|
||||
}
|
||||
|
||||
input CreateActionInput {
|
||||
# the type of action.
|
||||
action_type: ACTION_TYPE!
|
||||
|
||||
# the type of the item.
|
||||
item_type: ITEM_TYPE!
|
||||
|
||||
# the id of the item that is related to the action.
|
||||
item_id: ID!
|
||||
}
|
||||
|
||||
input UpdateUserSettingsInput {
|
||||
# user bio
|
||||
bio: String!
|
||||
}
|
||||
|
||||
type RootMutation {
|
||||
# creates a comment on the asset.
|
||||
createComment(asset_id: ID!, parent_id: ID, body: String!): Comment
|
||||
|
||||
# creates an action based on an input.
|
||||
createAction(action: CreateActionInput!): Action
|
||||
|
||||
# delete an action based on the action id.
|
||||
deleteAction(id: ID!): Boolean
|
||||
|
||||
# updates a user's settings, it will return if the query was successful.
|
||||
updateUserSettings(settings: UpdateUserSettingsInput!): Boolean
|
||||
}
|
||||
|
||||
schema {
|
||||
query: RootQuery
|
||||
mutation: RootMutation
|
||||
}
|
||||
`];
|
||||
|
||||
module.exports = typeDefs;
|
||||
@@ -1,27 +0,0 @@
|
||||
const express = require('express');
|
||||
const apollo = require('graphql-server-express');
|
||||
|
||||
const loaders = require('./loaders');
|
||||
const mutators = require('./mutators');
|
||||
const schema = require('./schema');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// GraphQL endpoint.
|
||||
router.use('/ql', apollo.graphqlExpress((req) => {
|
||||
|
||||
let context = {req};
|
||||
|
||||
context.loaders = loaders(context);
|
||||
context.mutators = mutators(context);
|
||||
|
||||
return {
|
||||
schema,
|
||||
context
|
||||
};
|
||||
}));
|
||||
|
||||
// Interactive graphiql interface.
|
||||
router.use('/iql', apollo.graphiqlExpress({endpointURL: '/api/v1/graph/ql'}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,93 +0,0 @@
|
||||
const errors = require('../../../errors');
|
||||
|
||||
const Asset = require('../../../models/asset');
|
||||
const Comment = require('../../../models/comment');
|
||||
|
||||
const Wordlist = require('../../../services/wordlist');
|
||||
|
||||
/**
|
||||
* Creates a new comment.
|
||||
* @param {Object} context a GraphQL context
|
||||
* @param {String} body body of the comment
|
||||
* @param {String} asset_id asset for the comment
|
||||
* @param {String} parent_id optional parent of the comment
|
||||
* @param {Object} [wordlist={}] results for the wordlist analysis
|
||||
* @return {Promise} resolves to the created comment
|
||||
*/
|
||||
const createComment = (context, {body, asset_id, parent_id = null}, wordlist = {}) => {
|
||||
|
||||
// Decide the status based on whether or not the current asset/settings
|
||||
// has pre-mod enabled or not. If the comment was rejected based on the
|
||||
// wordlist, then reject it, otherwise if the moderation setting is
|
||||
// premod, set it to `premod`.
|
||||
let status;
|
||||
|
||||
if (wordlist.banned) {
|
||||
status = Promise.resolve('rejected');
|
||||
} else {
|
||||
status = Asset
|
||||
.rectifySettings(Asset.findById(asset_id).then((asset) => {
|
||||
if (!asset) {
|
||||
return Promise.reject(errors.ErrNotFound);
|
||||
}
|
||||
|
||||
// Check to see if the asset has closed commenting...
|
||||
if (asset.isClosed) {
|
||||
|
||||
// They have, ensure that we send back an error.
|
||||
return Promise.reject(new errors.ErrAssetCommentingClosed(asset.closedMessage));
|
||||
}
|
||||
|
||||
return asset;
|
||||
}))
|
||||
|
||||
// Return `premod` if pre-moderation is enabled and an empty "new" status
|
||||
// in the event that it is not in pre-moderation mode.
|
||||
.then(({moderation, charCountEnable, charCount}) => {
|
||||
|
||||
// Reject if the comment is too long
|
||||
if (charCountEnable && body.length > charCount) {
|
||||
return 'rejected';
|
||||
}
|
||||
return moderation === 'pre' ? 'premod' : null;
|
||||
});
|
||||
}
|
||||
|
||||
return status.then((status) => Comment.publicCreate({
|
||||
body,
|
||||
asset_id,
|
||||
parent_id,
|
||||
status,
|
||||
author_id: context.req.user.id
|
||||
}))
|
||||
.then((comment) => {
|
||||
if (wordlist.suspect) {
|
||||
return Comment
|
||||
.addAction(comment.id, null, 'flag', {field: 'body', details: 'Matched suspect word filters.'})
|
||||
.then(() => comment);
|
||||
}
|
||||
|
||||
return comment;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the comment object and outputs wordlist results.
|
||||
* @param {Object} context graphql context
|
||||
* @param {String} body body of a comment
|
||||
* @return {Object} resolves to the wordlist results
|
||||
*/
|
||||
const filterNewComment = (context, {body}) => {
|
||||
|
||||
// Create a new instance of the Wordlist.
|
||||
const wl = new Wordlist();
|
||||
|
||||
// Load the wordlist and filter the comment content.
|
||||
return wl.load().then(() => wl.scan('body', body));
|
||||
};
|
||||
|
||||
module.exports = (context) => ({
|
||||
createComment: (comment) => filterNewComment(context, comment).then((wordlist) => {
|
||||
return createComment(context, comment, wordlist);
|
||||
})
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
module.exports = {
|
||||
Query: {
|
||||
assets(_, args, {loaders}) {
|
||||
return loaders.Assets.getAll.load();
|
||||
},
|
||||
asset(_, {id}, {loaders}) {
|
||||
return loaders.Assets.getByID.load(id);
|
||||
},
|
||||
settings(_, args, {loaders}) {
|
||||
return loaders.Settings.load();
|
||||
},
|
||||
me(_, args, {req}) {
|
||||
return req.user;
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
createComment(_, {asset_id, parent_id, body}, {mutators}) {
|
||||
return mutators.createComment({asset_id, parent_id, body});
|
||||
}
|
||||
},
|
||||
Asset: {
|
||||
comments({id}, _, {loaders}) {
|
||||
return loaders.Comments.getByAssetID.load(id);
|
||||
},
|
||||
settings({settings = null}, _, {loaders}) {
|
||||
return loaders.Settings.load()
|
||||
.then((globalSettings) => {
|
||||
|
||||
if (settings) {
|
||||
settings = Object.assign({}, settings, globalSettings);
|
||||
} else {
|
||||
settings = globalSettings;
|
||||
}
|
||||
|
||||
return settings;
|
||||
});
|
||||
}
|
||||
},
|
||||
User: {
|
||||
actions({id}, _, {loaders}) {
|
||||
return loaders.Actions.getByID.load(id);
|
||||
}
|
||||
},
|
||||
Comment: {
|
||||
user({author_id}, _, {loaders}) {
|
||||
return loaders.Users.getByID.load(author_id);
|
||||
},
|
||||
replies({id}, _, {loaders}) {
|
||||
return loaders.Comments.getByParentID.load(id);
|
||||
},
|
||||
actions({id}, _, {loaders}) {
|
||||
return loaders.Actions.getByID.load(id);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
const typeDefs = [`
|
||||
type UserSettings {
|
||||
bio: String
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
displayName: String!
|
||||
actions: [Action]
|
||||
settings: UserSettings
|
||||
}
|
||||
|
||||
type Comment {
|
||||
id: ID!
|
||||
body: String!
|
||||
user: User
|
||||
replies(limit: Int = 3): [Comment]
|
||||
actions: [Action]
|
||||
}
|
||||
|
||||
type Action {
|
||||
id: ID!
|
||||
item_id: ID!
|
||||
action_type: String!
|
||||
count: Int
|
||||
current_user: Action
|
||||
updated_at: String
|
||||
created_at: String
|
||||
}
|
||||
|
||||
type Settings {
|
||||
moderation: String
|
||||
infoBoxEnable: Boolean
|
||||
infoBoxContent: String
|
||||
closeTimeout: Int
|
||||
closedMessage: String
|
||||
charCountEnable: Boolean
|
||||
charCount: Int
|
||||
requireEmailConfirmation: Boolean
|
||||
}
|
||||
|
||||
type Asset {
|
||||
id: ID!
|
||||
title: String
|
||||
url: String
|
||||
comments: [Comment]
|
||||
settings: Settings!
|
||||
currentUser: User
|
||||
}
|
||||
|
||||
type Query {
|
||||
settings: Settings
|
||||
assets: [Asset]
|
||||
asset(id: ID!): Asset
|
||||
me: User
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createComment(asset_id: ID!, parent_id: ID, body: String!): Comment
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
`];
|
||||
|
||||
module.exports = typeDefs;
|
||||
+3
-1
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
||||
<meta property="csrf" content="<%= csrfToken %>">
|
||||
<title>Talk - Coral Admin</title>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
|
||||
<style media="screen">
|
||||
@@ -12,7 +13,8 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
background-color: #FAFAFA;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
/* putting this here until I can get webpack to behave */
|
||||
.react-tagsinput {
|
||||
|
||||
Reference in New Issue
Block a user