Merge branch 'master' into talk-product-guide-features

This commit is contained in:
Kim Gardner
2017-10-14 17:46:10 +01:00
committed by GitHub
18 changed files with 394 additions and 209 deletions
+18 -12
View File
@@ -23,6 +23,14 @@ export default class UserDetail extends React.Component {
toggleSelect: PropTypes.func.isRequired,
bulkAccept: PropTypes.func.isRequired,
bulkReject: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
data: PropTypes.shape({
refetch: PropTypes.func.isRequired,
}),
activeTab: PropTypes.string.isRequired,
selectedCommentIds: PropTypes.array.isRequired,
viewUserDetail: PropTypes.any.isRequired,
loadMore: PropTypes.any.isRequired
}
rejectThenReload = async (info) => {
@@ -116,24 +124,22 @@ export default class UserDetail extends React.Component {
<ul className={styles.stats}>
<li className={styles.stat}>
<span className={styles.statItem}> Total Comments </span>
<spam className={styles.statResult}> {totalComments} </spam>
<span className={styles.statItem}>Total Comments</span>
<span className={styles.statResult}>{totalComments}</span>
</li>
<li className={styles.stat}>
<spam className={styles.statItem}> Reject Rate </spam>
<spam className={styles.statResult}> {`${(rejectedPercent).toFixed(1)}%`} </spam>
<span className={styles.statItem}>Reject Rate</span>
<span className={styles.statResult}>
{rejectedPercent.toFixed(1)}%
</span>
</li>
<li className={styles.stat}>
<spam className={styles.statItem}> Reports </spam>
<spam className={cn(styles.statReportResult, styles[getReliability(user.reliable.flagger)])}>
<span className={styles.statItem}>Reports</span>
<span className={cn(styles.statReportResult, styles[getReliability(user.reliable.flagger)])}>
{capitalize(getReliability(user.reliable.flagger))}
</spam>
</span>
</li>
</ul>
<p className={styles.small}>
Data represents the last six months of activity
</p>
</div>
<Slot
@@ -165,7 +171,7 @@ export default class UserDetail extends React.Component {
cStyle='reject'
icon='close'>
</Button>
{`${selectedCommentIds.length} comments selected`}
{selectedCommentIds.length} comments selected
</div>
)
}
@@ -152,7 +152,7 @@ export const withUserDetailQuery = withQuery(gql`
}
${getSlotFragmentSpreads(slots, 'user')}
}
totalComments: commentCount(query: {author_id: $author_id})
totalComments: commentCount(query: {author_id: $author_id, statuses: []})
rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]})
comments: comments(query: {
author_id: $author_id,
@@ -1,8 +1,8 @@
import React from 'react';
import {SelectField, Option} from 'react-mdl-selectfield';
import styles from '../components/Table.css';
import t from 'coral-framework/services/i18n';
import PropTypes from 'prop-types';
import {Dropdown, Option} from 'coral-ui';
import cn from 'classnames';
const Table = ({headers, commenters, onHeaderClickHandler, onRoleChange, onCommenterStatusChange, viewUserDetail}) => (
@@ -13,6 +13,7 @@ const Table = ({headers, commenters, onHeaderClickHandler, onRoleChange, onComme
<th
key={i}
className="mdl-data-table__cell--non-numeric"
scope="col"
onClick={() => onHeaderClickHandler({field: header.field})}>
{header.title}
</th>
@@ -30,26 +31,24 @@ const Table = ({headers, commenters, onHeaderClickHandler, onRoleChange, onComme
{row.created_at}
</td>
<td className="mdl-data-table__cell--non-numeric">
<SelectField
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>
<Dropdown
value={row.status}
placeholder={t('community.status')}
onChange={(status) => onCommenterStatusChange(row.id, status)}>
<Option value={'ACTIVE'} label={t('community.active')} />
<Option value={'BANNED'} label={t('community.banned')} />
</Dropdown>
</td>
<td className="mdl-data-table__cell--non-numeric">
<SelectField
<Dropdown
value={row.roles[0] || ''}
className={styles.selectField}
label={t('community.role')}
placeholder={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>
<Option value={''} label={t('community.none')} />
<Option value={'STAFF'} label={t('community.staff')} />
<Option value={'MODERATOR'} label={t('community.moderator')} />
<Option value={'ADMIN'} label={t('community.admin')} />
</Dropdown>
</td>
</tr>
))}
@@ -53,45 +53,26 @@
list-style: none;
}
.selectField {
.dropdow {
position: relative;
width: 140px;
height: 36px;
top: 5px;
margin-right: 10px;
background: #7B7B7B;
color: white;
padding: 6px 15px;
box-sizing: border-box;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
margin-top: 5px;
@media (--tablet) {
display: inline-block;
margin-top: 0px;
margin-left: 10px;
}
> div {
padding: 0;
}
i {
position: absolute;
top: 7px;
right: 7px;
}
input {
font-size: 1rem;
border-bottom: 0px;
font-family: inherit;
}
label {
top: -4px;
}
&:hover {
cursor: pointer;
}
}
.dropdownToggle {
background: #7B7B7B;
color: white;
&:focus {
background: #aaa;
}
}
.dropdownToggleOpen {
background: #aaa;
}
@@ -1,10 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './ViewOptions.css';
import {Card} from 'coral-ui';
import cn from 'classnames';
import {SelectField, Option} from 'react-mdl-selectfield';
import t from 'coral-framework/services/i18n';
import {Card, Dropdown, Option} from 'coral-ui';
class ViewOptions extends React.Component {
render() {
@@ -19,18 +18,19 @@ class ViewOptions extends React.Component {
<h2 className={cn(styles.headline, 'talk-admin-moderation-view-options-headline')}>
View Options
</h2>
<div className={styles.viewOptionsContent}>
<div className={styles.viewOptionsContent}>
<ul className={styles.viewOptionsList}>
<li className={styles.viewOptionsItem}>
Sort Comments
<SelectField
className={styles.selectField}
label="Sort"
<Dropdown
toggleClassName={styles.dropdownToggle}
toggleOpenClassName={styles.dropdownToggleOpen}
placeholder={t('modqueue.sort')}
value={sort}
onChange={(sort) => selectSort(sort)}>
<Option value={'DESC'}>{t('modqueue.newest_first')}</Option>
<Option value={'ASC'}>{t('modqueue.oldest_first')}</Option>
</SelectField>
<Option value={'DESC'} label={t('modqueue.newest_first')} />
<Option value={'ASC'} label={t('modqueue.oldest_first')} />
</Dropdown>
</li>
</ul>
</div>
@@ -76,31 +76,6 @@
display: block;
}
.statusMenu {
border-radius: 2px;
width: 10em;
text-align: center;
float: right;
color: #fff;
cursor: pointer;
letter-spacing: 0.7px;
font-weight: 400;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
}
.statusMenuOpen {
padding: 10px;
background-color: #268D81;
}
.statusMenuIcon {
float: right;
}
.statusMenuClosed {
padding: 10px;
background-color: #262626;
}
.hidden {
display: none;
@@ -1,16 +1,16 @@
import React, {Component} from 'react';
import styles from './Stories.css';
import t from 'coral-framework/services/i18n';
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';
import PropTypes from 'prop-types';
import sortBy from 'lodash/sortBy';
import {Dropdown, Option, Pager, Icon} from 'coral-ui';
import {DataTable, TableHeader, RadioGroup, Radio} from 'react-mdl';
import t from 'coral-framework/services/i18n';
import styles from './Stories.css';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
const limit = 25;
export default class Stories extends Component {
class Stories extends Component {
state = {
search: '',
@@ -50,51 +50,28 @@ export default class Stories extends Component {
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
}
onStatusClick = (closeStream, id, statusMenuOpen) => async () => {
if (statusMenuOpen) {
this.setState((prev) => {
prev.statusMenus[id] = false;
return prev;
});
try {
await this.props.updateAssetState(id, closeStream ? Date.now() : null);
const {search, sort, filter, page} = this.state;
this.props.fetchAssets(page, limit, search, sort, filter);
} catch (err) {
// TODO: handle error.
console.error(err);
}
} else {
this.setState((prev) => {
prev.statusMenus[id] = true;
return prev;
});
onStatusChange = async (closeStream, id) => {
try {
this.props.updateAssetState(id, closeStream ? Date.now() : null);
const {search, sort, filter, page} = this.state;
this.props.fetchAssets(page, limit, search, sort, filter);
} catch(err) {
console.error(err);
}
}
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>;
const closed = !!(closedAt && new Date(closedAt).getTime() < Date.now());
return (
<Dropdown
value={closed}
onChange={(value) => this.onStatusChange(value, id)}>
<Option value={false} label={t('streams.open')} />
<Option value={true} label={t('streams.closed')} />
</Dropdown>
);
}
onPageClick = (page) => {
@@ -174,3 +151,11 @@ export default class Stories extends Component {
}
}
Stories.propTypes = {
assets: PropTypes.object,
fetchAssets: PropTypes.func,
updateAssetState: PropTypes.func,
};
export default Stories;
+62
View File
@@ -0,0 +1,62 @@
.dropdown {
position: relative;
width: 150px;
height: 36px;
background: #2c2c2c;
box-sizing: border-box;
color: white;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
line-height: 1.4;
font-size: 13px;
}
.toggle {
padding: 10px 15px;
cursor: pointer;
outline: none;
&:focus {
background: #888;
}
}
.toggleOpen {
background: #888;
}
.label {
text-transform: capitalize;
}
.list {
position: absolute;
top: 30px;
left: 10px;
color: #2c2c2c;
border-radius: 2px;
background: white;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
width: 100%;
list-style: none;
padding: 0;
margin: 0;
transform: scale(0);
transition: transform .1s cubic-bezier(.4,0,.2,1),opacity .1s cubic-bezier(.4,0,.2,1),-webkit-transform .1s cubic-bezier(.4,0,.2,1);
transform-origin: 0 0;
}
.listActive {
opacity: 1;
transform: scale(1);
z-index: 999;
}
.arrow {
position: absolute;
right: 15px;
font-size: 1.2rem;
vertical-align: middle;
top: 13px;
}
+187
View File
@@ -0,0 +1,187 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './Dropdown.css';
import Icon from './Icon';
import cn from 'classnames';
import ClickOutside from 'coral-framework/components/ClickOutside';
class Dropdown extends React.Component {
toggleRef = null;
optionsRef = [];
state = {
isOpen: false
};
componentDidUpdate(_, prevState) {
if (!this.state.isOpen && prevState.isOpen) {
// Refocus on the toggle element when menu closes.
this.toggleRef.focus();
}
}
goUp = () => {
const index = this.optionsRef.findIndex((ref) => ref.hasFocus());
if (index > 0) {
this.optionsRef[index - 1].focus();
}
}
goDown = () => {
const index = this.optionsRef.findIndex((ref) => ref.hasFocus());
if (index < this.optionsRef.length - 1) {
this.optionsRef[index + 1].focus();
}
}
handleOptionKeyDown = (value, e) => {
const code = e.which;
switch (code) {
case 13: // 13 = Return
case 32: // 32 = Space
e.preventDefault();
this.setValue(value);
break;
case 38: // 38 = Arrow Up
e.preventDefault();
this.goUp();
break;
case 40: // 40 = Arrow Down
e.preventDefault();
this.goDown();
break;
}
}
handleOptionClick = (value) => {
this.setValue(value);
}
setValue = (value) => {
if (this.props.onChange) {
this.props.onChange(value);
}
this.setState({
isOpen: false
});
}
toggle = () => {
this.setState({
isOpen: !this.state.isOpen
});
}
handleClick = () => {
this.toggle();
}
handleKeyDown = (e) => {
const code = e.which;
// 13 = Return, 32 = Space
if ((code === 13) || (code === 32)) {
e.preventDefault();
this.toggle();
}
}
hideMenu = () => {
this.setState({
isOpen: false
});
}
handleToggleRef = (ref) => this.toggleRef = ref;
handleOptionsRef = (ref, index) => {
this.optionsRef[index] = ref;
// Focus on current value when menu opens.
if (ref) {
if (ref.props.value === this.props.value || index === 0 && !this.props.value) {
ref.focus();
return;
}
}
}
// Trap keyboard focus inside the dropdown until a value has been chosen.
trapFocusBegin = () => this.optionsRef[this.optionsRef.length - 1].focus();
trapFocusEnd = () => this.optionsRef[0].focus();
renderLabel() {
const options = React.Children.toArray(this.props.children);
const option = options.find((option) => option.props.value === this.props.value);
if (option) {
return option.props.label ? option.props.label : option.props.value;
} else if (this.props.value) {
return this.props.value;
} else {
return this.props.placeholder;
}
}
render() {
const {className, toggleClassName, toggleOpenClassName} = this.props;
return (
<ClickOutside onClickOutside={this.hideMenu}>
<div className={cn(styles.dropdown, className)}>
<div
className={cn(styles.toggle, toggleClassName, {[cn(this.state.isOpen, toggleOpenClassName)]: this.state.isOpen})}
onClick={this.handleClick}
onKeyDown={this.handleKeyDown}
role="button"
aria-pressed={this.state.isOpen}
aria-haspopup="true"
tabIndex="0"
ref={this.handleToggleRef}
>
{this.props.icon && <Icon name={this.props.icon} className={styles.icon} aria-hidden="true" />}
<span className={styles.label}>{this.renderLabel()}</span>
{this.state.isOpen ? <Icon name="keyboard_arrow_up" className={styles.arrow} aria-hidden="true"/> : <Icon name="keyboard_arrow_down" className={styles.arrow} aria-hidden="true"/>}
</div>
{this.state.isOpen &&
<div>
<div tabIndex="0" onFocus={this.trapFocusBegin} />
<ul className={cn(styles.list, {[styles.listActive] : this.state.isOpen})}>
{React.Children.toArray(this.props.children)
.map((child, i) =>
React.cloneElement(child, {
key: child.props.value,
ref: (ref) => this.handleOptionsRef(ref, i),
index: i,
onClick: () => this.handleOptionClick(child.props.value),
onKeyDown: (e) => this.handleOptionKeyDown(child.props.value, e)
}))}
</ul>
<div tabIndex="0" onFocus={this.trapFocusEnd} />
</div>
}
</div>
</ClickOutside>
);
}
}
Dropdown.propTypes = {
className: PropTypes.string,
toggleClassName: PropTypes.string,
toggleOpenClassName: PropTypes.string,
placeholder: PropTypes.string,
icon: PropTypes.string,
onChange: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
value: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
PropTypes.bool
]),
};
export default Dropdown;
+4 -3
View File
@@ -4,12 +4,13 @@ import {Icon as IconMDL} from 'react-mdl';
import cn from 'classnames';
import styles from './Icon.css';
const Icon = ({className = '', name}) => (
<IconMDL className={cn(styles.root, className)} name={name} />
const Icon = ({className = '', ...rest}) => (
<IconMDL className={cn(styles.root, className)} {...rest} />
);
Icon.propTypes = {
name: PropTypes.string.isRequired
name: PropTypes.string.isRequired,
className: PropTypes.string,
};
export default Icon;
+9 -2
View File
@@ -1,3 +1,10 @@
.Option {
.option {
padding: 10px;
text-transform: capitalize;
outline: none;
&:focus, &:hover {
background-color: #ccc;
cursor: pointer;
}
}
+33 -8
View File
@@ -1,14 +1,39 @@
import React from 'react';
import {Option as OptionMDL} from 'react-mdl-selectfield';
import PropTypes from 'prop-types';
import styles from './Option.css';
import cn from 'classnames';
const Option = (props) => {
const {children, ...attrs} = props;
return (
<OptionMDL className={styles.Option} {...attrs}>
{children}
</OptionMDL>
);
class Option extends React.Component {
ref = null;
handleRef = (ref) => {
this.ref = ref;
};
focus = () => {this.ref.focus();}
hasFocus = () => document.activeElement === this.ref;
render() {
const {className, label = '', onClick, onKeyDown} = this.props;
return (
<li className={cn(styles.option, className)} onClick={onClick} onKeyDown={onKeyDown} role="option" tabIndex="0" ref={this.handleRef}>
{label}
</li>
);
}
}
Option.propTypes = {
className: PropTypes.string,
label: PropTypes.string,
onClick: PropTypes.func,
onKeyDown: PropTypes.func,
value: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
PropTypes.bool
]),
};
export default Option;
-33
View File
@@ -1,33 +0,0 @@
.Select {
position: relative;
width: 100%;
height: 40px;
background: #2c2c2c;
padding: 10px 15px;
box-sizing: border-box;
color: white;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
> div {
padding: 0;
}
i {
position: absolute;
top: 7px;
right: 7px;
}
input {
padding: 0;
font-size: 13px;
letter-spacing: 0.7px;
font-weight: 400;
}
&:hover {
cursor: pointer;
}
}
-14
View File
@@ -1,14 +0,0 @@
import React from 'react';
import {SelectField} from 'react-mdl-selectfield';
import styles from './Select.css';
const Select = (props) => {
const {children, ...attrs} = props;
return (
<SelectField className={styles.Select} {...attrs}>
{children}
</SelectField>
);
};
export default Select;
+2 -2
View File
@@ -21,10 +21,10 @@ export {default as Success} from './components/Success';
export {default as Pager} from './components/Pager';
export {default as Wizard} from './components/Wizard';
export {default as WizardNav} from './components/WizardNav';
export {default as Select} from './components/Select';
export {default as Option} from './components/Option';
export {default as SnackBar} from './components/SnackBar';
export {default as TextArea} from './components/TextArea';
export {default as Drawer} from './components/Drawer';
export {default as Label} from './components/Label';
export {default as FlagLabel} from './components/FlagLabel';
export {default as Dropdown} from './components/Dropdown';
export {default as Option} from './components/Option';
+2 -2
View File
@@ -92,7 +92,7 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, au
let query = CommentModel.find();
// If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs
// special priviledges.
// special privileges.
if (
(!statuses || statuses.some((status) => !['NONE', 'ACCEPTED'].includes(status))) &&
(context.user == null || !context.user.can(SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS))
@@ -100,7 +100,7 @@ const getCommentCountByQuery = (context, {ids, statuses, asset_id, parent_id, au
return null;
}
if (statuses) {
if (statuses && statuses.length > 0) {
query = query.where({status: {$in: statuses}});
}
+2
View File
@@ -75,6 +75,7 @@ en:
status: Status
username_and_email: "Username and Email"
yes_ban_user: "Yes Ban User"
none: "None"
configure:
apply: Apply
banned_word_text: "Comments which contain these words or phrases (not case-sensitive) will be automatically removed from the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list."
@@ -289,6 +290,7 @@ en:
select_stream: "Select Stream"
shift_key: "⇧"
shortcuts: "Shortcuts"
sort: "Sort"
show_shortcuts: "Show Shortcuts"
singleview: "Toggle single comment edit view"
thismenu: "Open this menu"
+2
View File
@@ -74,6 +74,7 @@ es:
status: Estado
username_and_email: "Usuario y Correo"
yes_ban_user: "Si, Suspendan el usuario"
none: "Ninguno"
configure:
apply: Aplicar
banned_word_text: "Comentarios que contengan estas palabras o frases, en mayusculas o minúsculas, serán automáticamente eliminados del hilo de comentario. Escribir una palabra y apretar Enter o Tabulador para agregarla. O pueden pegar una lista de palabras separadas por coma."
@@ -280,6 +281,7 @@ es:
select_stream: "Seleccionar hilo de comentarios"
shift_key:
shortcuts: Atajos
sort: "Ordenar"
show_shortcuts: "Mostrar Atajos"
singleview: "Colocar vista de edición de comentario único"
thismenu: "Abrir este menu"