replaced eslint:recommended with prettier

This commit is contained in:
Wyatt Johnson
2018-01-11 20:00:34 -07:00
parent d56c19016a
commit 0abc2ca243
649 changed files with 16235 additions and 13008 deletions
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import {murmur3} from 'murmurhash-js';
import { murmur3 } from 'murmurhash-js';
import styles from './AccountHistory.css';
import cn from 'classnames';
import flatten from 'lodash/flatten';
@@ -8,21 +8,27 @@ import orderBy from 'lodash/orderBy';
import moment from 'moment';
const buildUserHistory = (userState = {}) => {
return orderBy(flatten(Object.keys(userState.status)
.filter((k) => k !== '__typename')
.map((k) => userState.status[k].history)), 'created_at', 'desc');
return orderBy(
flatten(
Object.keys(userState.status)
.filter(k => k !== '__typename')
.map(k => userState.status[k].history)
),
'created_at',
'desc'
);
};
const buildActionResponse = (typename, until, status) => {
switch (typename) {
case 'UsernameStatusHistory':
return `Username ${status}`;
case 'BannedStatusHistory':
return status ? 'User banned' : 'Ban removed';
case 'SuspensionStatusHistory':
return until ? 'Account Suspended' : 'Suspension removed' ;
default:
return '-';
case 'UsernameStatusHistory':
return `Username ${status}`;
case 'BannedStatusHistory':
return status ? 'User banned' : 'Ban removed';
case 'SuspensionStatusHistory':
return until ? 'Account Suspended' : 'Suspension removed';
default:
return '-';
}
};
@@ -35,31 +41,55 @@ const getModerationValue = (userId, assignedBy = {}) => {
class AccountHistory extends React.Component {
render() {
const {user} = this.props;
const { user } = this.props;
const userHistory = buildUserHistory(user.state);
return (
<div>
<div className={cn(styles.table, 'talk-admin-account-history')}>
<div className={cn(styles.headerRow, 'talk-admin-account-history-header-row')}>
<div
className={cn(
styles.headerRow,
'talk-admin-account-history-header-row'
)}
>
<div className={styles.headerRowItem}>Date</div>
<div className={styles.headerRowItem}>Action</div>
<div className={styles.headerRowItem}>Moderation</div>
</div>
{
userHistory.map(({__typename, created_at, assigned_by, until, status}) => (
<div className={cn(styles.row, 'talk-admin-account-history-row')} key={`${__typename}_${murmur3(created_at)}`}>
<div className={cn(styles.item, 'talk-admin-account-history-row-date')}>
{userHistory.map(
({ __typename, created_at, assigned_by, until, status }) => (
<div
className={cn(styles.row, 'talk-admin-account-history-row')}
key={`${__typename}_${murmur3(created_at)}`}
>
<div
className={cn(
styles.item,
'talk-admin-account-history-row-date'
)}
>
{moment(new Date(created_at)).format('MMM DD, YYYY')}
</div>
<div className={cn(styles.item, styles.action, 'talk-admin-account-history-row-status')}>
<div
className={cn(
styles.item,
styles.action,
'talk-admin-account-history-row-status'
)}
>
{buildActionResponse(__typename, until, status)}
</div>
<div className={cn(styles.item, 'talk-admin-account-history-row-assigned-by')}>
<div
className={cn(
styles.item,
'talk-admin-account-history-row-assigned-by'
)}
>
{getModerationValue(user.id, assigned_by)}
</div>
</div>
))
}
)
)}
</div>
</div>
);
@@ -1,9 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button, Icon} from 'coral-ui';
import {Menu} from 'react-mdl';
import { Button, Icon } from 'coral-ui';
import { Menu } from 'react-mdl';
import cn from 'classnames';
import {findDOMNode} from 'react-dom';
import { findDOMNode } from 'react-dom';
import styles from './ActionsMenu.css';
import t from 'coral-framework/services/i18n';
@@ -13,36 +13,41 @@ let count = 0;
class ActionsMenu extends React.Component {
id = `actions-dropdown-${count++}`;
menu = null;
state = {open: false};
state = { open: false };
timeout = null;
componentWillUnmount() {
clearTimeout(this.timeout);
}
handleRef = (ref) => {
handleRef = ref => {
this.menu = ref ? findDOMNode(ref).parentNode : null;
}
};
syncOpenState = () => {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.setState({open: this.menu.className.indexOf('is-visible') >= 0});
this.setState({ open: this.menu.className.indexOf('is-visible') >= 0 });
}, 150);
};
render() {
const {className = '', buttonClassNames = '', label = ''} = this.props;
const { className = '', buttonClassNames = '', label = '' } = this.props;
return (
<div className={cn(styles.root, className)} onBlur={this.syncOpenState} >
<div className={cn(styles.root, className)} onBlur={this.syncOpenState}>
<Button
cStyle='actions'
className={cn(styles.button, {[styles.buttonOpen]: this.state.open}, buttonClassNames)}
cStyle="actions"
className={cn(
styles.button,
{ [styles.buttonOpen]: this.state.open },
buttonClassNames
)}
disabled={false}
id={this.id}
onClick={this.syncOpenState}
icon={this.props.icon}
raised>
raised
>
{label ? label : t('modqueue.actions')}
<Icon
name={this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
@@ -1,13 +1,18 @@
import React from 'react';
import cn from 'classnames';
import {MenuItem} from 'react-mdl';
import { MenuItem } from 'react-mdl';
import PropTypes from 'prop-types';
import styles from './ActionsMenu.css';
import camelCase from 'lodash/camelCase';
const ActionsMenuItem = (props) =>
<MenuItem className={cn(styles.menuItem, props.className, 'action-menu-item')} {...props} id={camelCase(props.children)}/>;
const ActionsMenuItem = props => (
<MenuItem
className={cn(styles.menuItem, props.className, 'action-menu-item')}
{...props}
id={camelCase(props.children)}
/>
);
ActionsMenuItem.propTypes = {
className: PropTypes.string,
children: PropTypes.string,
+80 -54
View File
@@ -2,102 +2,128 @@ import React from 'react';
import PropTypes from 'prop-types';
import Layout from 'coral-admin/src/components/ui/Layout';
import styles from './NotFound.css';
import {Button, TextField, Alert, Success} from 'coral-ui';
import { Button, TextField, Alert, Success } from 'coral-ui';
import Recaptcha from 'react-recaptcha';
import cn from 'classnames';
class AdminLogin extends React.Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {email: '', password: '', requestPassword: false};
this.state = { email: '', password: '', requestPassword: false };
}
handleSignIn = (e) => {
handleSignIn = e => {
e.preventDefault();
this.props.handleLogin(this.state.email, this.state.password);
}
};
onRecaptchaLoad = () => {
// do something?
}
};
onRecaptchaVerify = (recaptchaResponse) => {
this.props.handleLogin(this.state.email, this.state.password, recaptchaResponse);
}
onRecaptchaVerify = recaptchaResponse => {
this.props.handleLogin(
this.state.email,
this.state.password,
recaptchaResponse
);
};
handleRequestPassword = (e) => {
handleRequestPassword = e => {
e.preventDefault();
this.props.requestPasswordReset(this.state.email);
}
};
render () {
const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props;
render() {
const { errorMessage, loginMaxExceeded, recaptchaPublic } = this.props;
const signInForm = (
<form className="talk-admin-login-sign-in" onSubmit={this.handleSignIn}>
{errorMessage && <Alert>{errorMessage}</Alert>}
<TextField
id="email"
label='Email Address'
label="Email Address"
value={this.state.email}
onChange={(e) => this.setState({email: e.target.value})} />
onChange={e => this.setState({ email: e.target.value })}
/>
<TextField
id="password"
label='Password'
label="Password"
value={this.state.password}
onChange={(e) => this.setState({password: e.target.value})}
type='password' />
<div style={{height: 10}}></div>
onChange={e => this.setState({ password: e.target.value })}
type="password"
/>
<div style={{ height: 10 }} />
<Button
className="talk-admin-login-sign-in-button"
type='submit'
cStyle='black'
type="submit"
cStyle="black"
full
onClick={this.handleSignIn}>Sign In</Button>
onClick={this.handleSignIn}
>
Sign In
</Button>
<p className={styles.forgotPasswordCTA}>
Forgot your password? <a href="#" className={styles.forgotPasswordLink} onClick={(e) => {
e.preventDefault();
this.setState({requestPassword: true});
}}>Request a new one.</a>
Forgot your password?{' '}
<a
href="#"
className={styles.forgotPasswordLink}
onClick={e => {
e.preventDefault();
this.setState({ requestPassword: true });
}}
>
Request a new one.
</a>
</p>
{
loginMaxExceeded &&
{loginMaxExceeded && (
<Recaptcha
sitekey={recaptchaPublic}
render='explicit'
theme='dark'
render="explicit"
theme="dark"
onloadCallback={this.onRecaptchaLoad}
verifyCallback={this.onRecaptchaVerify} />
}
verifyCallback={this.onRecaptchaVerify}
/>
)}
</form>
);
const requestPasswordForm = (
this.props.passwordRequestSuccess
? <p className={styles.passwordRequestSuccess} onClick={() => {
const requestPasswordForm = this.props.passwordRequestSuccess ? (
<p
className={styles.passwordRequestSuccess}
onClick={() => {
location.href = location.href;
}}>
{this.props.passwordRequestSuccess} <a className={styles.signInLink} href="#">Sign in</a>
<Success />
</p>
: <form onSubmit={this.handleRequestPassword}>
<TextField
label='Email Address'
value={this.state.email}
onChange={(e) => this.setState({email: e.target.value})} />
<Button
type='submit'
cStyle='black'
full
onClick={this.handleRequestPassword}>Reset Password</Button>
</form>
}}
>
{this.props.passwordRequestSuccess}{' '}
<a className={styles.signInLink} href="#">
Sign in
</a>
<Success />
</p>
) : (
<form onSubmit={this.handleRequestPassword}>
<TextField
label="Email Address"
value={this.state.email}
onChange={e => this.setState({ email: e.target.value })}
/>
<Button
type="submit"
cStyle="black"
full
onClick={this.handleRequestPassword}
>
Reset Password
</Button>
</form>
);
return (
<Layout fixedDrawer restricted={true}>
<div className={cn(styles.loginLayout, 'talk-admin-login')}>
<h1 className={styles.loginHeader}>Team sign in</h1>
<p className={styles.loginCTA}>Sign in to interact with your community.</p>
{ this.state.requestPassword ? requestPasswordForm : signInForm }
<p className={styles.loginCTA}>
Sign in to interact with your community.
</p>
{this.state.requestPassword ? requestPasswordForm : signInForm}
</div>
</Layout>
);
+1 -1
View File
@@ -5,7 +5,7 @@ import 'material-design-lite';
import AppRouter from '../AppRouter';
export default class App extends React.Component {
render () {
render() {
return (
<div>
<ToastContainer />
@@ -3,15 +3,19 @@ import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './ApproveButton.css';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
const ApproveButton = ({active, minimal, onClick, className}) => {
const ApproveButton = ({ active, minimal, onClick, className }) => {
const text = active ? t('modqueue.approved') : t('modqueue.approve');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
className={cn(
styles.root,
{ [styles.minimal]: minimal, [styles.active]: active },
className
)}
onClick={onClick}
>
<Icon name={'done'} className={styles.icon} />
@@ -28,4 +32,3 @@ ApproveButton.propTypes = {
};
export default ApproveButton;
@@ -1,16 +1,15 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import { Dialog } from 'coral-ui';
import styles from './BanUserDialog.css';
import Button from 'coral-ui/components/Button';
import t from 'coral-framework/services/i18n';
const initialState = {step: 0, message: ''};
const initialState = { step: 0, message: '' };
class BanUserDialog extends React.Component {
state = initialState;
componentWillReceiveProps(next) {
@@ -18,53 +17,44 @@ class BanUserDialog extends React.Component {
this.setState(initialState);
}
}
handleMessageChange = (e) => {
const {value: message} = e;
this.setState({message});
}
handleMessageChange = e => {
const { value: message } = e;
this.setState({ message });
};
goToStep1 = () => {
this.setState({
step: 1,
message: t(
'bandialog.email_message_ban',
this.props.username,
),
message: t('bandialog.email_message_ban', this.props.username),
});
}
};
renderStep0() {
const {
onCancel,
username,
info,
} = this.props;
const { onCancel, username, info } = this.props;
return (
<section>
<h2 className={styles.header}>
{t('bandialog.ban_user')}
</h2>
<h2 className={styles.header}>{t('bandialog.ban_user')}</h2>
<h3 className={styles.subheader}>
{t('bandialog.are_you_sure', username)}
</h3>
<p className={styles.description}>
{info}
</p>
<p className={styles.description}>{info}</p>
<div className={styles.buttons}>
<Button
className={cn('talk-ban-user-dialog-button-cancel')}
cStyle="white"
onClick={onCancel}
raised >
raised
>
{t('bandialog.cancel')}
</Button>
<Button
<Button
className={cn('talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={this.goToStep1}
raised >
raised
>
{t('bandialog.yes_ban_user')}
</Button>
</div>
@@ -73,22 +63,19 @@ class BanUserDialog extends React.Component {
}
renderStep1() {
const {
onCancel,
onPerform,
} = this.props;
const {message} = this.state;
const { onCancel, onPerform } = this.props;
const { message } = this.state;
return (
<section>
<h2 className={styles.header}>
{t('bandialog.notify_ban_headline')}
</h2>
<h2 className={styles.header}>{t('bandialog.notify_ban_headline')}</h2>
<p className={styles.description}>
{t('bandialog.notify_ban_description')}
</p>
<fieldset>
<legend className={styles.legend}>{t('bandialog.write_a_message')}</legend>
<legend className={styles.legend}>
{t('bandialog.write_a_message')}
</legend>
<textarea
rows={5}
className={styles.messageInput}
@@ -101,14 +88,16 @@ class BanUserDialog extends React.Component {
className={cn('talk-ban-user-dialog-button-cancel')}
cStyle="white"
onClick={onCancel}
raised >
raised
>
{t('bandialog.cancel')}
</Button>
<Button
<Button
className={cn('talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={onPerform}
raised >
raised
>
{t('bandialog.send')}
</Button>
</div>
@@ -117,16 +106,19 @@ class BanUserDialog extends React.Component {
}
render() {
const {step} = this.state;
const {open, onCancel} = this.props;
const { step } = this.state;
const { open, onCancel } = this.props;
return (
<Dialog
className={cn(styles.dialog, 'talk-ban-user-dialog')}
id="banUserDialog"
open={open}
onCancel={onCancel}
title={t('bandialog.ban_user')} >
<span className={styles.close} onClick={onCancel}>×</span>
title={t('bandialog.ban_user')}
>
<span className={styles.close} onClick={onCancel}>
×
</span>
{step === 0 && this.renderStep0()}
{step === 1 && this.renderStep1()}
</Dialog>
@@ -1,15 +1,11 @@
import React from 'react';
import {Button} from 'coral-ui';
import { Button } from 'coral-ui';
import t from 'coral-framework/services/i18n';
import {withCopyToClipboard} from 'coral-framework/hocs';
import { withCopyToClipboard } from 'coral-framework/hocs';
class ButtonCopyToClipboard extends React.Component {
render () {
return (
<Button {...this.props} >
{t('common.copy')}
</Button>
);
render() {
return <Button {...this.props}>{t('common.copy')}</Button>;
}
}
@@ -1,10 +1,10 @@
import React from 'react';
import {murmur3} from 'murmurhash-js';
import {CSSTransitionGroup} from 'react-transition-group';
import { murmur3 } from 'murmurhash-js';
import { CSSTransitionGroup } from 'react-transition-group';
import styles from './CommentAnimatedEdit.css';
import PropTypes from 'prop-types';
const CommentBodyHighlighter = ({children, body}) => {
const CommentBodyHighlighter = ({ children, body }) => {
return (
<CSSTransitionGroup
component={'div'}
@@ -20,7 +20,9 @@ const CommentBodyHighlighter = ({children, body}) => {
transitionEnterTimeout={3600}
transitionLeaveTimeout={2800}
>
{React.cloneElement(React.Children.only(children), {key: murmur3(body)})}
{React.cloneElement(React.Children.only(children), {
key: murmur3(body),
})}
</CSSTransitionGroup>
);
};
@@ -1,5 +1,5 @@
import React from 'react';
import {matchLinks} from '../utils';
import { matchLinks } from '../utils';
import memoize from 'lodash/memoize';
function escapeRegExp(string) {
@@ -9,18 +9,18 @@ function escapeRegExp(string) {
// generate a regulare expression that catches the `phrases`.
function generateRegExp(phrases) {
const inner = phrases
.map((phrase) =>
phrase.split(/\s+/)
.map((word) => escapeRegExp(word))
.map(phrase =>
phrase
.split(/\s+/)
.map(word => escapeRegExp(word))
.join('[\\s"?!.]+')
).join('|');
)
.join('|');
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
try {
return new RegExp(pattern, 'iu');
}
catch (_err) {
} catch (_err) {
// IE does not support unicode support, so we'll create one without.
return new RegExp(pattern, 'i');
}
@@ -39,10 +39,9 @@ const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
const tokens = body.split(regexp);
return tokens.map((token, i) =>
i % 3 === 2
? <mark key={`${keyPrefix}_${i}`}>{token}</mark>
: token
return tokens.map(
(token, i) =>
i % 3 === 2 ? <mark key={`${keyPrefix}_${i}`}>{token}</mark> : token
);
}
@@ -53,34 +52,26 @@ function markLinks(body) {
const content = [];
let index = 0;
if (matches) {
matches
.forEach((match, i) => {
content.push(body.substring(index, match.index));
content.push(<mark key={i}>{match.text}</mark>);
index = match.lastIndex;
});
matches.forEach((match, i) => {
content.push(body.substring(index, match.index));
content.push(<mark key={i}>{match.text}</mark>);
index = match.lastIndex;
});
}
content.push(body.substring(index));
return content;
}
export default ({suspectWords, bannedWords, body, ...rest}) => {
export default ({ suspectWords, bannedWords, body, ...rest }) => {
// First highlight links.
const content = markLinks(body)
.map((element, index) => {
const content = markLinks(body).map((element, index) => {
// Keep highlighted links.
if (typeof element !== 'string') {
return element;
}
// Keep highlighted links.
if (typeof element !== 'string') {
return element;
}
// Highlight suspect and banned phrase inside this part of text.
return markPhrases(element, suspectWords, bannedWords, index);
});
return (
<div {...rest}>
{content}
</div>
);
// Highlight suspect and banned phrase inside this part of text.
return markPhrases(element, suspectWords, bannedWords, index);
});
return <div {...rest}>{content}</div>;
};
@@ -1,4 +1,4 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styles from './CommentDetails.css';
import t from 'coral-framework/services/i18n';
@@ -7,26 +7,26 @@ import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
class CommentDetails extends Component {
state = {
showDetail: false
showDetail: false,
};
constructor () {
constructor() {
super();
this.state = {
showDetail: false
showDetail: false,
};
}
toggleDetail = () => {
this.setState((state) => ({
showDetail: !state.showDetail
this.setState(state => ({
showDetail: !state.showDetail,
}));
this.props.clearHeightCache && this.props.clearHeightCache();
}
};
render() {
const {data, root, comment, clearHeightCache} = this.props;
const {showDetail} = this.state;
const { data, root, comment, clearHeightCache } = this.props;
const { showDetail } = this.state;
const queryData = {
root,
comment,
@@ -49,12 +49,14 @@ class CommentDetails extends Component {
queryData={queryData}
more={showDetail}
/>
{showDetail && <Slot
fill="adminCommentMoreDetails"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>}
{showDetail && (
<Slot
fill="adminCommentMoreDetails"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>
)}
</div>
);
}
@@ -9,37 +9,67 @@ import styles from './CommentLabels.css';
const staffRoles = ['ADMIN', 'STAFF', 'MODERATOR'];
function isUserFlagged(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.user);
return actions.some(
action => action.__typename === 'FlagAction' && action.user
);
}
function getUserFlaggedType(actions) {
return actions
.some((action) =>
return actions.some(
action =>
action.__typename === 'FlagAction' &&
action.user &&
staffRoles.includes(action.user.role)
) ? 'Staff' : 'User';
)
? 'Staff'
: 'User';
}
function hasSuspectedWords(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'SUSPECT_WORD');
return actions.some(
action =>
action.__typename === 'FlagAction' && action.reason === 'SUSPECT_WORD'
);
}
function hasHistoryFlag(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'TRUST');
return actions.some(
action => action.__typename === 'FlagAction' && action.reason === 'TRUST'
);
}
const CommentLabels = ({comment, comment: {className, status, actions, hasParent}}) => {
const CommentLabels = ({
comment,
comment: { className, status, actions, hasParent },
}) => {
return (
<div className={cn(className, styles.root)}>
<div className={styles.coreLabels}>
{hasParent && <Label iconName="reply" className={styles.replyLabel}>reply</Label>}
{status === 'PREMOD' && <Label iconName="query_builder" className={styles.premodLabel}>Pre-Mod</Label>}
{isUserFlagged(actions) && <FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>}
{hasSuspectedWords(actions) && <FlagLabel iconName="sms_failed">Suspect</FlagLabel>}
{hasHistoryFlag(actions) && <FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>}
{hasParent && (
<Label iconName="reply" className={styles.replyLabel}>
reply
</Label>
)}
{status === 'PREMOD' && (
<Label iconName="query_builder" className={styles.premodLabel}>
Pre-Mod
</Label>
)}
{isUserFlagged(actions) && (
<FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>
)}
{hasSuspectedWords(actions) && (
<FlagLabel iconName="sms_failed">Suspect</FlagLabel>
)}
{hasHistoryFlag(actions) && (
<FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>
)}
</div>
<Slot className={styles.slot} fill="adminCommentLabels" queryData={{comment}} />
<Slot
className={styles.slot}
fill="adminCommentLabels"
queryData={{ comment }}
/>
</div>
);
};
@@ -4,7 +4,7 @@ import styles from './CountBadge.css';
import t from 'coral-framework/services/i18n';
const CountBadge = ({count}) => {
const CountBadge = ({ count }) => {
let number = count;
// shorten large counts to abbreviations
@@ -16,13 +16,11 @@ const CountBadge = ({count}) => {
number = `${(number / 1e3).toFixed(1)}${t('modqueue.thousand')}`;
}
return (
<span className={styles.count}>{number}</span>
);
return <span className={styles.count}>{number}</span>;
};
CountBadge.propTypes = {
count: PropTypes.number.isRequired
count: PropTypes.number.isRequired,
};
export default CountBadge;
+30 -30
View File
@@ -1,61 +1,61 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Navigation, Drawer} from 'react-mdl';
import {IndexLink, Link} from 'react-router';
import { Navigation, Drawer } from 'react-mdl';
import { IndexLink, Link } from 'react-router';
import styles from './Drawer.css';
import t from 'coral-framework/services/i18n';
import {can} from 'coral-framework/services/perms';
import { can } from 'coral-framework/services/perms';
import cn from 'classnames';
const CoralDrawer = ({handleLogout, auth = {}}) => (
const CoralDrawer = ({ handleLogout, auth = {} }) => (
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
{ auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
<div>
<Navigation className={styles.nav}>
{
can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}>
{t('configure.moderate')}
</IndexLink>
)
}
{can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}
>
{t('configure.moderate')}
</IndexLink>
)}
<Link
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}>
activeClassName={styles.active}
>
{t('configure.stories')}
</Link>
<Link
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}>
activeClassName={styles.active}
>
{t('configure.community')}
</Link>
{
can(auth.user, 'UPDATE_CONFIG') &&
(
<Link
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}>
{t('configure.configure')}
</Link>
)
}
{can(auth.user, 'UPDATE_CONFIG') && (
<Link
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}
>
{t('configure.configure')}
</Link>
)}
<a onClick={handleLogout}>Sign Out</a>
<span>{`v${process.env.VERSION}`}</span>
</Navigation>
</div> : null }
</div>
) : null}
</Drawer>
);
CoralDrawer.propTypes = {
handleLogout: PropTypes.func.isRequired,
restricted: PropTypes.bool, // hide app elements from a logged out user
auth: PropTypes.object
auth: PropTypes.object,
};
export default CoralDrawer;
@@ -1,15 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Card} from 'coral-ui';
import { Card } from 'coral-ui';
const EmptyCard = (props) => (
<Card style={{textAlign: 'center', maxWidth: 400, margin: '0 auto'}}>
const EmptyCard = props => (
<Card style={{ textAlign: 'center', maxWidth: 400, margin: '0 auto' }}>
{props.children}
</Card>
);
EmptyCard.propTypes = {
children: PropTypes.node.isRequired
children: PropTypes.node.isRequired,
};
export default EmptyCard;
@@ -1,11 +1,11 @@
import React from 'react';
import {Layout} from 'react-mdl';
import { Layout } from 'react-mdl';
import styles from './FullLoading.css';
import {CoralLogo} from 'coral-ui';
import { CoralLogo } from 'coral-ui';
export const FullLoading = () => (
<Layout fixedDrawer>
<div className={styles.layout} >
<div className={styles.layout}>
<h1>Loading</h1>
<CoralLogo />
</div>
@@ -1,7 +1,7 @@
import React from 'react';
import {matchLinks} from '../utils';
import { matchLinks } from '../utils';
export default ({text, children}) => {
export default ({ text, children }) => {
const hasLinks = !!matchLinks(text);
if (!hasLinks) {
@@ -1,24 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button} from 'coral-ui';
import { Button } from 'coral-ui';
import styles from './LoadMore.css';
import cn from 'classnames';
const LoadMore = ({loadMore, showLoadMore, className = '', ...rest}) =>
const LoadMore = ({ loadMore, showLoadMore, className = '', ...rest }) => (
<div {...rest} className={cn(className, styles.loadMoreContainer)}>
{
showLoadMore && <Button
className={styles.loadMore}
onClick={loadMore}>
{showLoadMore && (
<Button className={styles.loadMore} onClick={loadMore}>
Load More
</Button>
}
</div>;
)}
</div>
);
LoadMore.propTypes = {
className: PropTypes.string,
loadMore: PropTypes.func.isRequired,
showLoadMore: PropTypes.bool.isRequired
showLoadMore: PropTypes.bool.isRequired,
};
export default LoadMore;
+5 -3
View File
@@ -1,11 +1,13 @@
import React from 'react';
import {Button, Icon} from 'react-mdl';
import { Button, Icon } from 'react-mdl';
import styles from './Modal.css';
export default ({open, children, onClose}) => (
export default ({ open, children, onClose }) => (
<div className={`${styles.container} ${!open ? styles.hide : ''}`}>
<div className={styles.inner}>
<Button className={styles.close} onClick={onClose}><Icon name='close' /></Button>
<Button className={styles.close} onClick={onClose}>
<Icon name="close" />
</Button>
{children}
</div>
</div>
@@ -5,52 +5,75 @@ import styles from './ModerationKeysModal.css';
import t from 'coral-framework/services/i18n';
export default class ModerationKeysModal extends React.Component {
static propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
hideShortcutsNote: PropTypes.func.isRequired,
shortcutsNoteVisible: PropTypes.string.isRequired,
queueCount: PropTypes.number.isRequired
}
queueCount: PropTypes.number.isRequired,
};
buildShortcuts = () => {
return [
{
title: 'modqueue.navigation',
shortcuts: {
'j': 'modqueue.next_comment',
'k': 'modqueue.prev_comment',
j: 'modqueue.next_comment',
k: 'modqueue.prev_comment',
'ctrl+f': 'modqueue.toggle_search',
't': 'modqueue.next_queue',
t: 'modqueue.next_queue',
[`1...${this.props.queueCount}`]: 'modqueue.jump_to_queue',
's': 'modqueue.singleview',
'?': 'modqueue.thismenu'
}
s: 'modqueue.singleview',
'?': 'modqueue.thismenu',
},
},
{
title: 'modqueue.actions',
shortcuts: {
'd': 'modqueue.approve',
'f': 'modqueue.reject'
}
}
d: 'modqueue.approve',
f: 'modqueue.reject',
},
},
];
}
};
render () {
const {open, onClose, hideShortcutsNote, shortcutsNoteVisible} = this.props;
render() {
const {
open,
onClose,
hideShortcutsNote,
shortcutsNoteVisible,
} = this.props;
return (
<div>
<div className={styles.callToAction} style={{display: shortcutsNoteVisible === 'show' ? 'block' : 'none'}}>
<div onClick={hideShortcutsNote} className={styles.closeButton}>×</div>
<div
className={styles.callToAction}
style={{
display: shortcutsNoteVisible === 'show' ? 'block' : 'none',
}}
>
<div onClick={hideShortcutsNote} className={styles.closeButton}>
×
</div>
<p className={styles.ctaHeader}>{t('modqueue.mod_faster')}</p>
<p><strong>{t('modqueue.try_these')}:</strong></p>
<p>
<strong>{t('modqueue.try_these')}:</strong>
</p>
<ul>
<li><span>{t('modqueue.approve')}</span> <span className={styles.smallKey}>d</span></li>
<li><span>{t('modqueue.reject')}</span> <span className={styles.smallKey}>f</span></li>
<li>
<span>{t('modqueue.approve')}</span>{' '}
<span className={styles.smallKey}>d</span>
</li>
<li>
<span>{t('modqueue.reject')}</span>{' '}
<span className={styles.smallKey}>f</span>
</li>
</ul>
<p><span>{t('modqueue.view_more_shortcuts')}</span> <span className={styles.smallKey}>{t('modqueue.shift_key')}</span> + <span className={styles.smallKey}>/</span></p>
<p>
<span>{t('modqueue.view_more_shortcuts')}</span>{' '}
<span className={styles.smallKey}>{t('modqueue.shift_key')}</span> +{' '}
<span className={styles.smallKey}>/</span>
</p>
</div>
<Modal open={open} onClose={onClose}>
<h3>{t('modqueue.shortcuts')}</h3>
@@ -63,9 +86,11 @@ export default class ModerationKeysModal extends React.Component {
</tr>
</thead>
<tbody>
{Object.keys(shortcut.shortcuts).map((key) => (
{Object.keys(shortcut.shortcuts).map(key => (
<tr key={`${key}tr`}>
<td className={styles.shortcut}><span className={styles.key}>{key}</span></td>
<td className={styles.shortcut}>
<span className={styles.key}>{key}</span>
</td>
<td>{t(shortcut.shortcuts[key])}</td>
</tr>
))}
@@ -1,13 +1,16 @@
import React from 'react';
import {Layout} from 'react-mdl';
import { Layout } from 'react-mdl';
import styles from './NotFound.css';
export const NotFound = () => (
<Layout fixedDrawer>
<div className={styles.layout} >
<div className={styles.layout}>
<h1>Page Not Found</h1>
<p>The communicorn feels your pain.</p>
<img src="https://coralproject.net/images/communicorn.jpg" alt="Communicorn"/>
<img
src="https://coralproject.net/images/communicorn.jpg"
alt="Communicorn"
/>
</div>
</Layout>
);
@@ -3,15 +3,19 @@ import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './RejectButton.css';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
const RejectButton = ({active, minimal, onClick, className}) => {
const RejectButton = ({ active, minimal, onClick, className }) => {
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
className={cn(
styles.root,
{ [styles.minimal]: minimal, [styles.active]: active },
className
)}
onClick={onClick}
>
<Icon name={'close'} className={styles.icon} />
@@ -28,4 +32,3 @@ RejectButton.propTypes = {
};
export default RejectButton;
@@ -1,25 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import {RadioGroup, Radio} from 'react-mdl';
import { Dialog } from 'coral-ui';
import { RadioGroup, Radio } from 'react-mdl';
import styles from './SuspendUserDialog.css';
import cn from 'classnames';
import Button from 'coral-ui/components/Button';
import t, {timeago} from 'coral-framework/services/i18n';
import {dateAdd} from 'coral-framework/utils';
import t, { timeago } from 'coral-framework/services/i18n';
import { dateAdd } from 'coral-framework/utils';
const initialState = {step: 0, duration: '3'};
const initialState = { step: 0, duration: '3' };
function durationsToDate(hours) {
// Add 1 minute more to help `timeago.js` to display the correct duration.
return dateAdd(new Date(), 'minute', hours * 60 + 1);
}
class SuspendUserDialog extends React.Component {
state = initialState;
componentWillReceiveProps(next) {
@@ -28,13 +26,13 @@ class SuspendUserDialog extends React.Component {
}
}
handleDurationChange = (event) => {
this.setState({duration: event.target.value});
}
handleDurationChange = event => {
this.setState({ duration: event.target.value });
};
handleMessageChange = (event) => {
this.setState({message: event.target.value});
}
handleMessageChange = event => {
this.setState({ message: event.target.value });
};
goToStep1 = () => {
this.setState({
@@ -43,13 +41,12 @@ class SuspendUserDialog extends React.Component {
'suspenduser.email_message_suspend',
this.props.username,
this.props.organizationName,
timeago(durationsToDate(this.state.duration)),
timeago(durationsToDate(this.state.duration))
),
});
}
};
handlePerform = () => {
this.props.onPerform({
message: this.state.message,
@@ -59,36 +56,49 @@ class SuspendUserDialog extends React.Component {
};
renderStep0() {
const {onCancel, username} = this.props;
const {duration} = this.state;
const { onCancel, username } = this.props;
const { duration } = this.state;
return (
<section className="talk-admin-suspend-user-dialog-step-0">
<h1 className={styles.header}>
{t('suspenduser.title_suspend')}
</h1>
<h1 className={styles.header}>{t('suspenduser.title_suspend')}</h1>
<p className={styles.description}>
{t('suspenduser.description_suspend', username)}
</p>
<fieldset>
<legend className={styles.legend}>{t('suspenduser.select_duration')}</legend>
<legend className={styles.legend}>
{t('suspenduser.select_duration')}
</legend>
<RadioGroup
name='status filter'
name="status filter"
value={duration}
childContainer='div'
childContainer="div"
onChange={this.handleDurationChange}
className={styles.radioGroup}
>
<Radio value='1'>{t('suspenduser.one_hour')}</Radio>
<Radio value='3'>{t('suspenduser.hours', 3)}</Radio>
<Radio value='24'>{t('suspenduser.hours', 24)}</Radio>
<Radio value='168'>{t('suspenduser.days', 7)}</Radio>
<Radio value="1">{t('suspenduser.one_hour')}</Radio>
<Radio value="3">{t('suspenduser.hours', 3)}</Radio>
<Radio value="24">{t('suspenduser.hours', 24)}</Radio>
<Radio value="168">{t('suspenduser.days', 7)}</Radio>
</RadioGroup>
</fieldset>
<div className={styles.buttons}>
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
<Button
cStyle="white"
className={styles.cancel}
onClick={onCancel}
raised
>
{t('suspenduser.cancel')}
</Button>
<Button cStyle="black" className={cn(styles.perform, 'talk-admin-suspend-user-dialog-confirm')} onClick={this.goToStep1} raised>
<Button
cStyle="black"
className={cn(
styles.perform,
'talk-admin-suspend-user-dialog-confirm'
)}
onClick={this.goToStep1}
raised
>
{t('suspenduser.suspend_user')}
</Button>
</div>
@@ -97,31 +107,40 @@ class SuspendUserDialog extends React.Component {
}
renderStep1() {
const {message} = this.state;
const {onCancel, username} = this.props;
const { message } = this.state;
const { onCancel, username } = this.props;
return (
<section className="talk-admin-suspend-user-dialog-step-1">
<h1 className={styles.header}>
{t('suspenduser.title_notify')}
</h1>
<h1 className={styles.header}>{t('suspenduser.title_notify')}</h1>
<p className={styles.description}>
{t('suspenduser.description_notify', username)}
</p>
<fieldset>
<legend className={styles.legend}>{t('suspenduser.write_message')}</legend>
<legend className={styles.legend}>
{t('suspenduser.write_message')}
</legend>
<textarea
rows={5}
className={styles.messageInput}
value={message}
onChange={this.handleMessageChange} />
onChange={this.handleMessageChange}
/>
</fieldset>
<div className={styles.buttons}>
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
<Button
cStyle="white"
className={styles.cancel}
onClick={onCancel}
raised
>
{t('suspenduser.cancel')}
</Button>
<Button
cStyle="black"
className={cn(styles.perform, 'talk-admin-suspend-user-dialog-send')}
className={cn(
styles.perform,
'talk-admin-suspend-user-dialog-send'
)}
onClick={this.handlePerform}
disabled={this.state.message.length === 0}
raised
@@ -134,8 +153,8 @@ class SuspendUserDialog extends React.Component {
}
render() {
const {open, onCancel} = this.props;
const {step} = this.state;
const { open, onCancel } = this.props;
const { step } = this.state;
return (
<Dialog
className={cn(styles.dialog, 'talk-admin-suspend-user-dialog')}
@@ -143,7 +162,13 @@ class SuspendUserDialog extends React.Component {
open={open}
>
<div className={styles.close}>
<button aria-label="Close" onClick={onCancel} className={styles.closeButton}>×</button>
<button
aria-label="Close"
onClick={onCancel}
className={styles.closeButton}
>
×
</button>
</div>
{step === 0 && this.renderStep0()}
{step === 1 && this.renderStep1()}
@@ -5,8 +5,9 @@ import AutosizeInput from 'react-input-autosize';
import PropTypes from 'prop-types';
import cn from 'classnames';
const autosizingRenderInput = ({onChange, value, addTag: _, ...other}) =>
<AutosizeInput type='text' onChange={onChange} value={value} {...other} />;
const autosizingRenderInput = ({ onChange, value, addTag: _, ...other }) => (
<AutosizeInput type="text" onChange={onChange} value={value} {...other} />
);
autosizingRenderInput.propTypes = {
onChange: PropTypes.func,
@@ -14,17 +15,16 @@ autosizingRenderInput.propTypes = {
addTag: PropTypes.func,
};
const TagsInputComponent = ({className = '', ...props}) => {
const TagsInputComponent = ({ className = '', ...props }) => {
return (
<TagsInput
addOnBlur={true}
addOnPaste={true}
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
pasteSplit={data => data.split(',').map(d => d.trim())}
className={cn(styles.root, 'tags-input', className)}
focusedClassName={styles.rootFocus}
renderInput={autosizingRenderInput}
{...props}
tagProps={{
className: styles.tag,
classNameRemove: styles.tagRemove,
@@ -1,6 +1,6 @@
import './ToastContainer.css';
import {defaultProps} from 'recompose';
import {ToastContainer} from 'react-toastify';
import { defaultProps } from 'recompose';
import { ToastContainer } from 'react-toastify';
export default defaultProps({
autoClose: 5000,
+140 -76
View File
@@ -2,78 +2,87 @@ import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import capitalize from 'lodash/capitalize';
import {getErrorMessages} from 'coral-framework/utils';
import { getErrorMessages } from 'coral-framework/utils';
import styles from './UserDetail.css';
import AccountHistory from './AccountHistory';
import {Slot} from 'coral-framework/components';
import { Slot } from 'coral-framework/components';
import UserDetailCommentList from '../components/UserDetailCommentList';
import {getReliability, isSuspended, isBanned} from 'coral-framework/utils/user';
import {
getReliability,
isSuspended,
isBanned,
} from 'coral-framework/utils/user';
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
import ClickOutside from 'coral-framework/components/ClickOutside';
import {Icon, Drawer, Spinner, TabBar, Tab, TabContent, TabPane} from 'coral-ui';
import {
Icon,
Drawer,
Spinner,
TabBar,
Tab,
TabContent,
TabPane,
} from 'coral-ui';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import UserInfoTooltip from './UserInfoTooltip';
class UserDetail extends React.Component {
rejectThenReload = async (info) => {
rejectThenReload = async info => {
try {
await this.props.rejectComment(info);
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
acceptThenReload = async (info) => {
acceptThenReload = async info => {
try {
await this.props.acceptComment(info);
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
bulkAcceptThenReload = async () => {
try {
await this.props.bulkAccept();
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
bulkRejectThenReload = async () => {
try {
await this.props.bulkReject();
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
changeTab = (tab) => {
changeTab = tab => {
this.props.changeTab(tab);
}
};
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
showSuspenUserDialog = () =>
this.props.showSuspendUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
showBanUserDialog = () => this.props.showBanUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
showBanUserDialog = () =>
this.props.showBanUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
renderLoading() {
return (
@@ -86,7 +95,7 @@ class UserDetail extends React.Component {
}
getActionMenuLabel() {
const {root: {user}} = this.props;
const { root: { user } } = this.props;
if (isBanned(user)) {
return 'Banned';
@@ -101,12 +110,7 @@ class UserDetail extends React.Component {
const {
data,
root,
root: {
me,
user,
totalComments,
rejectedComments,
},
root: { me, user, totalComments, rejectedComments },
activeTab,
selectedCommentIds,
toggleSelect,
@@ -120,7 +124,7 @@ class UserDetail extends React.Component {
} = this.props;
// if totalComments is 0, you're dividing by zero
let rejectedPercent = (rejectedComments / totalComments) * 100;
let rejectedPercent = rejectedComments / totalComments * 100;
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
rejectedPercent = 0;
@@ -131,43 +135,67 @@ class UserDetail extends React.Component {
return (
<ClickOutside onClickOutside={modal ? null : hideUserDetail}>
<Drawer className="talk-admin-user-detail-drawer" onClose={hideUserDetail}>
<h3 className={cn(styles.username, 'talk-admin-user-detail-username')}>
<Drawer
className="talk-admin-user-detail-drawer"
onClose={hideUserDetail}
>
<h3
className={cn(styles.username, 'talk-admin-user-detail-username')}
>
{user.username}
</h3>
{user.id &&
{user.id && (
<ActionsMenu
icon="person"
className={cn(styles.actionsMenu, 'talk-admin-user-detail-actions-menu')}
buttonClassNames={cn({
[styles.actionsMenuSuspended]: suspended,
[styles.actionsMenuBanned]: banned,
}, 'talk-admin-user-detail-actions-button')}
label={this.getActionMenuLabel()}>
{suspended ? <ActionsMenuItem
onClick={() => unsuspendUser({id: user.id})}>
Remove Suspension
</ActionsMenuItem> : <ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showSuspenUserDialog}>
Suspend User
</ActionsMenuItem>}
{banned ? <ActionsMenuItem
onClick={() => unbanUser({id: user.id})}>
Remove Ban
</ActionsMenuItem> : <ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showBanUserDialog}>
Ban User
</ActionsMenuItem>}
className={cn(
styles.actionsMenu,
'talk-admin-user-detail-actions-menu'
)}
buttonClassNames={cn(
{
[styles.actionsMenuSuspended]: suspended,
[styles.actionsMenuBanned]: banned,
},
'talk-admin-user-detail-actions-button'
)}
label={this.getActionMenuLabel()}
>
{suspended ? (
<ActionsMenuItem onClick={() => unsuspendUser({ id: user.id })}>
Remove Suspension
</ActionsMenuItem>
) : (
<ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showSuspenUserDialog}
>
Suspend User
</ActionsMenuItem>
)}
{banned ? (
<ActionsMenuItem onClick={() => unbanUser({ id: user.id })}>
Remove Ban
</ActionsMenuItem>
) : (
<ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showBanUserDialog}
>
Ban User
</ActionsMenuItem>
)}
</ActionsMenu>
}
)}
{(banned || suspended) && <UserInfoTooltip user={user} banned={banned} suspended={suspended} />}
{(banned || suspended) && (
<UserInfoTooltip
user={user}
banned={banned}
suspended={suspended}
/>
)}
<div>
<ul className={styles.userDetailList}>
@@ -177,13 +205,18 @@ class UserDetail extends React.Component {
{new Date(user.created_at).toLocaleString()}
</li>
{user.profiles.map(({id}) =>
{user.profiles.map(({ id }) => (
<li key={id}>
<Icon name="email" />
<span className={styles.userDetailItem}>Email:</span>
{id} <ButtonCopyToClipboard className={styles.copyButton} icon="content_copy" copyText={id} />
{id}{' '}
<ButtonCopyToClipboard
className={styles.copyButton}
icon="content_copy"
copyText={id}
/>
</li>
)}
))}
</ul>
<ul className={styles.stats}>
@@ -199,7 +232,12 @@ class UserDetail extends React.Component {
</li>
<li className={styles.stat}>
<span className={styles.statItem}>Reports</span>
<span className={cn(styles.statReportResult, styles[getReliability(user.reliable.flagger)])}>
<span
className={cn(
styles.statReportResult,
styles[getReliability(user.reliable.flagger)]
)}
>
{capitalize(getReliability(user.reliable.flagger))}
</span>
</li>
@@ -209,7 +247,7 @@ class UserDetail extends React.Component {
<Slot
fill="userProfile"
data={this.props.data}
queryData={{root, user}}
queryData={{ root, user }}
/>
<hr />
@@ -218,28 +256,48 @@ class UserDetail extends React.Component {
onTabClick={this.changeTab}
activeTab={activeTab}
className={cn(styles.tabBar, 'talk-admin-user-detail-tab-bar')}
aria-controls='talk-admin-user-detail-content'
aria-controls="talk-admin-user-detail-content"
tabClassNames={{
button: styles.tabButton,
buttonActive: styles.tabButtonActive,
}} >
}}
>
<Tab
tabId={'all'}
className={cn(styles.tab, styles.button, 'talk-admin-user-detail-all-tab')} >
className={cn(
styles.tab,
styles.button,
'talk-admin-user-detail-all-tab'
)}
>
All
</Tab>
<Tab
tabId={'rejected'}
className={cn(styles.tab, 'talk-admin-user-detail-rejected-tab')} >
className={cn(styles.tab, 'talk-admin-user-detail-rejected-tab')}
>
Rejected
</Tab>
<Tab tabId={'history'} className={cn(styles.tab, styles.button, 'talk-admin-user-detail-history-tab')}>
<Tab
tabId={'history'}
className={cn(
styles.tab,
styles.button,
'talk-admin-user-detail-history-tab'
)}
>
Account History
</Tab>
</TabBar>
<TabContent activeTab={activeTab} className='talk-admin-user-detail-content'>
<TabPane tabId={'all'} className={'talk-admin-user-detail-all-tab-pane'}>
<TabContent
activeTab={activeTab}
className="talk-admin-user-detail-content"
>
<TabPane
tabId={'all'}
className={'talk-admin-user-detail-all-tab-pane'}
>
<UserDetailCommentList
user={user}
root={root}
@@ -255,7 +313,10 @@ class UserDetail extends React.Component {
bulkRejectThenReload={this.bulkRejectThenReload}
/>
</TabPane>
<TabPane tabId={'rejected'} className={'talk-admin-user-detail-rejected-tab-pane'}>
<TabPane
tabId={'rejected'}
className={'talk-admin-user-detail-rejected-tab-pane'}
>
<UserDetailCommentList
user={user}
root={root}
@@ -271,7 +332,10 @@ class UserDetail extends React.Component {
bulkRejectThenReload={this.bulkRejectThenReload}
/>
</TabPane>
<TabPane tabId={'history'} className={'talk-admin-user-detail-history-tab-pane'}>
<TabPane
tabId={'history'}
className={'talk-admin-user-detail-history-tab-pane'}
>
<AccountHistory user={user} />
</TabPane>
</TabContent>
@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Link} from 'react-router';
import { Link } from 'react-router';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import CommentDetails from './CommentDetails';
import styles from './UserDetailComment.css';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
@@ -13,19 +13,18 @@ import CommentLabels from '../containers/CommentLabels';
import ApproveButton from './ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import t, {timeago} from 'coral-framework/services/i18n';
import t, { timeago } from 'coral-framework/services/i18n';
class UserDetailComment extends React.Component {
approve = () =>
this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({ commentId: this.props.comment.id });
approve = () => (this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({commentId: this.props.comment.id})
);
reject = () => (this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({commentId: this.props.comment.id})
);
reject = () =>
this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({ commentId: this.props.comment.id });
render() {
const {
@@ -34,30 +33,35 @@ class UserDetailComment extends React.Component {
toggleSelect,
className,
data,
root: {settings: {wordlist: {banned, suspect}}},
root: { settings: { wordlist: { banned, suspect } } },
} = this.props;
return (
<li
tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
className={cn(className, styles.root, {
[styles.rootSelected]: selected,
})}
>
<div className={styles.container}>
<div className={styles.header}>
<input
className={styles.bulkSelectInput}
type='checkbox'
type="checkbox"
value={comment.id}
checked={selected}
onChange={(e) => toggleSelect(e.target.value, e.target.checked)} />
onChange={e => toggleSelect(e.target.value, e.target.checked)}
/>
<span className={styles.created}>
{timeago(comment.created_at)}
</span>
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
{comment.editing && comment.editing.edited ? (
<span>
&nbsp;<span className={styles.editedMarker}>
({t('comment.edited')})
</span>
</span>
) : null}
<div className={styles.labels}>
<CommentLabels comment={comment} />
@@ -65,7 +69,11 @@ class UserDetailComment extends React.Component {
</div>
<div className={styles.story}>
Story: {comment.asset.title}
{<Link to={`/admin/moderate/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
{
<Link to={`/admin/moderate/${comment.asset.id}`}>
{t('modqueue.moderate')}
</Link>
}
</div>
<CommentAnimatedEdit body={comment.body}>
<div className={styles.bodyContainer}>
@@ -74,8 +82,7 @@ class UserDetailComment extends React.Component {
suspectWords={suspect}
bannedWords={banned}
body={comment.body}
/>
{' '}
/>{' '}
<a
className={styles.external}
href={`${comment.asset.url}?commentId=${comment.id}`}
@@ -106,11 +113,7 @@ class UserDetailComment extends React.Component {
</div>
</CommentAnimatedEdit>
</div>
<CommentDetails
data={data}
root={root}
comment={comment}
/>
<CommentDetails data={data} root={root} comment={comment} />
</li>
);
}
@@ -142,7 +145,7 @@ UserDetailComment.propTypes = {
asset: PropTypes.shape({
title: PropTypes.string,
url: PropTypes.string,
id: PropTypes.string
id: PropTypes.string,
}),
}),
};
@@ -7,17 +7,11 @@ import Comment from '../containers/UserDetailComment';
import RejectButton from './RejectButton';
import ApproveButton from './ApproveButton';
const UserDetailCommentList = (props) => {
const UserDetailCommentList = props => {
const {
data,
root,
root: {
user,
comments: {
nodes,
hasNextPage
}
},
root: { user, comments: { nodes, hasNextPage } },
acceptComment,
rejectComment,
selectedCommentIds,
@@ -30,39 +24,49 @@ const UserDetailCommentList = (props) => {
} = props;
return (
<div className={cn(styles.commentList, 'talk-admin-user-detail-comment-list')}>
<div className={(selectedCommentIds.length > 0) ? cn(styles.bulkActionHeader, styles.selected) : styles.bulkActionHeader}>
<div
className={cn(styles.commentList, 'talk-admin-user-detail-comment-list')}
>
<div
className={
selectedCommentIds.length > 0
? cn(styles.bulkActionHeader, styles.selected)
: styles.bulkActionHeader
}
>
{selectedCommentIds.length > 0 && (
<div className={styles.bulkActionGroup}>
<ApproveButton
onClick={bulkAcceptThenReload}
minimal
/>
<RejectButton
onClick={bulkRejectThenReload}
minimal
/>
<span className={styles.selectedCommentsInfo}> {selectedCommentIds.length} comments selected</span>
<ApproveButton onClick={bulkAcceptThenReload} minimal />
<RejectButton onClick={bulkRejectThenReload} minimal />
<span className={styles.selectedCommentsInfo}>
{' '}
{selectedCommentIds.length} comments selected
</span>
</div>
)}
<div className={styles.toggleAll}>
<input
type='checkbox'
id='toogleAll'
checked={selectedCommentIds.length > 0 && selectedCommentIds.length === nodes.length}
onChange={(e) => {
toggleSelectAll(nodes.map((comment) => comment.id), e.target.checked);
}} />
<label htmlFor='toogleAll'>Select all</label>
type="checkbox"
id="toogleAll"
checked={
selectedCommentIds.length > 0 &&
selectedCommentIds.length === nodes.length
}
onChange={e => {
toggleSelectAll(
nodes.map(comment => comment.id),
e.target.checked
);
}}
/>
<label htmlFor="toogleAll">Select all</label>
</div>
</div>
{
nodes.map((comment) => {
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return <Comment
{nodes.map(comment => {
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return (
<Comment
key={comment.id}
user={user}
root={root}
@@ -73,9 +77,9 @@ const UserDetailCommentList = (props) => {
selected={selected}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
/>;
})
}
/>
);
})}
<LoadMore
className={styles.loadMore}
loadMore={loadMore}
@@ -1,83 +1,165 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import styles from './UserInfoTooltip.css';
import ClickOutside from 'coral-framework/components/ClickOutside';
import moment from 'moment';
const initialState = {menuVisible: false};
const initialState = { menuVisible: false };
class UserInfoTooltip extends React.Component {
state = initialState;
toogleMenu = () => {
this.setState({menuVisible: !this.state.menuVisible});
}
this.setState({ menuVisible: !this.state.menuVisible });
};
hideMenu = () => {
this.setState({menuVisible: false});
}
this.setState({ menuVisible: false });
};
getLastHistoryItem = (user, status = 'banned') => {
const userHistory = user.state.status[status].history;
return userHistory[userHistory.length - 1];
}
};
render() {
const {menuVisible} = this.state;
const {user, banned, suspended} = this.props;
const { menuVisible } = this.state;
const { user, banned, suspended } = this.props;
return (
<ClickOutside onClickOutside={this.hideMenu}>
<div className={cn(styles.userInfo, 'talk-admin-user-info-tooltip')}>
<span onClick={this.toogleMenu} className={cn(styles.icon, 'talk-admin-user-info-tooltip-icon')}>
<span
onClick={this.toogleMenu}
className={cn(styles.icon, 'talk-admin-user-info-tooltip-icon')}
>
<Icon name="info_outline" />
</span>
{menuVisible && (
<div className={cn(styles.menu, 'talk-admin-user-info-tooltip-menu')}>
{
banned && (
<div className={cn(styles.description, 'talk-admin-user-info-tooltip-description-banned')}>
<ul className={cn(styles.descriptionList, 'talk-admin-user-info-tooltip-description-list')}>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>Banned On</strong>
<span>{moment(new Date(this.getLastHistoryItem(user, 'banned').created_at)).format('MMMM Do YYYY, h:mm:ss a')}</span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>By</strong>
<span>{this.getLastHistoryItem(user, 'banned').assigned_by.username}</span>
</li>
</ul>
</div>
)
}
<div
className={cn(styles.menu, 'talk-admin-user-info-tooltip-menu')}
>
{banned && (
<div
className={cn(
styles.description,
'talk-admin-user-info-tooltip-description-banned'
)}
>
<ul
className={cn(
styles.descriptionList,
'talk-admin-user-info-tooltip-description-list'
)}
>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>Banned On</strong>
<span>
{moment(
new Date(
this.getLastHistoryItem(user, 'banned').created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>By</strong>
<span>
{
this.getLastHistoryItem(user, 'banned').assigned_by
.username
}
</span>
</li>
</ul>
</div>
)}
{
suspended && (
<div className={cn(styles.description, 'talk-admin-user-info-tooltip-description-suspended')}>
<ul className={cn(styles.descriptionList, 'talk-admin-user-info-tooltip-description-list')}>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>Suspension</strong>
<span></span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>By</strong>
<span>{this.getLastHistoryItem(user, 'suspension').assigned_by.username}</span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>Start</strong>
<span>{moment(new Date(this.getLastHistoryItem(user, 'suspension').created_at)).format('MMMM Do YYYY, h:mm:ss a')}</span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>End</strong>
<span>{moment(new Date(this.getLastHistoryItem(user, 'suspension').until)).format('MMMM Do YYYY, h:mm:ss a')}</span>
</li>
</ul>
</div>
)
}
{suspended && (
<div
className={cn(
styles.description,
'talk-admin-user-info-tooltip-description-suspended'
)}
>
<ul
className={cn(
styles.descriptionList,
'talk-admin-user-info-tooltip-description-list'
)}
>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>Suspension</strong>
<span />
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>By</strong>
<span>
{
this.getLastHistoryItem(user, 'suspension')
.assigned_by.username
}
</span>
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>Start</strong>
<span>
{moment(
new Date(
this.getLastHistoryItem(
user,
'suspension'
).created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>End</strong>
<span>
{moment(
new Date(
this.getLastHistoryItem(user, 'suspension').until
)
).format('MMMM Do YYYY, h:mm:ss a')}
</span>
</li>
</ul>
</div>
)}
</div>
)}
</div>
+76 -62
View File
@@ -1,98 +1,112 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
import {Link, IndexLink} from 'react-router';
import { Navigation, Header, IconButton, MenuItem, Menu } from 'react-mdl';
import { Link, IndexLink } from 'react-router';
import styles from './Header.css';
import t from 'coral-framework/services/i18n';
import {Logo} from './Logo';
import {can} from 'coral-framework/services/perms';
import { Logo } from './Logo';
import { can } from 'coral-framework/services/perms';
import Indicator from './Indicator';
const CoralHeader = ({
handleLogout,
showShortcuts = () => {},
auth,
root
root,
}) => {
return (
<div className={styles.headerWrapper}>
<Header className={styles.header}>
<Logo className={styles.logo} />
<div>
{
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
<Navigation className={styles.nav}>
{
can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
id='moderateNav'
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}>
{t('configure.moderate')}
{(root.premodCount !== 0 || root.reportedCount !== 0) && <Indicator />}
</IndexLink>
)
}
<Link
id='storiesNav'
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}>
{t('configure.stories')}
</Link>
<div>
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
<Navigation className={styles.nav}>
{can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
id="moderateNav"
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}
>
{t('configure.moderate')}
{(root.premodCount !== 0 || root.reportedCount !== 0) && (
<Indicator />
)}
</IndexLink>
)}
<Link
id="storiesNav"
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}
>
{t('configure.stories')}
</Link>
<Link
id='communityNav'
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}>
{t('configure.community')}
{root.flaggedUsernamesCount !== 0 && <Indicator />}
</Link>
<Link
id="communityNav"
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}
>
{t('configure.community')}
{root.flaggedUsernamesCount !== 0 && <Indicator />}
</Link>
{
can(auth.user, 'UPDATE_CONFIG') && (
<Link
id='configureNav'
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}>
{t('configure.configure')}
</Link>
)
}
</Navigation>
:
null
}
{can(auth.user, 'UPDATE_CONFIG') && (
<Link
id="configureNav"
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}
>
{t('configure.configure')}
</Link>
)}
</Navigation>
) : null}
<div className={styles.rightPanel}>
<ul>
<li className={cn(styles.settings, 'talk-admin-header-settings')}>
<div>
<IconButton name="settings" id="menu-settings" className="talk-admin-header-settings-button"/>
<IconButton
name="settings"
id="menu-settings"
className="talk-admin-header-settings-button"
/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
<MenuItem onClick={() => showShortcuts(true)}>
{t('configure.shortcuts')}
</MenuItem>
<MenuItem>
<a href="https://github.com/coralproject/talk/releases" target="_blank" rel="noopener noreferrer">
View latest version
<a
href="https://github.com/coralproject/talk/releases"
target="_blank"
rel="noopener noreferrer"
>
View latest version
</a>
</MenuItem>
<MenuItem>
<a href="https://support.coralproject.net" target="_blank" rel="noopener noreferrer">
Report a bug or give feedback
<a
href="https://support.coralproject.net"
target="_blank"
rel="noopener noreferrer"
>
Report a bug or give feedback
</a>
</MenuItem>
<MenuItem onClick={handleLogout} className="talk-admin-header-sign-out">
<MenuItem
onClick={handleLogout}
className="talk-admin-header-sign-out"
>
{t('configure.sign_out')}
</MenuItem>
</Menu>
</div>
</li>
<li>
{`v${process.env.VERSION}`}
</li>
<li>{`v${process.env.VERSION}`}</li>
</ul>
</div>
</div>
@@ -105,7 +119,7 @@ CoralHeader.propTypes = {
auth: PropTypes.object,
showShortcuts: PropTypes.func,
handleLogout: PropTypes.func.isRequired,
root: PropTypes.object.isRequired
root: PropTypes.object.isRequired,
};
export default CoralHeader;
@@ -1,7 +1,6 @@
import React from 'react';
import styles from './Indicator.css';
const Indicator = () =>
<span className={styles.indicator}></span>;
const Indicator = () => <span className={styles.indicator} />;
export default Indicator;
+4 -10
View File
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Layout as LayoutMDL} from 'react-mdl';
import { Layout as LayoutMDL } from 'react-mdl';
import Header from '../../containers/Header';
import Drawer from '../Drawer';
import styles from './Layout.css';
@@ -18,14 +18,8 @@ const Layout = ({
showShortcuts={toggleShortcutModal}
auth={auth}
/>
<Drawer
handleLogout={handleLogout}
restricted={restricted}
auth={auth}
/>
<div className={styles.layout}>
{children}
</div>
<Drawer handleLogout={handleLogout} restricted={restricted} auth={auth} />
<div className={styles.layout}>{children}</div>
</LayoutMDL>
);
@@ -34,7 +28,7 @@ Layout.propTypes = {
auth: PropTypes.object,
handleLogout: PropTypes.func,
toggleShortcutModal: PropTypes.func,
restricted: PropTypes.bool // hide elements from a user that's logged out
restricted: PropTypes.bool, // hide elements from a user that's logged out
};
export default Layout;
+3 -3
View File
@@ -1,9 +1,9 @@
import React from 'react';
import styles from './Logo.css';
import {CoralLogo} from 'coral-ui';
import { CoralLogo } from 'coral-ui';
import PropTypes from 'prop-types';
export const Logo = ({className = ''}) => (
export const Logo = ({ className = '' }) => (
<div className={`${styles.logo} ${className}`}>
<h1>
<CoralLogo className={styles.base} />
@@ -13,5 +13,5 @@ export const Logo = ({className = ''}) => (
);
Logo.propTypes = {
className: PropTypes.string
className: PropTypes.string,
};