Merge branch 'master' into i18n-emails

This commit is contained in:
gaba
2017-05-31 15:31:14 -07:00
303 changed files with 15973 additions and 3510 deletions
+6
View File
@@ -1,7 +1,13 @@
dist
docs
client/lib
**/*.html
plugins/*
!plugins/coral-plugin-facebook-auth
!plugins/coral-plugin-auth
!plugins/coral-plugin-respect
!plugins/coral-plugin-offtopic
!plugins/coral-plugin-like
!plugins/coral-plugin-mod
!plugins/coral-plugin-love
node_modules
+1
View File
@@ -17,6 +17,7 @@ coverage/
plugins.json
plugins/*
!plugins/coral-plugin-facebook-auth
!plugins/coral-plugin-auth
!plugins/coral-plugin-respect
!plugins/coral-plugin-offtopic
!plugins/coral-plugin-like
+8 -1
View File
@@ -17,7 +17,6 @@ To set up a development environment or build from source, see [INSTALL.md](https
To launch a Talk server of your own from your browser without any need to muck about in a terminal or think about engineering concepts, stay tuned. We will launch [our installer](https://github.com/coralproject/talk-install) shortly!
### Configuration
The Talk application looks for the following configuration values either as environment variables:
@@ -45,6 +44,12 @@ sign and verify tokens via a `HS256` algorithm.
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
alternative methods of loading configuration during development.
### Using Trust
Talk ships with core components we call "Trust". This allows Talk to automate certain actions based on previous user behavior.
Our first feature is the notion of Karma. Talk will automatically pre-moderate comments of users who have a negative karma score. You can [see more how karma works here](/services/karma.js).
## Supported Browsers
### Web
@@ -58,6 +63,8 @@ alternative methods of loading configuration during development.
- iPad
- iPad Pro
- iPhone 7 Plus
- iPhone 7
- iPhone 6 Plus
- iPhone 6
- iPhone 5
+4
View File
@@ -11,6 +11,8 @@ const errors = require('./errors');
const {createGraphOptions} = require('./graph');
const apollo = require('graphql-server-express');
const accepts = require('accepts');
const compression = require('compression');
const cookieParser = require('cookie-parser');
const app = express();
@@ -31,6 +33,8 @@ app.set('trust proxy', 1);
app.use(helmet({
frameguard: false
}));
app.use(compression());
app.use(cookieParser());
app.use(bodyParser.json());
//==============================================================================
-3
View File
@@ -1,3 +0,0 @@
{
"basePath": "admin"
}
+24 -29
View File
@@ -1,25 +1,20 @@
import React from 'react';
import {Router, Route, IndexRedirect, browserHistory} from 'react-router';
import Stories from 'containers/Stories/Stories';
import Configure from 'containers/Configure/Configure';
import LayoutContainer from 'containers/LayoutContainer';
import InstallContainer from 'containers/Install/InstallContainer';
import Configure from 'routes/Configure';
import Dashboard from 'routes/Dashboard';
import Install from 'routes/Install';
import Stories from 'routes/Stories';
import {CommunityLayout, Community} from 'routes/Community';
import {ModerationLayout, Moderation} from 'routes/Moderation';
import CommunityLayout from 'containers/Community/CommunityLayout';
import CommunityContainer from 'containers/Community/CommunityContainer';
import ModerationLayout from 'containers/ModerationQueue/ModerationLayout';
import ModerationContainer from 'containers/ModerationQueue/ModerationContainer';
import Dashboard from 'containers/Dashboard/Dashboard';
import Layout from 'containers/Layout';
const routes = (
<div>
<Route exact path="/admin/install" component={InstallContainer}/>
<Route path='/admin' component={LayoutContainer}>
<Route exact path="/admin/install" component={Install}/>
<Route path='/admin' component={Layout}>
<IndexRedirect to='/admin/moderate/all' />
<Route path='community' component={CommunityContainer} />
<Route path='configure' component={Configure} />
<Route path='stories' component={Stories} />
<Route path='dashboard' component={Dashboard} />
@@ -27,11 +22,11 @@ const routes = (
{/* Community Routes */}
<Route path='community' component={CommunityLayout}>
<Route path='flagged' components={CommunityContainer}>
<Route path=':id' components={CommunityContainer} />
<Route path='flagged' components={Community}>
<Route path=':id' components={Community} />
</Route>
<Route path='people' components={CommunityContainer}>
<Route path=':id' components={CommunityContainer} />
<Route path='people' components={Community}>
<Route path=':id' components={Community} />
</Route>
<IndexRedirect to='flagged' />
</Route>
@@ -39,22 +34,22 @@ const routes = (
{/* Moderation Routes */}
<Route path='moderate' component={ModerationLayout}>
<Route path='all' components={ModerationContainer}>
<Route path=':id' components={ModerationContainer} />
<Route path='all' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path='accepted' components={ModerationContainer}>
<Route path=':id' components={ModerationContainer} />
<Route path='accepted' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path='premod' components={ModerationContainer}>
<Route path=':id' components={ModerationContainer} />
<Route path='premod' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path='rejected' components={ModerationContainer}>
<Route path=':id' components={ModerationContainer} />
<Route path='rejected' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path='flagged' components={ModerationContainer}>
<Route path=':id' components={ModerationContainer} />
<Route path='flagged' components={Moderation}>
<Route path=':id' components={Moderation} />
</Route>
<Route path=':id' components={ModerationContainer} />
<Route path=':id' components={Moderation} />
<IndexRedirect to='premod' />
</Route>
</Route>
+1 -1
View File
@@ -8,7 +8,7 @@ import {
UPDATE_ASSETS
} from '../constants/assets';
import coralApi from '../../../coral-framework/helpers/response';
import coralApi from '../../../coral-framework/helpers/request';
/**
* Action disptacher related to assets
+27 -7
View File
@@ -1,6 +1,7 @@
import bowser from 'bowser';
import * as actions from '../constants/auth';
import coralApi from 'coral-framework/helpers/request';
import * as Storage from 'coral-framework/helpers/storage';
import coralApi from 'coral-framework/helpers/response';
import {handleAuthToken} from 'coral-framework/actions/auth';
//==============================================================================
@@ -9,21 +10,36 @@ import {handleAuthToken} from 'coral-framework/actions/auth';
export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => {
dispatch({type: actions.LOGIN_REQUEST});
const params = {method: 'POST', body: {email, password}};
const params = {
method: 'POST',
body: {
email,
password
}
};
if (recaptchaResponse) {
params.headers = {'X-Recaptcha-Response': recaptchaResponse};
params.headers = {
'X-Recaptcha-Response': recaptchaResponse
};
}
return coralApi('/auth/local', params)
.then(({user, token}) => {
if (!user) {
Storage.removeItem('token');
if (!bowser.safari && !bowser.ios) {
Storage.removeItem('token');
}
return dispatch(checkLoginFailure('not logged in'));
}
dispatch(handleAuthToken(token));
dispatch(checkLoginSuccess(user));
})
.catch((error) => {
if (error.translation_key === 'login_maximum_exceeded') {
if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
dispatch({
type: actions.LOGIN_MAXIMUM_EXCEEDED,
message: error.translation_key
@@ -52,7 +68,9 @@ const forgotPassowordFailure = () => ({
export const requestPasswordReset = (email) => (dispatch) => {
dispatch(forgotPassowordRequest(email));
return coralApi('/account/password/reset', {method: 'POST', body: {email}})
const redirectUri = location.href;
return coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
.then(() => dispatch(forgotPassowordSuccess()))
.catch((error) => dispatch(forgotPassowordFailure(error)));
};
@@ -81,7 +99,9 @@ export const checkLogin = () => (dispatch) => {
return coralApi('/auth')
.then(({user}) => {
if (!user) {
Storage.removeItem('token');
if (!bowser.safari && !bowser.ios) {
Storage.removeItem('token');
}
return dispatch(checkLoginFailure('not logged in'));
}
+1 -1
View File
@@ -14,7 +14,7 @@ import {
HIDE_SUSPENDUSER_DIALOG
} from '../constants/community';
import coralApi from '../../../coral-framework/helpers/response';
import coralApi from '../../../coral-framework/helpers/request';
export const fetchAccounts = (query = {}) => (dispatch) => {
+1 -1
View File
@@ -1,4 +1,4 @@
import coralApi from 'coral-framework/helpers/response';
import coralApi from 'coral-framework/helpers/request';
import * as actions from '../constants/install';
import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
+7 -1
View File
@@ -5,7 +5,7 @@ export const singleView = () => ({type: actions.SINGLE_VIEW});
// Ban User Dialog
export const showBanUserDialog = (user, commentId, commentStatus, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, commentStatus, showRejectedNote});
export const hideBanUserDialog = (showDialog) => ({type: actions.HIDE_BANUSER_DIALOG, showDialog});
export const hideBanUserDialog = () => ({type: actions.HIDE_BANUSER_DIALOG});
// Suspend User Dialog
export const showSuspendUserDialog = (userId, username, commentId, commentStatus) =>
@@ -27,3 +27,9 @@ export const hideShortcutsNote = () => {
export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, userId});
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
export const setSortOrder = (order) => ({
type: actions.SET_SORT_ORDER,
order
});
+1 -1
View File
@@ -1,4 +1,4 @@
import coralApi from '../../../coral-framework/helpers/response';
import coralApi from '../../../coral-framework/helpers/request';
export const SETTINGS_LOADING = 'SETTINGS_LOADING';
export const SETTINGS_RECEIVED = 'SETTINGS_RECEIVED';
+1 -1
View File
@@ -1,4 +1,4 @@
import coralApi from '../../../coral-framework/helpers/response';
import coralApi from '../../../coral-framework/helpers/request';
import * as userTypes from '../constants/users';
/**
@@ -1,7 +1,7 @@
import React, {PropTypes} from 'react';
import styles from './ModerationList.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../containers/ModerationQueue/helpers/moderationQueueActionsMap';
import {menuActionsMap} from '../routes/Moderation/helpers/moderationQueueActionsMap';
import t from 'coral-framework/services/i18n';
@@ -35,7 +35,7 @@ class AdminLogin extends React.Component {
const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props;
const signInForm = (
<form onSubmit={this.handleSignIn}>
{errorMessage && <Alert>{t(`errors.${errorMessage}`)}</Alert>}
{errorMessage && <Alert>{t(`error.${errorMessage}`)}</Alert>}
<TextField
label='Email Address'
value={this.state.email}
@@ -73,6 +73,8 @@
font-weight: 500;
background-color: transparent;
transition: background-color 200ms;
font-weight: 100;
letter-spacing: .8;
&:hover {
background-color: #232323;
@@ -80,6 +82,7 @@
&.active {
background-color: #232323;
font-weight: 400;
}
}
@@ -7,3 +7,4 @@ export const SHOW_SUSPEND_USER_DIALOG = 'SHOW_SUSPEND_USER_DIALOG';
export const HIDE_SUSPEND_USER_DIALOG = 'HIDE_SUSPEND_USER_DIALOG';
export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL';
export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL';
export const SET_SORT_ORDER = 'MODERATION_SET_SORT_ORDER';
@@ -1,9 +0,0 @@
import React from 'react';
import t from 'coral-framework/services/i18n';
const Loading = () => (
<h1> {t('loading_results')}</h1>
);
export default Loading;
@@ -1,79 +0,0 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {SelectField, Option} from 'react-mdl-selectfield';
import styles from './Community.css';
import {setRole, setCommenterStatus} from '../../actions/community';
import t from 'coral-framework/services/i18n';
class Table extends Component {
constructor (props) {
super(props);
this.onRoleChange = this.onRoleChange.bind(this);
}
onRoleChange (id, role) {
this.props.dispatch(setRole(id, role));
}
onCommenterStatusChange (id, status) {
this.props.dispatch(setCommenterStatus(id, status));
}
render () {
const {headers, commenters, onHeaderClickHandler} = this.props;
return (
<table className={`mdl-data-table ${styles.dataTable}`}>
<thead>
<tr>
{headers.map((header, i) =>(
<th
key={i}
className="mdl-data-table__cell--non-numeric"
onClick={() => onHeaderClickHandler({field: header.field})}>
{header.title}
</th>
))}
</tr>
</thead>
<tbody>
{commenters.map((row, i)=> (
<tr key={i}>
<td className="mdl-data-table__cell--non-numeric">
{row.username}
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
</td>
<td className="mdl-data-table__cell--non-numeric">
{row.created_at}
</td>
<td className="mdl-data-table__cell--non-numeric">
<SelectField label={'Select me'} value={row.status || ''}
className={styles.selectField}
label={t('community.status')}
onChange={(status) => this.onCommenterStatusChange(row.id, status)}>
<Option value={'ACTIVE'}>{t('community.active')}</Option>
<Option value={'BANNED'}>{t('community.banned')}</Option>
</SelectField>
</td>
<td className="mdl-data-table__cell--non-numeric">
<SelectField label={'Select me'} value={row.roles[0] || ''}
className={styles.selectField}
label={t('community.role')}
onChange={(role) => this.onRoleChange(row.id, role)}>
<Option value={''}>.</Option>
<Option value={'STAFF'}>{t('community.staff')}</Option>
<Option value={'MODERATOR'}>{t('community.moderator')}</Option>
<Option value={'ADMIN'}>{t('community.admin')}</Option>
</SelectField>
</td>
</tr>
))}
</tbody>
</table>
);
}
}
export default connect((state) => ({commenters: state.community.get('accounts')}))(Table);
@@ -1,48 +0,0 @@
import React from 'react';
import styles from './Dashboard.css';
import {compose} from 'react-apollo';
import {connect} from 'react-redux';
import {getMetrics} from 'coral-admin/src/graphql/queries';
import FlagWidget from './FlagWidget';
import ActivityWidget from './ActivityWidget';
import CountdownTimer from 'coral-admin/src/components/CountdownTimer';
import {Spinner} from 'coral-ui';
class Dashboard extends React.Component {
reloadData = () => {
this.props.data.refetch();
}
render () {
if (this.props.data && this.props.data.loading) {
return <Spinner />;
}
const {data: {assetsByActivity, assetsByFlag}} = this.props;
return (
<div>
<CountdownTimer handleTimeout={this.reloadData} />
<div className={styles.Dashboard}>
<FlagWidget assets={assetsByFlag} />
<ActivityWidget assets={assetsByActivity} />
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
settings: state.settings.toJS(),
moderation: state.moderation.toJS()
};
};
export default compose(
connect(mapStateToProps),
getMetrics
)(Dashboard);
@@ -1,101 +0,0 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import styles from './style.css';
import {Wizard, WizardNav} from 'coral-ui';
import Layout from 'coral-admin/src/components/ui/Layout';
import {
goToStep,
nextStep,
submitUser,
checkInstall,
previousStep,
finishInstall,
submitSettings,
updateUserFormData,
updateSettingsFormData,
updatePermittedDomains
} from '../../actions/install';
import InitialStep from './components/Steps/InitialStep';
import AddOrganizationName from './components/Steps/AddOrganizationName';
import CreateYourAccount from './components/Steps/CreateYourAccount';
import PermittedDomainsStep from './components/Steps/PermittedDomainsStep';
import FinalStep from './components/Steps/FinalStep';
class InstallContainer extends Component {
componentDidMount() {
const {checkInstall} = this.props;
checkInstall(() => {
this.context.router.push('/admin');
});
}
render() {
const {install} = this.props;
return (
<Layout restricted={true}>
<div className={styles.Install}>
{
!install.alreadyInstalled ? (
<div>
<h2>Welcome to the Coral Project</h2>
{ install.step !== 0 ? <WizardNav items={install.navItems} currentStep={install.step} icon='check'/> : null }
<Wizard currentStep={install.step} {...this.props}>
<InitialStep/>
<AddOrganizationName/>
<CreateYourAccount/>
<PermittedDomainsStep/>
<FinalStep/>
</Wizard>
</div>
) : (
<div>Talk is already installed</div>
)
}
</div>
</Layout>
);
}
}
InstallContainer.contextTypes = {
router: React.PropTypes.object
};
const mapStateToProps = (state) => ({
install: state.install.toJS()
});
const mapDispatchToProps = (dispatch) => ({
nextStep: () => dispatch(nextStep()),
goToStep: (step) => dispatch(goToStep(step)),
previousStep: () => dispatch(previousStep()),
finishInstall: () => dispatch(finishInstall()),
checkInstall: (next) => dispatch(checkInstall(next)),
handleDomainsChange: (value) => {
dispatch(updatePermittedDomains(value));
},
handleSettingsChange: (e) => {
const {name, value} = e.currentTarget;
dispatch(updateSettingsFormData(name, value));
},
handleUserChange: (e) => {
const {name, value} = e.currentTarget;
dispatch(updateUserFormData(name, value));
},
handleSettingsSubmit: (e) => {
e.preventDefault();
dispatch(submitSettings());
},
handleUserSubmit: (e) => {
e.preventDefault();
dispatch(submitUser());
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(InstallContainer);
@@ -1,185 +0,0 @@
import React, {Component} from 'react';
import styles from './Stories.css';
import {connect} from 'react-redux';
import t from 'coral-framework/services/i18n';
import {fetchAssets, updateAssetState} from '../../actions/assets';
import {Link} from 'react-router';
import {Pager, Icon} from 'coral-ui';
import {DataTable, TableHeader, RadioGroup, Radio} from 'react-mdl';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
const limit = 25;
class Stories extends Component {
state = {
search: '',
sort: 'desc',
filter: 'all',
statusMenus: {},
timer: null,
page: 0
}
componentDidMount () {
this.props.fetchAssets(0, limit, '', this.state.sortBy);
}
onSettingChange = (setting) => (e) => {
let options = this.state;
this.setState({[setting]: e.target.value});
options[setting] = e.target.value;
this.props.fetchAssets(0, limit, options.search, options.sort, options.filter);
}
onSearchChange = (e) => {
const search = e.target.value;
this.setState((prevState) => {
prevState.search = search;
clearTimeout(prevState.timer);
const fetchAssets = this.props.fetchAssets;
prevState.timer = setTimeout(() => {
fetchAssets(0, limit, search, this.state.sort, this.state.filter);
}, 350);
return prevState;
});
}
renderDate = (date) => {
const d = new Date(date);
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
}
onStatusClick = (closeStream, id, statusMenuOpen) => () => {
if (statusMenuOpen) {
this.setState((prev) => {
prev.statusMenus[id] = false;
return prev;
});
this.props.updateAssetState(id, closeStream ? Date.now() : null)
.then(() => {
const {search, sort, filter, page} = this.state;
this.props.fetchAssets(page, limit, search, sort, filter);
});
} else {
this.setState((prev) => {
prev.statusMenus[id] = true;
return prev;
});
}
}
renderTitle = (title, {id}) => <Link to={`/admin/moderate/${id}`}>{title}</Link>
renderStatus = (closedAt, {id}) => {
const closed = closedAt && new Date(closedAt).getTime() < Date.now();
const statusMenuOpen = this.state.statusMenus[id];
return <div className={styles.statusMenu}>
<div
className={closed ? styles.statusMenuClosed : styles.statusMenuOpen}
onClick={this.onStatusClick(closed, id, statusMenuOpen)}>
{!statusMenuOpen && <Icon className={styles.statusMenuIcon} name='keyboard_arrow_down'/>}
{closed ? t('streams.closed') : t('streams.open')}
</div>
{
statusMenuOpen &&
<div
className={!closed ? styles.statusMenuClosed : styles.statusMenuOpen}
onClick={this.onStatusClick(!closed, id, statusMenuOpen)}>
{!closed ? t('streams.closed') : t('streams.open')}
</div>
}
</div>;
}
onPageClick = (page) => {
this.setState({page});
const {search, sort, filter} = this.state;
this.props.fetchAssets((page - 1) * limit, limit, search, sort, filter);
}
render () {
const {search, sort, filter} = this.state;
const {assets} = this.props;
const assetsIds = assets.ids.map((id) => assets.byId[id]);
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={t('streams.search')}/>
</div>
<div className={styles.optionHeader}>{t('streams.filter_streams')}</div>
<div className={styles.optionDetail}>{t('streams.stream_status')}</div>
<RadioGroup
name='status filter'
value={filter}
childContainer='div'
onChange={this.onSettingChange('filter')}
className={styles.radioGroup}
>
<Radio value='all'>{t('streams.all')}</Radio>
<Radio value='open'>{t('streams.open')}</Radio>
<Radio value='closed'>{t('streams.closed')}</Radio>
</RadioGroup>
<div className={styles.optionHeader}>{t('streams.sort_by')}</div>
<RadioGroup
name='sort by'
value={sort}
childContainer='div'
onChange={this.onSettingChange('sort')}
className={styles.radioGroup}
>
<Radio value='desc'>{t('streams.newest')}</Radio>
<Radio value='asc'>{t('streams.oldest')}</Radio>
</RadioGroup>
</div>
{
assetsIds.length
? <div className={styles.mainContent}>
<DataTable className={styles.streamsTable} rows={assetsIds} onClick={this.goToModeration}>
<TableHeader name="title" cellFormatter={this.renderTitle}>{t('streams.article')}</TableHeader>
<TableHeader name="publication_date" cellFormatter={this.renderDate}>
{t('streams.pubdate')}
</TableHeader>
<TableHeader name="closedAt" cellFormatter={this.renderStatus} className={styles.status}>
{t('streams.status')}
</TableHeader>
</DataTable>
<Pager
totalPages={Math.ceil((assets.count || 0) / limit)}
page={this.state.page}
onNewPageHandler={this.onPageClick} />
</div>
: <EmptyCard>{t('streams.empty_result')}</EmptyCard>
}
</div>
);
}
}
const mapStateToProps = ({assets}) => {
return {
assets: assets.toJS()
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchAssets: (...args) => {
dispatch(fetchAssets.apply(this, args));
},
updateAssetState: (...args) => dispatch(updateAssetState.apply(this, args))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Stories);
@@ -1,12 +0,0 @@
fragment metrics on Asset {
id
title
url
author
created_at
commentCount
action_summaries {
actionCount
actionableItemCount
}
}
@@ -1,31 +0,0 @@
fragment commentView on Comment {
id
body
created_at
status
user {
id
name: username
status
}
asset {
id
title
url
}
action_summaries {
count
... on FlagActionSummary {
reason
}
}
actions {
... on FlagAction {
reason
message
user {
username
}
}
}
}
+68
View File
@@ -0,0 +1,68 @@
import {add} from 'coral-framework/services/graphqlRegistry';
const queues = ['all', 'premod', 'flagged', 'accepted', 'rejected'];
const extension = {
mutations: {
SetUserStatus: () => ({
refetchQueries: ['CoralAdmin_Community'],
}),
RejectUsername: () => ({
refetchQueries: ['CoralAdmin_Community'],
}),
SetCommentStatus: ({variables: {commentId, status}}) => ({
updateQueries: {
CoralAdmin_Moderation: (oldData) => {
const comment = queues.reduce((comment, queue) => {
return comment ? comment : oldData[queue].find((c) => c.id === commentId);
}, null);
let accepted = oldData.accepted;
let acceptedCount = oldData.acceptedCount;
let rejected = oldData.rejected;
let rejectedCount = oldData.rejectedCount;
if (status !== comment.status) {
if (status === 'ACCEPTED') {
comment.status = 'ACCEPTED';
acceptedCount++;
accepted = [comment, ...accepted];
}
else if (status === 'REJECTED') {
comment.status = 'REJECTED';
rejectedCount++;
rejected = [comment, ...rejected];
}
}
const premod = oldData.premod.filter((c) => c.id !== commentId);
const flagged = oldData.flagged.filter((c) => c.id !== commentId);
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
if (status === 'REJECTED') {
accepted = oldData.accepted.filter((c) => c.id !== commentId);
acceptedCount = accepted.length < oldData.accepted.length ? oldData.acceptedCount - 1 : oldData.acceptedCount;
}
else if (status === 'ACCEPTED') {
rejected = oldData.rejected.filter((c) => c.id !== commentId);
rejectedCount = rejected.length < oldData.rejected.length ? oldData.rejectedCount - 1 : oldData.rejectedCount;
}
return {
...oldData,
premodCount: Math.max(0, premodCount),
flaggedCount: Math.max(0, flaggedCount),
acceptedCount: Math.max(0, acceptedCount),
rejectedCount: Math.max(0, rejectedCount),
premod,
flagged,
accepted,
rejected,
};
}
}
}),
},
};
add(extension);
@@ -1,153 +0,0 @@
import {graphql} from 'react-apollo';
import SET_USER_STATUS from './setUserStatus.graphql';
import SET_COMMENT_STATUS from './setCommentStatus.graphql';
import SUSPEND_USER from './suspendUser.graphql';
import REJECT_USERNAME from './rejectUsername.graphql';
export const banUser = graphql(SET_USER_STATUS, {
props: ({mutate}) => ({
banUser: ({userId}) => {
return mutate({
variables: {
userId,
status: 'BANNED'
},
refetchQueries: ['Users']
});
}}),
});
export const setUserStatus = graphql(SET_USER_STATUS, {
props: ({mutate}) => ({
approveUser: ({userId}) => {
return mutate({
variables: {
userId,
status: 'APPROVED'
},
refetchQueries: ['Users']
});
}
})
});
export const suspendUser = graphql(SUSPEND_USER, {
props: ({mutate}) => ({
suspendUser: (input) => {
return mutate({
variables: {
input,
},
});
}
})
});
export const rejectUsername = graphql(REJECT_USERNAME, {
props: ({mutate}) => ({
rejectUsername: (input) => {
return mutate({
variables: {
input,
},
refetchQueries: ['Users']
});
}
})
});
const views = ['all', 'premod', 'flagged', 'accepted', 'rejected'];
export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
props: ({mutate}) => ({
acceptComment: ({commentId}) => {
return mutate({
variables: {
commentId,
status: 'ACCEPTED'
},
updateQueries: {
ModQueue: (oldData) => {
const comment = views.reduce((comment, view) => {
return comment ? comment : oldData[view].find((c) => c.id === commentId);
}, null);
let accepted;
let acceptedCount = oldData.acceptedCount;
// if the comment was already in the Approved queue, don't re-add it
if (comment.status === 'ACCEPTED') {
accepted = [...oldData.accepted];
} else {
comment.status = 'ACCEPTED';
acceptedCount++;
accepted = [comment, ...oldData.accepted];
}
const premod = oldData.premod.filter((c) => c.id !== commentId);
const flagged = oldData.flagged.filter((c) => c.id !== commentId);
const rejected = oldData.rejected.filter((c) => c.id !== commentId);
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
const rejectedCount = rejected.length < oldData.rejected.length ? oldData.rejectedCount - 1 : oldData.rejectedCount;
return {
...oldData,
premodCount: Math.max(0, premodCount),
flaggedCount: Math.max(0, flaggedCount),
acceptedCount: Math.max(0, acceptedCount),
rejectedCount: Math.max(0, rejectedCount),
premod,
flagged,
accepted,
rejected,
};
}
}
});
},
rejectComment: ({commentId}) => {
return mutate({
variables: {
commentId,
status: 'REJECTED'
},
updateQueries: {
ModQueue: (oldData) => {
const comment = views.reduce((comment, view) => {
return comment ? comment : oldData[view].find((c) => c.id === commentId);
}, null);
let rejected;
let rejectedCount = oldData.rejectedCount;
// if the item was already in the Rejected queue, don't put it in again
if (comment.status === 'REJECTED') {
rejected = oldData.rejected;
} else {
comment.status = 'REJECTED';
rejectedCount++;
rejected = [comment, ...oldData.rejected];
}
const premod = oldData.premod.filter((c) => c.id !== commentId);
const flagged = oldData.flagged.filter((c) => c.id !== commentId);
const accepted = oldData.accepted.filter((c) => c.id !== commentId);
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
const acceptedCount = accepted.length < oldData.accepted.length ? oldData.acceptedCount - 1 : oldData.acceptedCount;
return {
...oldData,
premodCount: Math.max(0, premodCount),
flaggedCount: Math.max(0, flaggedCount),
acceptedCount: Math.max(0, acceptedCount),
rejectedCount: Math.max(0, rejectedCount),
premod,
flagged,
accepted,
rejected
};
}
}
});
}
})
});
@@ -1,7 +0,0 @@
mutation rejectUsername($input: RejectUsernameInput!) {
rejectUsername(input: $input) {
errors {
translation_key
}
}
}
@@ -1,7 +0,0 @@
mutation setCommentStatus($commentId: ID!, $status: COMMENT_STATUS!){
setCommentStatus(id: $commentId, status: $status) {
errors {
translation_key
}
}
}
@@ -1,7 +0,0 @@
mutation setUserStatus($userId: ID!, $status: USER_STATUS!) {
setUserStatus(id: $userId, status: $status) {
errors {
translation_key
}
}
}
@@ -1,7 +0,0 @@
mutation suspendUser($input: SuspendUserInput!) {
suspendUser(input: $input) {
errors {
translation_key
}
}
}
@@ -1,6 +0,0 @@
query Assets {
assets {
id
title
}
}
@@ -1,22 +0,0 @@
query Counts ($asset_id: ID) {
allCount: commentCount(query: {
asset_id: $asset_id
})
acceptedCount: commentCount(query: {
statuses: [ACCEPTED],
asset_id: $asset_id
})
premodCount: commentCount(query: {
statuses: [PREMOD],
asset_id: $asset_id
})
rejectedCount: commentCount(query: {
statuses: [REJECTED],
asset_id: $asset_id
})
flaggedCount: commentCount(query: {
action_type: FLAG,
asset_id: $asset_id,
statuses: [NONE, PREMOD]
})
}
@@ -1,116 +0,0 @@
import {graphql} from 'react-apollo';
import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
import MOD_QUEUE_LOAD_MORE from './loadMore.graphql';
import MOD_USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql';
import METRICS from './metricsQuery.graphql';
import USER_DETAIL from './userDetail.graphql';
import GET_QUEUE_COUNTS from './getQueueCounts.graphql';
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
options: ({params: {id = null}}) => {
return {
variables: {
asset_id: id,
sort: 'REVERSE_CHRONOLOGICAL'
}
};
},
props: ({ownProps: {params: {id = null}}, data}) => ({
data,
modQueueResort: modQueueResort(id, data.fetchMore),
loadMore: loadMore(data.fetchMore)
})
});
export const getMetrics = graphql(METRICS, {
options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
return {
variables: {
from: dashboardWindowStart,
to: dashboardWindowEnd
}
};
}
});
export const loadMore = (fetchMore) => ({limit = 10, cursor, sort, tab, asset_id}) => {
let variables = {
limit,
cursor,
sort,
asset_id
};
switch(tab) {
case 'all':
variables.statuses = null;
break;
case 'accepted':
variables.statuses = ['ACCEPTED'];
break;
case 'premod':
variables.statuses = ['PREMOD'];
break;
case 'flagged':
variables.statuses = ['NONE', 'PREMOD'];
variables.action_type = 'FLAG';
break;
case 'rejected':
variables.statuses = ['REJECTED'];
break;
}
return fetchMore({
query: MOD_QUEUE_LOAD_MORE,
variables,
updateQuery: (oldData, {fetchMoreResult:{comments}}) => {
return {
...oldData,
[tab]: [
...oldData[tab],
...comments
]
};
}
});
};
export const modUserFlaggedQuery = graphql(MOD_USER_FLAGGED_QUERY, {
options: ({params: {action_type = 'FLAG'}}) => {
return {
variables: {
action_type: action_type
}
};
}
});
export const modQueueResort = (id, fetchMore) => (sort) => {
return fetchMore({
query: MOD_QUEUE_QUERY,
variables: {
asset_id: id,
sort
},
updateQuery: (oldData, {fetchMoreResult:{data}}) => data
});
};
export const getUserDetail = graphql(USER_DETAIL, {
options: ({id}) => {
return {
variables: {author_id: id}
};
}
});
export const getQueueCounts = graphql(GET_QUEUE_COUNTS, {
options: ({params: {id = null}}) => {
return {
pollInterval: 5000,
variables: {
asset_id: id
}
};
}
});
@@ -1,13 +0,0 @@
#import "../fragments/commentView.graphql"
query LoadMoreModQueue($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort, action_type: $action_type}) {
...commentView
action_summaries {
count
... on FlagActionSummary {
reason
}
}
}
}
@@ -1,10 +0,0 @@
#import "../fragments/assetMetricsView.graphql"
query Metrics ($from: Date!, $to: Date!) {
assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
...metrics
}
assetsByActivity: assetMetrics(from: $from, to: $to, sort: ACTIVITY) {
...metrics
}
}
@@ -1,68 +0,0 @@
#import "../fragments/commentView.graphql"
query ModQueue ($asset_id: ID, $sort: SORT_ORDER) {
all: comments(query: {
statuses: [NONE, PREMOD, ACCEPTED, REJECTED],
asset_id: $asset_id,
sort: $sort
}) {
...commentView
}
accepted: comments(query: {
statuses: [ACCEPTED],
asset_id: $asset_id,
sort: $sort
}) {
...commentView
}
premod: comments(query: {
statuses: [PREMOD],
asset_id: $asset_id,
sort: $sort
}) {
...commentView
}
flagged: comments(query: {
action_type: FLAG,
asset_id: $asset_id,
statuses: [NONE, PREMOD],
sort: $sort
}) {
...commentView
}
rejected: comments(query: {
statuses: [REJECTED],
asset_id: $asset_id,
sort: $sort
}) {
...commentView
}
assets: assets {
id
title
url
}
allCount: commentCount(query: {
asset_id: $asset_id
})
acceptedCount: commentCount(query: {
statuses: [ACCEPTED],
asset_id: $asset_id
})
premodCount: commentCount(query: {
statuses: [PREMOD],
asset_id: $asset_id
})
rejectedCount: commentCount(query: {
statuses: [REJECTED],
asset_id: $asset_id
})
flaggedCount: commentCount(query: {
action_type: FLAG,
asset_id: $asset_id,
statuses: [NONE, PREMOD]
})
settings {
organizationName
}
}
@@ -1,26 +0,0 @@
query Users ($action_type: ACTION_TYPE) {
users (query:{action_type: $action_type}){
id
username
status
roles
actions{
id
created_at
... on FlagAction {
reason
message
user {
id
username
}
}
}
action_summaries {
count
... on FlagActionSummary {
reason
}
}
}
}
@@ -1,13 +0,0 @@
query UserDetail ($author_id: ID!) {
user(id: $author_id) {
id
username
created_at
profiles {
id
provider
}
}
totalComments: commentCount(query: {author_id: $author_id})
rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]})
}
+4
View File
@@ -8,6 +8,10 @@ import store from './services/store';
import App from './components/App';
import 'react-mdl/extra/material.js';
import './graphql';
import {loadPluginsTranslations} from 'coral-framework/helpers/plugins';
loadPluginsTranslations();
render(
<ApolloProvider client={client} store={store}>
@@ -10,6 +10,7 @@ const initialState = fromJS({
userDetailId: null,
banDialog: false,
shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show',
sortOrder: 'REVERSE_CHRONOLOGICAL',
suspendUserDialog: {
show: false,
userId: null,
@@ -64,6 +65,8 @@ export default function moderation (state = initialState, action) {
return state.set('userDetailId', action.userId);
case actions.HIDE_USER_DETAIL:
return state.set('userDetailId', null);
case actions.SET_SORT_ORDER:
return state.set('sortOrder', action.order);
default :
return state;
}
@@ -1,8 +1,8 @@
import React from 'react';
import styles from '../Community.css';
import styles from './Community.css';
import BanUserButton from './BanUserButton';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../../../containers/ModerationQueue/helpers/moderationQueueActionsMap';
import {menuActionsMap} from '../../Moderation/helpers/moderationQueueActionsMap';
import t from 'coral-framework/services/i18n';
@@ -1,55 +1,26 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {compose} from 'react-apollo';
import {modUserFlaggedQuery} from 'coral-admin/src/graphql/queries';
import {banUser, setUserStatus, rejectUsername} from 'coral-admin/src/graphql/mutations';
import {
fetchAccounts,
updateSorting,
newPage,
showBanUserDialog,
hideBanUserDialog,
showSuspendUserDialog,
hideSuspendUserDialog
} from '../../actions/community';
import CommunityMenu from './components/CommunityMenu';
import BanUserDialog from './components/BanUserDialog';
import SuspendUserDialog from './components/SuspendUserDialog';
import CommunityMenu from './CommunityMenu';
import BanUserDialog from './BanUserDialog';
import SuspendUserDialog from './SuspendUserDialog';
import People from './People';
import FlaggedAccounts from './FlaggedAccounts';
class CommunityContainer extends Component {
export default class Community extends Component {
constructor(props) {
super(props);
state = {
searchValue: '',
timer: null
};
this.state = {
searchValue: '',
timer: null
};
this.onKeyDownHandler = this.onKeyDownHandler.bind(this);
this.onSearchChange = this.onSearchChange.bind(this);
this.onHeaderClickHandler = this.onHeaderClickHandler.bind(this);
this.onNewPageHandler = this.onNewPageHandler.bind(this);
}
componentWillMount() {
this.props.fetchAccounts({});
}
onKeyDownHandler(e) {
onKeyDownHandler = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.search();
}
}
onSearchChange(e) {
onSearchChange = (e) => {
const value = e.target.value;
this.setState((prevState) => {
prevState.searchValue = value;
@@ -62,6 +33,16 @@ class CommunityContainer extends Component {
});
}
onHeaderClickHandler = (sort) => {
this.props.updateSorting(sort);
this.search();
}
onNewPageHandler = (page) => {
this.props.newPage(page);
this.search({page});
}
search(query = {}) {
const {community} = this.props;
@@ -71,21 +52,10 @@ class CommunityContainer extends Component {
asc: community.ascPeople,
...query
});
}
onHeaderClickHandler(sort) {
this.props.dispatch(updateSorting(sort));
this.search();
}
onNewPageHandler(page) {
this.props.dispatch(newPage(page));
this.search({page});
}
getTabContent(searchValue, props) {
const {community, data} = props;
const {community, root: {users}} = props;
const activeTab = props.route.path === ':id' ? 'flagged' : props.route.path;
if (activeTab === 'people') {
@@ -108,9 +78,7 @@ class CommunityContainer extends Component {
return (
<div>
<FlaggedAccounts
commenters={data.users}
isFetching={data.loading}
error={data.error}
commenters={users}
showBanUserDialog={props.showBanUserDialog}
approveUser={props.approveUser}
rejectUsername={props.rejectUsername}
@@ -134,7 +102,6 @@ class CommunityContainer extends Component {
render() {
const {searchValue} = this.state;
const tab = this.getTabContent(searchValue, this.props);
return (
@@ -148,22 +115,3 @@ class CommunityContainer extends Component {
}
}
const mapStateToProps = (state) => ({
community: state.community.toJS()
});
const mapDispatchToProps = (dispatch) => ({
fetchAccounts: (query) => dispatch(fetchAccounts(query)),
showBanUserDialog: (user) => dispatch(showBanUserDialog(user)),
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
showSuspendUserDialog: (user) => dispatch(showSuspendUserDialog(user)),
hideSuspendUserDialog: () => dispatch(hideSuspendUserDialog())
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
modUserFlaggedQuery,
banUser,
setUserStatus,
rejectUsername
)(CommunityContainer);
@@ -2,20 +2,18 @@ import React from 'react';
import t from 'coral-framework/services/i18n';
import styles from './Community.css';
import Loading from './Loading';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
import User from './components/User';
import User from './User';
const FlaggedAccounts = ({...props}) => {
const {commenters, isFetching} = props;
const hasResults = !isFetching && commenters && !!commenters.length;
const {commenters} = props;
const hasResults = commenters && !!commenters.length;
// if (commenter.status === 'PENDING' && commenter.actions.length > 0) {
return (
<div className={styles.container}>
<div className={styles.mainFlaggedContent}>
{ isFetching && <Loading /> }
{
hasResults
? commenters.map((commenter, index) => {
@@ -1,9 +1,9 @@
import React from 'react';
import styles from './Community.css';
import Table from './Table';
import Table from '../containers/Table';
import {Pager, Icon} from 'coral-ui';
import EmptyCard from '../../components/EmptyCard';
import EmptyCard from '../../../components/EmptyCard';
import t from 'coral-framework/services/i18n';
const tableHeaders = [
@@ -0,0 +1,54 @@
import React from 'react';
import {SelectField, Option} from 'react-mdl-selectfield';
import styles from '../components/Community.css';
import t from 'coral-framework/services/i18n';
export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onCommenterStatusChange}) => (
<table className={`mdl-data-table ${styles.dataTable}`}>
<thead>
<tr>
{headers.map((header, i) =>(
<th
key={i}
className="mdl-data-table__cell--non-numeric"
onClick={() => onHeaderClickHandler({field: header.field})}>
{header.title}
</th>
))}
</tr>
</thead>
<tbody>
{commenters.map((row, i)=> (
<tr key={i}>
<td className="mdl-data-table__cell--non-numeric">
{row.username}
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
</td>
<td className="mdl-data-table__cell--non-numeric">
{row.created_at}
</td>
<td className="mdl-data-table__cell--non-numeric">
<SelectField label={'Select me'} value={row.status || ''}
className={styles.selectField}
label={t('community.status')}
onChange={(status) => onCommenterStatusChange(row.id, status)}>
<Option value={'ACTIVE'}>{t('community.active')}</Option>
<Option value={'BANNED'}>{t('community.banned')}</Option>
</SelectField>
</td>
<td className="mdl-data-table__cell--non-numeric">
<SelectField label={'Select me'} value={row.roles[0] || ''}
className={styles.selectField}
label={t('community.role')}
onChange={(role) => onRoleChange(row.id, role)}>
<Option value={''}>.</Option>
<Option value={'STAFF'}>{t('community.staff')}</Option>
<Option value={'MODERATOR'}>{t('community.moderator')}</Option>
<Option value={'ADMIN'}>{t('community.admin')}</Option>
</SelectField>
</td>
</tr>
))}
</tbody>
</table>
);
@@ -1,5 +1,5 @@
import React from 'react';
import styles from '../Community.css';
import styles from './Community.css';
import ActionButton from './ActionButton';
@@ -0,0 +1,107 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import {Spinner} from 'coral-ui';
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
import {
fetchAccounts,
updateSorting,
newPage,
showBanUserDialog,
hideBanUserDialog,
showSuspendUserDialog,
hideSuspendUserDialog
} from '../../../actions/community';
import Community from '../components/Community';
class CommunityContainer extends Component {
componentWillMount() {
this.props.fetchAccounts({});
}
approveUser = ({userId}) => {
return this.props.setUserStatus({userId, status: 'APPROVED'});
}
banUser = ({userId}) => {
return this.props.setUserStatus({userId, status: 'BANNED'});
}
render() {
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
}
if (!('users' in this.props.root)) {
return <div><Spinner/></div>;
}
return (
<Community {...this.props} approveUser={this.approveUser} banUser={this.banUser}/>
);
}
}
export const withCommunityQuery = withQuery(gql`
query CoralAdmin_Community($action_type: ACTION_TYPE) {
users(query:{action_type: $action_type}){
id
username
status
roles
actions{
id
created_at
... on FlagAction {
reason
message
user {
id
username
}
}
}
action_summaries {
count
... on FlagActionSummary {
reason
}
}
}
}
`, {
options: ({params: {action_type = 'FLAG'}}) => {
return {
variables: {
action_type: action_type
}
};
}
});
const mapStateToProps = (state) => ({
community: state.community.toJS()
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
fetchAccounts,
showBanUserDialog,
hideBanUserDialog,
showSuspendUserDialog,
hideSuspendUserDialog,
updateSorting,
newPage,
}, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withCommunityQuery,
withSetUserStatus,
withRejectUsername,
)(CommunityContainer);
@@ -0,0 +1,37 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose} from 'react-apollo';
import {setRole, setCommenterStatus} from '../../../actions/community';
import Table from '../components/Table';
class TableContainer extends Component {
constructor (props) {
super(props);
}
render () {
return <Table
{...this.props}
onRoleChange={this.props.setRole}
onCommenterStatusChange={this.props.setCommenterStatus}
commenters={this.props.commenters}
/>;
}
}
const mapStateToProps = (state) => ({
commenters: state.community.get('accounts'),
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
setCommenterStatus,
setRole,
}, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
)(TableContainer);
@@ -0,0 +1,2 @@
export {default as Community} from './containers/Community';
export {default as CommunityLayout} from './components/CommunityLayout';
@@ -1,12 +1,4 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {
fetchSettings,
updateSettings,
saveSettingsToServer,
updateWordlist,
updateDomainlist
} from '../../actions/settings';
import {Button, List, Item, Card, Spinner} from 'coral-ui';
import styles from './Configure.css';
@@ -16,45 +8,36 @@ import TechSettings from './TechSettings';
import t from 'coral-framework/services/i18n';
import {can} from 'coral-framework/services/perms';
class Configure extends Component {
constructor (props) {
super(props);
export default class Configure extends Component {
this.state = {
activeSection: 'stream',
changed: false,
errors: {}
};
this.changeSection = this.changeSection.bind(this);
}
componentWillMount = () => {
this.props.dispatch(fetchSettings());
}
state = {
activeSection: 'stream',
changed: false,
errors: {}
};
saveSettings = () => {
this.props.dispatch(saveSettingsToServer());
this.props.saveSettingsToServer();
this.setState({changed: false});
}
changeSection(activeSection) {
changeSection = (activeSection) => {
this.setState({activeSection});
}
onChangeWordlist = (listName, list) => {
this.setState({changed: true});
this.props.dispatch(updateWordlist(listName, list));
this.props.updateWordlist(listName, list);
}
onChangeDomainlist = (listName, list) => {
this.setState({changed: true});
this.props.dispatch(updateDomainlist(listName, list));
this.props.updateDomainlist(listName, list);
}
onSettingUpdate = (setting) => {
this.setState({changed: true});
this.props.dispatch(updateSettings(setting));
this.props.updateSettings(setting);
}
// Sets an arbitrary error string and a boolean state.
@@ -175,9 +158,3 @@ class Configure extends Component {
);
}
}
const mapStateToProps = (state) => ({
auth: state.auth.toJS(),
settings: state.settings.toJS()
});
export default connect(mapStateToProps)(Configure);
@@ -1,5 +1,4 @@
import React, {Component} from 'react';
import t from 'coral-framework/services/i18n';
import styles from './Configure.css';
import {Button, Card} from 'coral-ui';
@@ -0,0 +1,42 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose} from 'react-apollo';
import {
fetchSettings,
updateSettings,
saveSettingsToServer,
updateWordlist,
updateDomainlist
} from '../../../actions/settings';
import Configure from '../components/Configure';
class ConfigureContainer extends Component {
componentWillMount = () => {
this.props.fetchSettings();
}
render () {
return <Configure {...this.props} />;
}
}
const mapStateToProps = (state) => ({
auth: state.auth.toJS(),
settings: state.settings.toJS()
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
fetchSettings,
updateSettings,
saveSettingsToServer,
updateWordlist,
updateDomainlist
}, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
)(ConfigureContainer);
@@ -0,0 +1 @@
export {default} from './containers/Configure';
@@ -1,5 +1,5 @@
import React, {PropTypes} from 'react';
import styles from 'coral-admin/src/containers/Dashboard/Dashboard.css';
import styles from './Dashboard.css';
import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
@@ -0,0 +1,15 @@
import React from 'react';
import FlagWidget from './FlagWidget';
import ActivityWidget from './ActivityWidget';
import CountdownTimer from './CountdownTimer';
import styles from './Dashboard.css';
export default ({root: {assetsByActivity, assetsByFlag}, reloadData}) => (
<div>
<CountdownTimer handleTimeout={reloadData} />
<div className={styles.Dashboard}>
<FlagWidget assets={assetsByFlag} />
<ActivityWidget assets={assetsByActivity} />
</div>
</div>
);
@@ -0,0 +1,65 @@
import React from 'react';
import {connect} from 'react-redux';
import Dashboard from '../components/Dashboard';
import {compose, gql} from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import {Spinner} from 'coral-ui';
class DashboardContainer extends React.Component {
reloadData = () => {
this.props.data.refetch();
}
render () {
if (this.props.data.loading) {
return <Spinner />;
}
return <Dashboard {...this.props} reloadData={this.reloadData} />;
}
}
export const witDashboardQuery = withQuery(gql`
query CoralAdmin_Dashboard($from: Date!, $to: Date!) {
assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
...CoralAdmin_Metrics
}
assetsByActivity: assetMetrics(from: $from, to: $to, sort: ACTIVITY) {
...CoralAdmin_Metrics
}
}
fragment CoralAdmin_Metrics on Asset {
id
title
url
author
created_at
commentCount
action_summaries {
actionCount
actionableItemCount
}
}
`, {
options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
return {
variables: {
from: dashboardWindowStart,
to: dashboardWindowEnd
}
};
}
});
const mapStateToProps = (state) => {
return {
settings: state.settings.toJS(),
moderation: state.moderation.toJS()
};
};
export default compose(
connect(mapStateToProps),
witDashboardQuery,
)(DashboardContainer);
@@ -0,0 +1 @@
export {default} from './containers/Dashboard.js';
@@ -0,0 +1,79 @@
import React, {Component} from 'react';
import styles from './style.css';
import {Wizard, WizardNav} from 'coral-ui';
import Layout from 'coral-admin/src/components/ui/Layout';
import InitialStep from './Steps/InitialStep';
import AddOrganizationName from './Steps/AddOrganizationName';
import CreateYourAccount from './Steps/CreateYourAccount';
import PermittedDomainsStep from './Steps/PermittedDomainsStep';
import FinalStep from './Steps/FinalStep';
export default class Install extends Component {
handleDomainsChange = (value) => {
this.props.updatePermittedDomains(value);
};
handleSettingsChange = ({currentTarget: {name, value}}) => {
this.props.updateSettingsFormData(name, value);
};
handleUserChange = ({currentTarget: {name, value}}) => {
this.props.updateUserFormData(name, value);
};
handleSettingsSubmit = (e) => {
e.preventDefault();
this.props.submitSettings();
};
handleUserSubmit = (e) => {
e.preventDefault();
this.props.submitUser();
}
render() {
const {install} = this.props;
return (
<Layout restricted={true}>
<div className={styles.Install}>
{
!install.alreadyInstalled ? (
<div>
<h2>Welcome to the Coral Project</h2>
{ install.step !== 0 ? <WizardNav items={install.navItems} currentStep={install.step} icon='check'/> : null }
<Wizard
currentStep={install.step}
nextStep={this.props.nextStep}
previousStep={this.props.previousStep}
goToStep={this.props.goToStep}
>
<InitialStep/>
<AddOrganizationName
install={install}
handleSettingsChange={this.handleSettingsChange}
handleSettingsSubmit={this.handleSettingsSubmit}
/>
<CreateYourAccount
install={install}
handleUserChange={this.handleUserChange}
handleUserSubmit={this.handleUserSubmit}
/>
<PermittedDomainsStep
install={install}
handleDomainsChange={this.handleDomainsChange}
finishInstall={this.props.finishInstall}
/>
<FinalStep/>
</Wizard>
</div>
) : (
<div>Talk is already installed</div>
)
}
</div>
</Layout>
);
}
}
@@ -8,18 +8,18 @@ const AddOrganizationName = (props) => {
const {handleSettingsChange, handleSettingsSubmit, install} = props;
return (
<div className={styles.step}>
<p>{t('ADD_ORGANIZATION.DESCRIPTION')}</p>
<p>{t('install.add_organization.description')}</p>
<div className={styles.form}>
<form onSubmit={handleSettingsSubmit}>
<TextField
className={styles.TextField}
id="organizationName"
type="text"
label={t('ADD_ORGANIZATION.LABEL')}
label={t('install.add_organization.label')}
onChange={handleSettingsChange}
showErrors={install.showErrors}
errorMsg={install.errors.organizationName} />
<Button type="submit" cStyle='black' full>{t('ADD_ORGANIZATION.SAVE')}</Button>
<Button type="submit" cStyle='black' full>{t('install.add_organization.save')}</Button>
</form>
</div>
</div>
@@ -14,7 +14,7 @@ const InitialStep = (props) => {
className={styles.textField}
id="email"
type="email"
label={t('CREATE.EMAIL')}
label={t('install.create.email')}
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.email}
@@ -25,7 +25,7 @@ const InitialStep = (props) => {
className={styles.textField}
id="username"
type="text"
label={t('CREATE.USERNAME')}
label={t('install.create.username')}
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.username}
@@ -35,7 +35,7 @@ const InitialStep = (props) => {
className={styles.textField}
id="password"
type="password"
label={t('CREATE.PASSWORD')}
label={t('install.create.password')}
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.password}
@@ -45,7 +45,7 @@ const InitialStep = (props) => {
className={styles.textField}
id="confirmPassword"
type="password"
label={t('CREATE.CONFIRM_PASSWORD')}
label={t('install.create.confirm_password')}
onChange={handleUserChange}
showErrors={install.showErrors}
errorMsg={install.errors.confirm_password}
@@ -53,7 +53,7 @@ const InitialStep = (props) => {
{
!props.install.isLoading ?
<Button cStyle='black' type="submit" full>{t('CREATE.SAVE')}</Button>
<Button cStyle='black' type="submit" full>{t('install.create.save')}</Button>
:
<Spinner />
}
@@ -8,9 +8,9 @@ import t from 'coral-framework/services/i18n';
const InitialStep = () => {
return (
<div className={`${styles.step} ${styles.finalStep}`}>
<p>{t('FINAL.DESCRIPTION')}</p>
<Button raised><Link to='/admin'>{t('FINAL.LAUNCH')}</Link></Button>
<Button cStyle='black' raised><a href="http://coralproject.net">{t('FINAL.CLOSE')}</a></Button>
<p>{t('install.final.description')}</p>
<Button raised><Link to='/admin'>{t('install.final.launch')}</Link></Button>
<Button cStyle='black' raised><a href="http://coralproject.net">{t('install.final.close')}</a></Button>
</div>
);
};
@@ -8,8 +8,8 @@ const InitialStep = (props) => {
const {nextStep} = props;
return (
<div className={styles.step}>
<p>{t('INITIAL.DESCRIPTION')}</p>
<Button cStyle='green' onClick={nextStep} raised>{t('INITIAL.SUBMIT')}</Button>
<p>{t('install.initial.description')}</p>
<Button cStyle='green' onClick={nextStep} raised>{t('install.initial.submit')}</Button>
</div>
);
};
@@ -10,9 +10,9 @@ const PermittedDomainsStep = (props) => {
const domains = install.data.settings.domains.whitelist;
return (
<div className={styles.step}>
<h3>{t('PERMITTED_DOMAINS.TITLE')}</h3>
<h3>{t('install.permitted_domains.title')}</h3>
<Card className={styles.card}>
<p>{t('PERMITTED_DOMAINS.DESCRIPTION')}</p>
<p>{t('install.permitted_domains.description')}</p>
<TagsInput
value={domains}
inputProps={{placeholder: 'URL'}}
@@ -21,7 +21,7 @@ const PermittedDomainsStep = (props) => {
onChange={(tags) => handleDomainsChange(tags)}
/>
</Card>
<Button cStyle='green' onClick={finishInstall} raised>{t('PERMITTED_DOMAINS.SUBMIT')}</Button>
<Button cStyle='green' onClick={finishInstall} raised>{t('install.permitted_domains.submit')}</Button>
</div>
);
};
@@ -0,0 +1,57 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose} from 'react-apollo';
import Install from '../components/Install';
import {
goToStep,
nextStep,
submitUser,
checkInstall,
previousStep,
finishInstall,
submitSettings,
updateUserFormData,
updateSettingsFormData,
updatePermittedDomains
} from '../../../actions/install';
class InstallContainer extends Component {
componentDidMount() {
const {checkInstall} = this.props;
checkInstall(() => {
this.context.router.push('/admin');
});
}
render() {
return <Install {...this.props}/>;
}
}
InstallContainer.contextTypes = {
router: React.PropTypes.object
};
const mapStateToProps = (state) => ({
install: state.install.toJS()
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
goToStep,
nextStep,
submitUser,
checkInstall,
previousStep,
finishInstall,
submitSettings,
updateUserFormData,
updateSettingsFormData,
updatePermittedDomains
}, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
)(InstallContainer);
@@ -0,0 +1 @@
export {default} from './containers/Install.js';
@@ -39,8 +39,8 @@ const Comment = ({
}
// since words are checked against word boundaries on the backend,
// this should be the behavior on the front end as well.
// currently the highlighter plugin does not support this out of the box.
// should be the behavior on the front end as well.
// currently the highlighter plugin does not support out of the box.
const searchWords = [...suspectWords, ...bannedWords]
.filter((w) => {
return new RegExp(`(^|\\s)${w}(\\s|$)`).test(comment.body);
@@ -82,7 +82,12 @@ const Comment = ({
{t('comment.banned_user')}
</span>
: null}
<Slot fill="adminCommentInfoBar" comment={comment} />
<Slot
data={props.data}
root={props.root}
fill="adminCommentInfoBar"
comment={comment}
/>
</div>
<div className={styles.moderateArticle}>
Story: {comment.asset.title}
@@ -104,7 +109,12 @@ const Comment = ({
<Icon name="open_in_new" /> {t('comment.view_context')}
</a>
</p>
<Slot fill="adminCommentContent" comment={comment} />
<Slot
data={props.data}
root={props.root}
fill="adminCommentContent"
comment={comment}
/>
<div className={styles.sideActions}>
{links
? <span className={styles.hasLinks}>
@@ -135,12 +145,22 @@ const Comment = ({
);
})}
</div>
<Slot fill="adminSideActions" comment={comment} />
<Slot
data={props.data}
root={props.root}
fill="adminSideActions"
comment={comment}
/>
</div>
</div>
</div>
<div>
<Slot fill="adminCommentDetailArea" comment={comment} />
<Slot
data={props.data}
root={props.root}
fill="adminCommentDetailArea"
comment={comment}
/>
</div>
{flagActions && flagActions.length
? <FlagBox
@@ -3,12 +3,13 @@
background: #989797;
margin: 2px;
vertical-align: middle;
padding: 4px 7px;
padding: 1px 7px;
border-radius: 2px;
margin-left: 10px;
margin-left: 2px;
line-height: 20px;
box-sizing: border-box;
height: 27px;
height: 21px;
right: 0;
margin-top: -2px;
font-size: 12px;
}

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