mirror of
https://github.com/wassname/talk.git
synced 2026-06-30 02:23:06 +08:00
Merge branch 'master' into flag-username-reset
This commit is contained in:
@@ -12,3 +12,5 @@ dump.rdb
|
||||
*.cfg
|
||||
.idea/
|
||||
coverage/
|
||||
.tags
|
||||
.tags1
|
||||
|
||||
@@ -5,10 +5,13 @@ import Streams from 'containers/Streams/Streams';
|
||||
import Configure from 'containers/Configure/Configure';
|
||||
import LayoutContainer from 'containers/LayoutContainer';
|
||||
import InstallContainer from 'containers/Install/InstallContainer';
|
||||
|
||||
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';
|
||||
|
||||
const routes = (
|
||||
@@ -21,6 +24,18 @@ const routes = (
|
||||
<Route path='streams' component={Streams} />
|
||||
<Route path='dashboard' component={Dashboard} />
|
||||
|
||||
{/* Community Routes */}
|
||||
|
||||
<Route path='community' component={CommunityLayout}>
|
||||
<Route path='flagged' components={CommunityContainer}>
|
||||
<Route path=':id' components={CommunityContainer} />
|
||||
</Route>
|
||||
<Route path='people' components={CommunityContainer}>
|
||||
<Route path=':id' components={CommunityContainer} />
|
||||
</Route>
|
||||
<IndexRedirect to='flagged' />
|
||||
</Route>
|
||||
|
||||
{/* Moderation Routes */}
|
||||
|
||||
<Route path='moderate' component={ModerationLayout}>
|
||||
|
||||
@@ -7,28 +7,33 @@ import {
|
||||
SORT_UPDATE,
|
||||
COMMENTERS_NEW_PAGE,
|
||||
SET_ROLE,
|
||||
SET_COMMENTER_STATUS
|
||||
SET_COMMENTER_STATUS,
|
||||
SHOW_BANUSER_DIALOG,
|
||||
HIDE_BANUSER_DIALOG,
|
||||
SHOW_SUSPENDUSER_DIALOG,
|
||||
HIDE_SUSPENDUSER_DIALOG
|
||||
} from '../constants/community';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
|
||||
export const fetchCommenters = (query = {}) => dispatch => {
|
||||
dispatch(requestFetchCommenters());
|
||||
export const fetchAccounts = (query = {}) => dispatch => {
|
||||
|
||||
dispatch(requestFetchAccounts());
|
||||
coralApi(`/users?${qs.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>
|
||||
.then(({result, page, count, limit, totalPages}) =>{
|
||||
dispatch({
|
||||
type: FETCH_COMMENTERS_SUCCESS,
|
||||
commenters: result,
|
||||
accounts: result,
|
||||
page,
|
||||
count,
|
||||
limit,
|
||||
totalPages
|
||||
})
|
||||
)
|
||||
});
|
||||
})
|
||||
.catch(error => dispatch({type: FETCH_COMMENTERS_FAILURE, error}));
|
||||
};
|
||||
|
||||
const requestFetchCommenters = () => ({
|
||||
const requestFetchAccounts = () => ({
|
||||
type: FETCH_COMMENTERS_REQUEST
|
||||
});
|
||||
|
||||
@@ -55,3 +60,11 @@ export const setCommenterStatus = (id, status) => (dispatch) => {
|
||||
return dispatch({type: SET_COMMENTER_STATUS, id, status});
|
||||
});
|
||||
};
|
||||
|
||||
// Ban User Dialog
|
||||
export const showBanUserDialog = (user) => ({type: SHOW_BANUSER_DIALOG, user});
|
||||
export const hideBanUserDialog = () => ({type: HIDE_BANUSER_DIALOG});
|
||||
|
||||
// Suspend User Dialog
|
||||
export const showSuspendUserDialog = (user) => ({type: SHOW_SUSPENDUSER_DIALOG, user});
|
||||
export const hideSuspendUserDialog = () => ({type: HIDE_SUSPENDUSER_DIALOG});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import React from 'react';
|
||||
import {Button, Icon} from 'react-mdl';
|
||||
import styles from './Modal.css';
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
import styles from './SuspendUserModal.css';
|
||||
import {Button} from 'coral-ui';
|
||||
|
||||
const stages = [
|
||||
{
|
||||
title: 'suspenduser.title_0',
|
||||
description: 'suspenduser.description_0',
|
||||
options: {
|
||||
'j': 'suspenduser.no_cancel',
|
||||
'k': 'suspenduser.yes_suspend'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'suspenduser.title_1',
|
||||
description: 'suspenduser.description_1',
|
||||
options: {
|
||||
'j': 'bandialog.cancel',
|
||||
'k': 'suspenduser.send'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
class SuspendUserModal extends Component {
|
||||
|
||||
state = {email: '', stage: 0}
|
||||
|
||||
static propTypes = {
|
||||
stage: PropTypes.number,
|
||||
actionType: PropTypes.string,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
suspendUser: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const about = lang.t('suspenduser.username');
|
||||
this.setState({email: lang.t('suspenduser.email', about)});
|
||||
}
|
||||
|
||||
/*
|
||||
* When an admin clicks to suspend a user a dialog is shown, this function
|
||||
* handles the possible actions for that dialog.
|
||||
*/
|
||||
onActionClick = (stage, menuOption) => () => {
|
||||
const {suspendUser, action} = this.props;
|
||||
const {stage, email} = this.state;
|
||||
const cancel = this.props.onClose;
|
||||
const next = () => this.setState({stage: stage + 1});
|
||||
const suspend = () => suspendUser(action.item_id, lang.t('suspenduser.email_subject'), email)
|
||||
.then(this.props.onClose);
|
||||
const suspendModalActions = [
|
||||
[ cancel, next ],
|
||||
[ cancel, suspend ]
|
||||
];
|
||||
return suspendModalActions[stage][menuOption]();
|
||||
}
|
||||
|
||||
onEmailChange = (e) => this.setState({email: e.target.value})
|
||||
|
||||
render () {
|
||||
const {action, onClose} = this.props;
|
||||
|
||||
if (!action) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {stage} = this.state;
|
||||
const actionType = action.actionType;
|
||||
const about = actionType === 'flag_bio' ? lang.t('suspenduser.bio') : lang.t('suspenduser.username');
|
||||
return <Modal open={true} onClose={onClose}>
|
||||
<div className={styles.title}>{lang.t(stages[stage].title, about)}</div>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.description}>
|
||||
{lang.t(stages[stage].description, about)}
|
||||
</div>
|
||||
{
|
||||
stage === 1 &&
|
||||
<div className={styles.writeContainer}>
|
||||
<div className={styles.emailMessage}>{lang.t('suspenduser.write_message')}</div>
|
||||
<div className={styles.emailContainer}>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={styles.emailInput}
|
||||
value={this.state.email}
|
||||
onChange={this.onEmailChange}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className={styles.modalButtons}>
|
||||
{Object.keys(stages[stage].options).map((key, i) => (
|
||||
<Button key={i} onClick={this.onActionClick(stage, i)}>
|
||||
{lang.t(stages[stage].options[key], about)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>;
|
||||
}
|
||||
}
|
||||
|
||||
export default SuspendUserModal;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './ModerationList.css';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
import {Icon} from 'react-mdl';
|
||||
import ActionButton from './ActionButton';
|
||||
|
||||
// Render a single comment for the list
|
||||
const User = props => {
|
||||
const {action, user} = props;
|
||||
let userStatus = user.status;
|
||||
|
||||
// Do not display unless the user status is 'pending' or 'banned'.
|
||||
// This means that they have already been reviewed and approved.
|
||||
return (userStatus === 'PENDING' || userStatus === 'BANNED') &&
|
||||
<li tabIndex={props.index} className={`mdl-card mdl-shadow--2dp ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
<span>{user.username}</span>
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{props.modActions.map(
|
||||
(action, i) =>
|
||||
<ActionButton
|
||||
type={action.toUpperCase()}
|
||||
key={i}
|
||||
user={user}
|
||||
menuOptionsMap={props.menuOptionsMap}
|
||||
onClickAction={props.onClickAction}
|
||||
onClickShowBanDialog={props.onClickShowBanDialog}/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{userStatus === 'banned' ?
|
||||
<span className={styles.banned}><Icon name='error_outline'/> {lang.t('comment.banned_user')}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.flagCount}>
|
||||
{`${action.count} ${action.action_type === 'flag_bio' ? lang.t('user.bio_flags') : lang.t('user.username_flags')}`}
|
||||
</div>
|
||||
</li>;
|
||||
};
|
||||
|
||||
export default User;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
@@ -1,6 +1,5 @@
|
||||
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
|
||||
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
|
||||
export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS';
|
||||
export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST = 'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST';
|
||||
export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS = 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS';
|
||||
export const COMMENT_CREATE_SUCCESS = 'COMMENT_CREATE_SUCCESS';
|
||||
|
||||
@@ -5,3 +5,13 @@ export const SORT_UPDATE = 'SORT_UPDATE';
|
||||
export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE';
|
||||
export const SET_ROLE = 'SET_ROLE';
|
||||
export const SET_COMMENTER_STATUS = 'SET_COMMENTER_STATUS';
|
||||
|
||||
export const FETCH_FLAGGED_COMMENTERS_REQUEST = 'FETCH_FLAGGED_COMMENTERS_REQUEST';
|
||||
export const FETCH_FLAGGED_COMMENTERS_SUCCESS = 'FETCH_FLAGGED_COMMENTERS_SUCCESS';
|
||||
export const FETCH_FLAGGED_COMMENTERS_FAILURE = 'FETCH_FLAGGED_COMMENTERS_FAILURE';
|
||||
|
||||
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
|
||||
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
|
||||
|
||||
export const SHOW_SUSPENDUSER_DIALOG = 'SHOW_SUSPENDUSER_DIALOG';
|
||||
export const HIDE_SUSPENDUSER_DIALOG = 'HIDE_SUSPENDUSER_DIALOG';
|
||||
|
||||
@@ -3,4 +3,3 @@ export const UPDATE_STATUS_SUCCESS = 'UPDATE_STATUS_SUCCESS';
|
||||
export const UPDATE_STATUS_FAILURE = 'UPDATE_STATUS_FAILURE';
|
||||
export const USER_EMAIL_FAILURE = 'USER_EMAIL_FAILURE';
|
||||
export const USERNAME_ENABLE_FAILURE = 'USERNAME_ENABLE_FAILURE';
|
||||
export const USERS_MODERATION_QUEUE_FETCH_SUCCESS = 'USERS_MODERATION_QUEUE_FETCH_SUCCESS';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
@@ -15,6 +17,12 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mainFlaggedContent {
|
||||
width: 100%;
|
||||
padding: 34px 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.roleButton {
|
||||
display: block;
|
||||
}
|
||||
@@ -94,3 +102,188 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 8px 0;
|
||||
list-style: none;
|
||||
display: block;
|
||||
|
||||
&.singleView .listItem {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.singleView .listItem.activeItem {
|
||||
display: block;
|
||||
height: 100%;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5em;
|
||||
border: none;
|
||||
|
||||
.actions {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 25%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 50%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listItem {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 14px;
|
||||
position: relative;
|
||||
transition: box-shadow 200ms;
|
||||
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 40px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: inline;
|
||||
|
||||
.author {
|
||||
font-size: 24px;
|
||||
min-width: 50px;
|
||||
align-items: left;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.itemBody {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.created {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 20px;
|
||||
flex: 1;
|
||||
font-size: 0.88em;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
color: rgba(255, 0, 0, .5);
|
||||
padding-top: 15px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.flagCount{
|
||||
font-size: 12px;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #444;
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@media (--big-viewport) {
|
||||
.listItem {
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
&.activeItem {
|
||||
border: 2px solid #333;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.hasLinks {
|
||||
color: #f00;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.banned {
|
||||
color: #f00;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.ban {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.banButton {
|
||||
width: 114px;
|
||||
letter-spacing: 1px;
|
||||
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.flaggedByCount {
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.flaggedBy {
|
||||
display: inline;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.flaggedByLabel {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
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, suspendUser} from '../../graphql/mutations';
|
||||
|
||||
import {
|
||||
fetchCommenters,
|
||||
fetchAccounts,
|
||||
updateSorting,
|
||||
newPage,
|
||||
showBanUserDialog,
|
||||
hideBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
hideSuspendUserDialog
|
||||
} from '../../actions/community';
|
||||
|
||||
import Community from './Community';
|
||||
import CommunityMenu from './components/CommunityMenu';
|
||||
import BanUserDialog from './components/BanUserDialog';
|
||||
import SuspendUserDialog from './components/SuspendUserDialog';
|
||||
|
||||
import People from './People';
|
||||
import FlaggedAccounts from './FlaggedAccounts';
|
||||
|
||||
class CommunityContainer extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -22,6 +37,10 @@ class CommunityContainer extends Component {
|
||||
this.onNewPageHandler = this.onNewPageHandler.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.fetchAccounts({});
|
||||
}
|
||||
|
||||
onKeyDownHandler(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
@@ -38,16 +57,13 @@ class CommunityContainer extends Component {
|
||||
search(query = {}) {
|
||||
const {community} = this.props;
|
||||
|
||||
this.props.dispatch(fetchCommenters({
|
||||
this.props.fetchAccounts({
|
||||
value: this.state.searchValue,
|
||||
field: community.field,
|
||||
asc: community.asc,
|
||||
field: community.fieldPeople,
|
||||
asc: community.ascPeople,
|
||||
...query
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
componentDidMount() {
|
||||
this.search();
|
||||
}
|
||||
|
||||
onHeaderClickHandler(sort) {
|
||||
@@ -60,19 +76,64 @@ class CommunityContainer extends Component {
|
||||
this.search({page});
|
||||
}
|
||||
|
||||
getTabContent(searchValue, props) {
|
||||
const {community, data} = props;
|
||||
const activeTab = props.route.path === ':id' ? 'flagged' : props.route.path;
|
||||
|
||||
if (activeTab === 'people') {
|
||||
return (
|
||||
<People
|
||||
isFetching={community.isFetchingPeople}
|
||||
commenters={community.accounts}
|
||||
searchValue={searchValue}
|
||||
error={community.errorPeople}
|
||||
totalPages={community.totalPagesPeople}
|
||||
page={community.pagePeople}
|
||||
{...this}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FlaggedAccounts
|
||||
commenters={data.users}
|
||||
isFetching={data.loading}
|
||||
error={data.error}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
approveUser={props.approveUser}
|
||||
suspendUser={props.suspendUser}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
{...this}
|
||||
/>
|
||||
<BanUserDialog
|
||||
open={community.banDialog}
|
||||
user={community.user}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
handleBanUser={props.banUser}
|
||||
/>
|
||||
<SuspendUserDialog
|
||||
open={community.suspendDialog}
|
||||
handleClose={props.hideSuspendUserDialog}
|
||||
user={community.user}
|
||||
suspendUser={props.suspendUser}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {searchValue} = this.state;
|
||||
const {community} = this.props;
|
||||
|
||||
const tab = this.getTabContent(searchValue, this.props);
|
||||
|
||||
return (
|
||||
<Community
|
||||
searchValue={searchValue}
|
||||
commenters={community.commenters}
|
||||
isFetching={community.isFetching}
|
||||
error={community.error}
|
||||
totalPages={community.totalPages}
|
||||
page={community.page}
|
||||
{...this}
|
||||
/>
|
||||
<div>
|
||||
<CommunityMenu />
|
||||
<div>
|
||||
{ tab }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -81,4 +142,18 @@ const mapStateToProps = state => ({
|
||||
community: state.community.toJS()
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(CommunityContainer);
|
||||
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,
|
||||
suspendUser
|
||||
)(CommunityContainer);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
const CommunityLayout = props => (
|
||||
<div>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default CommunityLayout;
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
import styles from './Community.css';
|
||||
|
||||
import Loading from './Loading';
|
||||
import EmptyCard from '../../components/EmptyCard';
|
||||
import User from './components/User';
|
||||
|
||||
const FlaggedAccounts = ({...props}) => {
|
||||
const {commenters, isFetching} = props;
|
||||
const hasResults = !isFetching && commenters && !!commenters.length;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.mainFlaggedContent}>
|
||||
{ isFetching && <Loading /> }
|
||||
{
|
||||
hasResults
|
||||
? commenters.map((commenter, index) => {
|
||||
if (commenter.status === 'PENDING' && commenter.actions.length > 0) {
|
||||
return <User
|
||||
user={commenter}
|
||||
key={index}
|
||||
index={index}
|
||||
modActionButtons={['REJECT', 'APPROVE']}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
approveUser={props.approveUser}
|
||||
suspendUser={props.suspendUser}
|
||||
/>;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
: <EmptyCard>{lang.t('community.no-flagged-accounts')}</EmptyCard>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlaggedAccounts;
|
||||
+3
-3
@@ -29,7 +29,7 @@ const tableHeaders = [
|
||||
}
|
||||
];
|
||||
|
||||
const Community = ({isFetching, commenters, ...props}) => {
|
||||
const People = ({isFetching, commenters, ...props}) => {
|
||||
const hasResults = !isFetching && !!commenters.length;
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@@ -58,7 +58,7 @@ const Community = ({isFetching, commenters, ...props}) => {
|
||||
hasResults
|
||||
? <Table
|
||||
headers={tableHeaders}
|
||||
data={commenters}
|
||||
commenters={commenters}
|
||||
onHeaderClickHandler={props.onHeaderClickHandler}
|
||||
/>
|
||||
: <EmptyCard>{lang.t('community.no-results')}</EmptyCard>
|
||||
@@ -73,4 +73,4 @@ const Community = ({isFetching, commenters, ...props}) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Community;
|
||||
export default People;
|
||||
@@ -77,4 +77,4 @@ class Table extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({commenters: state.community.get('commenters')}))(Table);
|
||||
export default connect(state => ({commenters: state.community.get('accounts')}))(Table);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import styles from '../Community.css';
|
||||
import BanUserButton from '../../../components/BanUserButton';
|
||||
import {FabButton} from 'coral-ui';
|
||||
import {menuActionsMap} from '../../../containers/ModerationQueue/helpers/moderationQueueActionsMap';
|
||||
|
||||
const ActionButton = ({type = '', user, ...props}) => {
|
||||
if (type === 'BAN') {
|
||||
return <BanUserButton icon='not interested' user={user} onClick={() => props.showBanUserDialog(user)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<FabButton
|
||||
className={`${type.toLowerCase()} ${styles.actionButton}`}
|
||||
cStyle={type.toLowerCase()}
|
||||
icon={menuActionsMap[type].icon}
|
||||
onClick={() => {
|
||||
type === 'APPROVE' ? props.approveUser({userId: user.id}) : props.showSuspendUserDialog({user: user});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
@@ -0,0 +1,164 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 500px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 184px;
|
||||
padding: 20px;
|
||||
|
||||
h2 {
|
||||
color: black;
|
||||
font-size: 1.76em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: black;
|
||||
font-size: 1.4em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.textField {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.textField label {
|
||||
font-size: 1.08em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.textField input {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
outline: none;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
padding: 10px 6px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin: 5px auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin: 20px auto 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer span {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.socialConnections {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.signInButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #6b6b6b;
|
||||
}
|
||||
|
||||
input.error{
|
||||
border: solid 2px #f44336;
|
||||
}
|
||||
|
||||
.errorMsg, .hint {
|
||||
color: grey;
|
||||
font-weight: 600;
|
||||
padding: 3px 0 16px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.alert--success {
|
||||
border: solid 1px #1ec00e;
|
||||
background: #cbf1b8;
|
||||
color: #006900;
|
||||
}
|
||||
|
||||
.alert--error {
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
}
|
||||
|
||||
.userBox a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.attention {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: #B71C1C;
|
||||
color: #FFEBEE;
|
||||
font-weight: bolder;
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 9px;
|
||||
line-height: 7px;
|
||||
text-align: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.passwordRequestSuccess {
|
||||
border: 1px solid green;
|
||||
background-color: lightgreen;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.passwordRequestFailure {
|
||||
border: 1px solid orange;
|
||||
background-color: 1px solid coral;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
margin-right: 10px;
|
||||
width: 47%;
|
||||
}
|
||||
|
||||
.ban {
|
||||
width: 47%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin: 20px 0;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import styles from './BanUserDialog.css';
|
||||
|
||||
import Button from 'coral-ui/components/Button';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const BanUserDialog = ({open, handleClose, handleBanUser, user}) => (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
id="banuserDialog"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
title={lang.t('community.ban_user')}>
|
||||
<span className={styles.close} onClick={handleClose}>×</span>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h2>{lang.t('community.ban_user')}</h2>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h3>{lang.t('community.are_you_sure', user.username)}</h3>
|
||||
<i>{lang.t('community.note')}</i>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="cancel" className={styles.cancel} onClick={handleClose} raised>
|
||||
{lang.t('community.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
cStyle="black" className={styles.ban}
|
||||
onClick={() => {
|
||||
handleBanUser({userId: user.id}).then(() => {
|
||||
handleClose();
|
||||
});
|
||||
}}
|
||||
raised>
|
||||
{lang.t('community.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
BanUserDialog.propTypes = {
|
||||
handleBanUser: PropTypes.func.isRequired,
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default BanUserDialog;
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './styles.css';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations.json';
|
||||
|
||||
import {Link} from 'react-router';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const CommunityMenu = () => {
|
||||
const flaggedPath = '/admin/community/flagged';
|
||||
const peoplePath = '/admin/community/people';
|
||||
return (
|
||||
<div className='mdl-tabs'>
|
||||
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
|
||||
<div>
|
||||
<Link to={flaggedPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
{lang.t('community.flaggedaccounts')}
|
||||
</Link>
|
||||
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
{lang.t('community.people')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommunityMenu;
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
|
||||
import {Dialog, Button} from 'coral-ui';
|
||||
import styles from './SuspendUserDialog.css';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../../translations.json';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const stages = [
|
||||
{
|
||||
title: 'suspenduser.title_0',
|
||||
description: 'suspenduser.description_0',
|
||||
options: {
|
||||
'j': 'suspenduser.no_cancel',
|
||||
'k': 'suspenduser.yes_suspend'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'suspenduser.title_1',
|
||||
description: 'suspenduser.description_1',
|
||||
options: {
|
||||
'j': 'bandialog.cancel',
|
||||
'k': 'suspenduser.send'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
class SuspendUserDialog extends Component {
|
||||
|
||||
state = {email: '', stage: 0}
|
||||
|
||||
static propTypes = {
|
||||
stage: PropTypes.number,
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
suspendUser: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({email: lang.t('suspenduser.email'), about: lang.t('suspenduser.username')});
|
||||
}
|
||||
|
||||
/*
|
||||
* When an admin clicks to suspend a user a dialog is shown, this function
|
||||
* handles the possible actions for that dialog.
|
||||
*/
|
||||
onActionClick = (stage, menuOption) => () => {
|
||||
const {suspendUser, user} = this.props;
|
||||
const {stage} = this.state;
|
||||
|
||||
const cancel = this.props.onClose;
|
||||
const next = () => this.setState({stage: stage + 1});
|
||||
const suspend = () => {
|
||||
suspendUser({userId: user.user.id})
|
||||
.then(() => {
|
||||
this.props.handleClose();
|
||||
});
|
||||
};
|
||||
|
||||
const suspendModalActions = [
|
||||
[ cancel, next ],
|
||||
[ cancel, suspend ]
|
||||
];
|
||||
return suspendModalActions[stage][menuOption]();
|
||||
}
|
||||
|
||||
onEmailChange = (e) => {
|
||||
this.setState({email: e.target.value});
|
||||
}
|
||||
|
||||
render () {
|
||||
const {open, handleClose} = this.props;
|
||||
const {stage} = this.state;
|
||||
|
||||
return <Dialog
|
||||
className={styles.suspendDialog}
|
||||
id="suspendUserDialog"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
title={lang.t('suspenduser.title')}>
|
||||
<div className={styles.title}>
|
||||
{lang.t(stages[stage].title, lang.t('suspenduser.username'))}
|
||||
</div>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.description}>
|
||||
{lang.t(stages[stage].description, lang.t('suspenduser.username'))}
|
||||
</div>
|
||||
{
|
||||
stage === 1 &&
|
||||
<div className={styles.writeContainer}>
|
||||
<div className={styles.emailMessage}>{lang.t('suspenduser.write_message')}</div>
|
||||
<div className={styles.emailContainer}>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={styles.emailInput}
|
||||
value={this.state.email}
|
||||
onChange={this.onEmailChange}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className={styles.modalButtons}>
|
||||
{Object.keys(stages[stage].options).map((key, i) => (
|
||||
<Button key={i} onClick={this.onActionClick(stage, i)}>
|
||||
{lang.t(stages[stage].options[key], lang.t('suspenduser.username'))}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>;
|
||||
}
|
||||
}
|
||||
|
||||
export default SuspendUserDialog;
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import styles from '../Community.css';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../../translations.json';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
// Render a single user for the list
|
||||
const User = props => {
|
||||
const {user, modActionButtons} = props;
|
||||
let userStatus = user.status;
|
||||
|
||||
// Do not display unless the user status is 'pending' or 'banned'.
|
||||
// This means that they have already been reviewed and approved.
|
||||
return (userStatus === 'PENDING' || userStatus === 'BANNED') &&
|
||||
<li tabIndex={props.index} className={`mdl-card mdl-shadow--2dp ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
|
||||
<div className={styles.itemHeader}>
|
||||
<span className={styles.author}>{user.username}</span>
|
||||
<ActionButton
|
||||
className={styles.banButton}
|
||||
type='BAN'
|
||||
user={user}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.itemBody}>
|
||||
<div className={styles.flaggedByCount}>
|
||||
<i className="material-icons">flag</i><span className={styles.flaggedByLabel}>Flags({ user.actions.length })</span>:
|
||||
{ user.action_summaries.map(
|
||||
(action, i ) => {
|
||||
return <span className={styles.flaggedBy} key={i}>
|
||||
{lang.t(`community.${action.reason}`)} ({action.count})
|
||||
</span>;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.flaggedReasons}>
|
||||
{user.actions.map(
|
||||
(action, i) => {
|
||||
return <span key={i}>
|
||||
{action.reason}
|
||||
{/* action.user.username */}
|
||||
</span>;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{modActionButtons.map(
|
||||
(action, i) => {
|
||||
return <ActionButton
|
||||
type={action.toUpperCase()}
|
||||
key={i}
|
||||
user={user}
|
||||
approveUser={props.approveUser}
|
||||
suspendUser={props.suspendUser}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
/>;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>;
|
||||
};
|
||||
|
||||
export default User;
|
||||
@@ -0,0 +1,336 @@
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.listContainer {
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tabBar {
|
||||
background-color: rgba(44, 44, 44, 0.89);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
color: white;
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
letter-spacing: 1px;
|
||||
transition: border-bottom 200ms;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: white;
|
||||
box-sizing: border-box;
|
||||
border-bottom: solid 5px #F36451;
|
||||
}
|
||||
|
||||
.active > span {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.active:after {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.showShortcuts {
|
||||
position: absolute;
|
||||
right: 130px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
|
||||
span {
|
||||
margin-left: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (--big-viewport) {
|
||||
.tab {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
.notFound {
|
||||
position: relative;
|
||||
margin: 20px auto;
|
||||
text-align: center;
|
||||
padding: 68px 45px;
|
||||
vertical-align: middle;
|
||||
min-width: 500px;
|
||||
|
||||
a {
|
||||
color: rgb(244, 126, 107);
|
||||
font-weight: 500;
|
||||
|
||||
&.goToStreams {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #2c2c2c;
|
||||
color: white;
|
||||
margin-bottom: -1px;
|
||||
|
||||
.settingsButton {
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
.moderateAsset {
|
||||
a {
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
color: white;
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
letter-spacing: 1px;
|
||||
transition: opacity 200ms;
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
opacity: .8;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.list {
|
||||
padding: 8px 0;
|
||||
list-style: none;
|
||||
display: block;
|
||||
|
||||
&.singleView .listItem {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.singleView .listItem.activeItem {
|
||||
display: block;
|
||||
height: 100%;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5em;
|
||||
border: none;
|
||||
|
||||
.actions {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 25%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 50%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listItem {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 14px;
|
||||
position: relative;
|
||||
transition: box-shadow 200ms;
|
||||
margin-top: 0;
|
||||
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.context {
|
||||
a {
|
||||
color: #f36451;
|
||||
text-decoration: underline;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 40px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.author {
|
||||
min-width: 230px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.itemBody {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 16px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #757575;
|
||||
font-size: 40px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.created {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 0px;
|
||||
flex: 1;
|
||||
font-size: 0.88em;
|
||||
color: black;
|
||||
max-width: 500px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
color: rgba(255, 0, 0, .5);
|
||||
padding-top: 15px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.flagCount{
|
||||
font-size: 12px;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #444;
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@media (--big-viewport) {
|
||||
.listItem {
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
&.activeItem {
|
||||
border: 2px solid #333;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.hasLinks {
|
||||
color: #f00;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.banned {
|
||||
color: #f00;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.ban {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.Comment {
|
||||
.moderateArticle {
|
||||
font-size: 12px;
|
||||
a {
|
||||
display: inline-block;
|
||||
color: #679af3;
|
||||
text-decoration: none;
|
||||
font-size: 1em;
|
||||
font-weight: 400;
|
||||
letter-spacing: .5px;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: .9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flagBox {
|
||||
max-width: 480px;
|
||||
border-top: 1px solid rgba(66, 66, 66, 0.12);
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@ const ModerationHeader = props => (
|
||||
props.asset ?
|
||||
<div className={`mdl-tabs__tab-bar ${styles.moderateAsset}`}>
|
||||
<Link className="mdl-tabs__tab" to="/admin/moderate">All Streams</Link>
|
||||
<a className="mdl-tabs__tab">
|
||||
{props.asset.title}
|
||||
<a href={props.asset.url} className={styles.settingsButton}><Icon name="settings"/></a>
|
||||
<a className="mdl-tabs__tab" href={props.asset.url}>
|
||||
<span>{props.asset.title}</span>
|
||||
<Icon className={styles.settingsButton} name="open_in_new"/>
|
||||
</a>
|
||||
<Link className="mdl-tabs__tab" to="/admin/streams">Select Stream</Link>
|
||||
</div>
|
||||
|
||||
@@ -84,11 +84,10 @@ span {
|
||||
margin-bottom: -1px;
|
||||
|
||||
.settingsButton {
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
margin-top: -4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.moderateAsset {
|
||||
@@ -114,7 +113,15 @@ span {
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
text-align: center;
|
||||
span {
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 344px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
|
||||
export const banUser = graphql(SET_USER_STATUS, {
|
||||
props: ({mutate}) => ({
|
||||
@@ -9,11 +10,39 @@ export const banUser = graphql(SET_USER_STATUS, {
|
||||
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: ({userId}) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
userId
|
||||
},
|
||||
refetchQueries: ['Users']
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
|
||||
props: ({mutate}) => ({
|
||||
acceptComment: ({commentId}) => {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
mutation suspendUser($userId: ID!) {
|
||||
suspendUser(id: $userId) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import {graphql} from 'react-apollo';
|
||||
|
||||
import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
|
||||
import MOD_USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql';
|
||||
import METRICS from './metricsQuery.graphql';
|
||||
|
||||
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
|
||||
@@ -30,6 +31,8 @@ export const getMetrics = graphql(METRICS, {
|
||||
}
|
||||
});
|
||||
|
||||
export const modUserFlaggedQuery = graphql(MOD_USER_FLAGGED_QUERY);
|
||||
|
||||
export const modQueueResort = (id, fetchMore) => (sort) => {
|
||||
return fetchMore({
|
||||
query: MOD_QUEUE_QUERY,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
query Users ($n: ACTION_TYPE) {
|
||||
users (query:{action_type: $n}){
|
||||
id
|
||||
username
|
||||
status
|
||||
roles
|
||||
actions{
|
||||
id
|
||||
created_at
|
||||
... on FlagAction {
|
||||
reason
|
||||
}
|
||||
user {
|
||||
username
|
||||
}
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
reason
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,58 +6,88 @@ import {
|
||||
FETCH_COMMENTERS_SUCCESS,
|
||||
SORT_UPDATE,
|
||||
SET_ROLE,
|
||||
SET_COMMENTER_STATUS
|
||||
SET_COMMENTER_STATUS,
|
||||
SHOW_BANUSER_DIALOG,
|
||||
HIDE_BANUSER_DIALOG,
|
||||
SHOW_SUSPENDUSER_DIALOG,
|
||||
HIDE_SUSPENDUSER_DIALOG
|
||||
} from '../constants/community';
|
||||
|
||||
const initialState = Map({
|
||||
community: Map(),
|
||||
isFetching: false,
|
||||
error: '',
|
||||
commenters: [],
|
||||
field: 'created_at',
|
||||
asc: false,
|
||||
totalPages: 0,
|
||||
page: 0
|
||||
isFetchingPeople: false,
|
||||
errorPeople: '',
|
||||
accounts: [],
|
||||
fieldPeople: 'created_at',
|
||||
ascPeople: false,
|
||||
totalPagesPeople: 0,
|
||||
pagePeople: 0,
|
||||
user: Map({}),
|
||||
banDialog: false,
|
||||
suspendDialog: false
|
||||
});
|
||||
|
||||
export default function community (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case FETCH_COMMENTERS_REQUEST :
|
||||
return state
|
||||
.set('isFetching', true);
|
||||
.set('isFetchingPeople', true);
|
||||
case FETCH_COMMENTERS_FAILURE :
|
||||
return state
|
||||
.set('isFetching', false)
|
||||
.set('error', action.error);
|
||||
.set('isFetchingPeople', false)
|
||||
.set('errorPeople', action.error);
|
||||
|
||||
case FETCH_COMMENTERS_SUCCESS : {
|
||||
const {commenters, type, ...rest} = action; // eslint-disable-line
|
||||
const {accounts, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line
|
||||
return state
|
||||
.merge({
|
||||
isFetching: false,
|
||||
error: '',
|
||||
isFetchingPeople: false,
|
||||
errorPeople: '',
|
||||
pagePeople: page,
|
||||
countPeople: count,
|
||||
limitPeople: limit,
|
||||
totalPagesPeople: totalPages,
|
||||
...rest
|
||||
})
|
||||
.set('commenters', commenters); // Sets to normal array
|
||||
.set('accounts', accounts); // Sets to normal array
|
||||
}
|
||||
case SET_ROLE : {
|
||||
const commenters = state.get('commenters');
|
||||
const commenters = state.get('accounts');
|
||||
const idx = commenters.findIndex(el => el.id === action.id);
|
||||
|
||||
commenters[idx].roles[0] = action.role;
|
||||
return state.set('commenters', commenters.map(id => id));
|
||||
return state.set('accounts', commenters.map(id => id));
|
||||
}
|
||||
case SET_COMMENTER_STATUS: {
|
||||
const commenters = state.get('commenters');
|
||||
const commenters = state.get('accounts');
|
||||
const idx = commenters.findIndex(el => el.id === action.id);
|
||||
|
||||
commenters[idx].status = action.status;
|
||||
return state.set('commenters', commenters.map(id => id));
|
||||
return state.set('accounts', commenters.map(id => id));
|
||||
|
||||
}
|
||||
case SORT_UPDATE :
|
||||
return state
|
||||
.set('field', action.sort.field)
|
||||
.set('asc', !state.get('asc'));
|
||||
.set('fieldPeople', action.sort.field)
|
||||
.set('ascPeople', !state.get('ascPeople'));
|
||||
case HIDE_BANUSER_DIALOG:
|
||||
return state
|
||||
.set('banDialog', false);
|
||||
case SHOW_BANUSER_DIALOG:
|
||||
return state
|
||||
.merge({
|
||||
user: Map(action.user),
|
||||
banDialog: true
|
||||
});
|
||||
case HIDE_SUSPENDUSER_DIALOG:
|
||||
return state
|
||||
.set('suspendDialog', false);
|
||||
case SHOW_SUSPENDUSER_DIALOG:
|
||||
return state
|
||||
.merge({
|
||||
user: Map(action.user),
|
||||
suspendDialog: true
|
||||
});
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,20 @@
|
||||
"active": "Active",
|
||||
"banned": "Banned",
|
||||
"banned-user": "Banned User",
|
||||
"loading": "Loading results"
|
||||
"loading": "Loading results",
|
||||
"flaggedaccounts": "Flagged Usernames",
|
||||
"people": "People",
|
||||
"no-flagged-accounts": "The Account Flags queue is currently empty.",
|
||||
"I don't like this username": "I don't like this username",
|
||||
"This user is impersonating": "Impersonation",
|
||||
"This looks like an ad/marketing": "Spam/Ads",
|
||||
"This username is offensive": "Offensive",
|
||||
"Other": "Other",
|
||||
"ban_user": "Ban User?",
|
||||
"are_you_sure": "Are you sure you would like to ban {0}?",
|
||||
"note": "Note: Banning this user will not let them edit, comment or remove anything.",
|
||||
"cancel": "Cancel",
|
||||
"yes_ban_user": "Yes, Ban User"
|
||||
},
|
||||
"modqueue": {
|
||||
"likes": "likes",
|
||||
@@ -103,7 +116,8 @@
|
||||
"yes_ban_user": "Yes, Ban User"
|
||||
},
|
||||
"suspenduser": {
|
||||
"title_0": "We noticed you rejected a {0}",
|
||||
"title": "Suspend a user",
|
||||
"title_0": "We noticed you rejected a username",
|
||||
"description_0": "Would you like to temporarily ban this user becuase of their {0}? Doing so will temporarily hide their comments until they rewrite their {0}.",
|
||||
"title_1": "Notify the user of their temporary suspension",
|
||||
"description_1": "Suspending this user will temporarily disable their account and hide all of their comments on the site.",
|
||||
@@ -113,7 +127,7 @@
|
||||
"bio": "bio",
|
||||
"username": "username",
|
||||
"email_subject": "Your account has been suspended",
|
||||
"email": "Another member of the community recently flagged your {0} for review. Because of its content your {0} was rejected. This means you can no longer comment, like, or flag content until you rewrite your {0}. Please e-mail moderator@newsorg.com if you have any questions or concerns.",
|
||||
"email": "Another member of the community recently flagged your username for review. Because of its content your user was rejected. This means you can no longer comment, like, or flag content until you rewrite your {0}. Please e-mail moderator@newsorg.com if you have any questions or concerns.",
|
||||
"write_message": "Write a message"
|
||||
},
|
||||
"dashboard": {
|
||||
@@ -157,7 +171,34 @@
|
||||
"active": "Activa",
|
||||
"banned": "Suspendido",
|
||||
"banned-user": "Usuario Suspendido",
|
||||
"loading": "Cargando resultados"
|
||||
"loading": "Cargando resultados",
|
||||
"flaggedaccounts": "Nombres de Usuario Reportados",
|
||||
"people": "Gente",
|
||||
"no-flagged-accounts": "No hay ninguna cuenta reportada.",
|
||||
"I don't like this username": "No me gusta ese nombre de usuario",
|
||||
"This user is impersonating": "Suplantación",
|
||||
"This looks like an ad/marketing": "Spam/Propaganda",
|
||||
"This username is offensive": "Ofensivo",
|
||||
"Other": "Otros",
|
||||
"ban_user": "Quieres suspender el Usuario?",
|
||||
"are_you_sure": "Estas segura que quieres suspender a {0}?",
|
||||
"note": "Nota: Suspender a este usuario no le va a permitir borrar ni editar ni comentar.",
|
||||
"cancel": "Cancelar",
|
||||
"yes_ban_user": "Si, Suspendan el usuario"
|
||||
},
|
||||
"suspenduser": {
|
||||
"title": "Suspendiendo un usuario",
|
||||
"title_0": "Esta queriendo suspender un usuario?",
|
||||
"description_0": "Le gustaria suspender a esta usuaria temporarianmente por su nombre de usuario? Si lo hace sus comentarios serán escondidos temporariamente hasta que puedan reescribir su nombre de usuario.",
|
||||
"title_1": "Enviarle una nota al usuario sobre su cuenta suspendida",
|
||||
"description_1": "Si suspende a este usuario, su cuenta va a ser deshabilitada y todos sus comentarios escondidos del sitio.",
|
||||
"no_cancel": "No, cancelar",
|
||||
"yes_suspend": "Si, suspender",
|
||||
"send": "Enviar",
|
||||
"username": "nombre de usuario",
|
||||
"email_subject": "Su cuenta ha sido suspendida temporariamente",
|
||||
"email": "Otra persona de la comunidad recientemente marcó su nombre de usuario para ser revisado. Por su contenido, el nombre de usuario ha sido rechazado. Esto quiere decir que no puede comentar, gustar o marcar contenido hasta que modifique su nombre de usuario. Por favor, envienos un correo a moderator@newsorg.com si tiene alguna pregunta o preocupación",
|
||||
"write_message": "Escribir un mensaje"
|
||||
},
|
||||
"modqueue": {
|
||||
"likes": "gustos",
|
||||
|
||||
@@ -23,14 +23,14 @@ const getPopupMenu = [
|
||||
{val: 'This comment is offensive', text: lang.t('comment-offensive')},
|
||||
{val: 'This looks like an ad/marketing', text: lang.t('marketing')},
|
||||
{val: 'I don\'t agree with this comment', text: lang.t('no-agree-comment')},
|
||||
{val: 'other', text: lang.t('other')}
|
||||
{val: 'Other', text: lang.t('other')}
|
||||
]
|
||||
: [
|
||||
{val: 'This username is offensive', text: lang.t('username-offensive')},
|
||||
{val: 'I don\'t like this username', text: lang.t('no-like-username')},
|
||||
{val: 'This user is impersonating', text: lang.t('user-impersonating')},
|
||||
{val: 'This looks like an ad/marketing', text: lang.t('marketing')},
|
||||
{val: 'other', text: lang.t('other')}
|
||||
{val: 'Other', text: lang.t('other')}
|
||||
];
|
||||
return {
|
||||
header: lang.t('step-2-header'),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import {Icon as IconMDL} from 'react-mdl';
|
||||
|
||||
const Icon = ({className, name}) => (
|
||||
const Icon = ({className = '', name}) => (
|
||||
<IconMDL className={className} name={name} />
|
||||
);
|
||||
|
||||
|
||||
@@ -3,11 +3,51 @@ const DataLoader = require('dataloader');
|
||||
const util = require('./util');
|
||||
|
||||
const UsersService = require('../../services/users');
|
||||
const UserModel = require('../../models/user');
|
||||
|
||||
const genUserByIDs = (context, ids) => UsersService
|
||||
.findByIdArray(ids)
|
||||
.then(util.singleJoinBy(ids, 'id'));
|
||||
|
||||
/**
|
||||
* Retrieves users based on the passed in query that is filtered by the
|
||||
* current used passed in via the context.
|
||||
* @param {Object} context graph context
|
||||
* @param {Object} query query terms to apply to the users query
|
||||
*/
|
||||
const getUsersByQuery = ({user}, {ids, limit, cursor, sort}) => {
|
||||
|
||||
let users = UserModel.find();
|
||||
|
||||
if (ids) {
|
||||
users = users.find({
|
||||
id: {
|
||||
$in: ids
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (cursor) {
|
||||
if (sort === 'REVERSE_CHRONOLOGICAL') {
|
||||
users = users.where({
|
||||
created_at: {
|
||||
$lt: cursor
|
||||
}
|
||||
});
|
||||
} else {
|
||||
users = users.where({
|
||||
created_at: {
|
||||
$gt: cursor
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return users
|
||||
.sort({created_at: sort === 'REVERSE_CHRONOLOGICAL' ? -1 : 1})
|
||||
.limit(limit);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
@@ -15,6 +55,7 @@ const genUserByIDs = (context, ids) => UsersService
|
||||
*/
|
||||
module.exports = (context) => ({
|
||||
Users: {
|
||||
getByQuery: (query) => getUsersByQuery(context, query),
|
||||
getByID: new DataLoader((ids) => genUserByIDs(context, ids))
|
||||
}
|
||||
});
|
||||
|
||||
+18
-10
@@ -6,18 +6,26 @@ const setUserStatus = ({user}, {id, status}) => {
|
||||
.then(res => res);
|
||||
};
|
||||
|
||||
module.exports = (context) => {
|
||||
if (context.user && context.user.can('mutation:setUserStatus')) {
|
||||
return {
|
||||
User: {
|
||||
setUserStatus: (action) => setUserStatus(context, action)
|
||||
}
|
||||
};
|
||||
}
|
||||
const suspendUser = ({user}, {id}) => {
|
||||
return UsersService.suspendUser(id)
|
||||
.then(res => res);
|
||||
};
|
||||
|
||||
return {
|
||||
module.exports = (context) => {
|
||||
let mutators = {
|
||||
User: {
|
||||
setUserStatus: () => Promise.reject(errors.ErrNotAuthorized)
|
||||
setUserStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
suspendUser: () => Promise.reject(errors.ErrNotAuthorized)
|
||||
}
|
||||
};
|
||||
|
||||
if (context.user && context.user.can('mutation:setUserStatus')) {
|
||||
mutators.User.setUserStatus = (action) => setUserStatus(context, action);
|
||||
}
|
||||
|
||||
if (context.user && context.user.can('mutation:suspendUser')) {
|
||||
mutators.User.suspendUser = (action) => suspendUser(context, action);
|
||||
}
|
||||
|
||||
return mutators;
|
||||
};
|
||||
|
||||
@@ -47,6 +47,9 @@ const RootMutation = {
|
||||
setUserStatus(_, {id, status}, {mutators: {User}}) {
|
||||
return wrapResponse(null)(User.setUserStatus({id, status}));
|
||||
},
|
||||
suspendUser(_, {id}, {mutators: {User}}) {
|
||||
return wrapResponse(null)(User.suspendUser({id}));
|
||||
},
|
||||
setCommentStatus(_, {id, status}, {mutators: {Comment}}) {
|
||||
return wrapResponse(null)(Comment.setCommentStatus({id, status}));
|
||||
},
|
||||
|
||||
@@ -82,6 +82,28 @@ const RootQuery = {
|
||||
}
|
||||
|
||||
return user;
|
||||
},
|
||||
|
||||
// This endpoint is used for loading the user moderation queues (users whose username has been flagged),
|
||||
// so hide it in the event that we aren't an admin.
|
||||
users(_, {query: {action_type, limit, cursor, sort}}, {user, loaders: {Users, Actions}}) {
|
||||
|
||||
if (user == null || !user.hasRoles('ADMIN')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = {limit, cursor, sort};
|
||||
|
||||
if (action_type) {
|
||||
return Actions.getByTypes({action_type, item_type: 'USERS'})
|
||||
.then((ids) => {
|
||||
|
||||
// Perform the query using the available resolver.
|
||||
return Users.getByQuery({ids, limit, cursor, sort});
|
||||
});
|
||||
}
|
||||
|
||||
return Users.getByQuery(query);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+34
-3
@@ -31,7 +31,7 @@ type User {
|
||||
username: String!
|
||||
|
||||
# Action summaries against the user.
|
||||
action_summaries: [ActionSummary]
|
||||
action_summaries: [FlagActionSummary]
|
||||
|
||||
# Actions completed on the parent.
|
||||
actions: [Action]
|
||||
@@ -45,6 +45,9 @@ type User {
|
||||
# returns all comments based on a query.
|
||||
comments(query: CommentsQuery): [Comment]
|
||||
|
||||
# returns all users based on a query.
|
||||
users(query: UsersQuery): [User]
|
||||
|
||||
# returns user status
|
||||
status: USER_STATUS
|
||||
}
|
||||
@@ -60,6 +63,20 @@ type Tag {
|
||||
created_at: Date!
|
||||
}
|
||||
|
||||
# UsersQuery allows the ability to query users by a specific fields.
|
||||
input UsersQuery {
|
||||
action_type: ACTION_TYPE
|
||||
|
||||
# Limit the number of results to be returned.
|
||||
limit: Int = 10
|
||||
|
||||
# Skip results from the last created_at timestamp.
|
||||
cursor: Date
|
||||
|
||||
# Sort the results by created_at.
|
||||
sort: SORT_ORDER = REVERSE_CHRONOLOGICAL
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Comments
|
||||
################################################################################
|
||||
@@ -501,6 +518,9 @@ type RootQuery {
|
||||
# role.
|
||||
me: User
|
||||
|
||||
# Users returned based on a query.
|
||||
users(query: UsersQuery): [User]
|
||||
|
||||
# Asset metrics related to user actions are saturated into the assets
|
||||
# returned.
|
||||
assetMetrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Asset!]
|
||||
@@ -634,6 +654,14 @@ type SetUserStatusResponse implements Response {
|
||||
errors: [UserError]
|
||||
}
|
||||
|
||||
# SuspendUserResponse is the response returned with possibly some errors
|
||||
# relating to the suspend action attempt.
|
||||
type SuspendUserResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError]
|
||||
}
|
||||
|
||||
# SetCommentStatusResponse is the response returned with possibly some errors
|
||||
# relating to the delete action attempt.
|
||||
type SetCommentStatusResponse implements Response {
|
||||
@@ -646,14 +674,14 @@ type SetCommentStatusResponse implements Response {
|
||||
type AddCommentTagResponse implements Response {
|
||||
# An array of errors relating to the mutation that occured.
|
||||
comment: Comment
|
||||
errors: [UserError]
|
||||
errors: [UserError]
|
||||
}
|
||||
|
||||
# Response to removeCommentTag mutation
|
||||
type RemoveCommentTagResponse implements Response {
|
||||
# An array of errors relating to the mutation that occured.
|
||||
comment: Comment
|
||||
errors: [UserError]
|
||||
errors: [UserError]
|
||||
}
|
||||
|
||||
# All mutations for the application are defined on this object.
|
||||
@@ -677,6 +705,9 @@ type RootMutation {
|
||||
# Sets User status. Requires the `ADMIN` role.
|
||||
setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse
|
||||
|
||||
# Sets User status to BANNED and canEditName to true. Requires the `ADMIN` role.
|
||||
suspendUser(id: ID!): SuspendUserResponse
|
||||
|
||||
# Sets Comment status. Requires the `ADMIN` role.
|
||||
setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse
|
||||
|
||||
|
||||
+3
-2
@@ -156,9 +156,10 @@ const USER_GRAPH_OPERATIONS = [
|
||||
'mutation:deleteAction',
|
||||
'mutation:editName',
|
||||
'mutation:setUserStatus',
|
||||
'mutation:suspendUser',
|
||||
'mutation:setCommentStatus',
|
||||
'mutation:addCommentTag',
|
||||
'mutation:removeCommentTag',
|
||||
'mutation:removeCommentTag'
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -174,7 +175,7 @@ UserSchema.method('can', function(...actions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:setCommentStatus') && !this.hasRoles('ADMIN')) {
|
||||
if (actions.some((action) => action === 'mutation:setUserStatus' || action === 'mutation:suspendUser' || action === 'mutation:setCommentStatus') && !this.hasRoles('ADMIN')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -378,6 +378,22 @@ module.exports = class UsersService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend a user. It changes the status to BANNED and canEditName to True.
|
||||
* @param {String} id id of a user
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
static suspendUser(id) {
|
||||
return UserModel.update({
|
||||
id
|
||||
}, {
|
||||
$set: {
|
||||
status: 'BANNED',
|
||||
canEditName: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a user with the id.
|
||||
* @param {String} id user id (uuid)
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('graph.mutations.addCommentTag', () => {
|
||||
}
|
||||
expect(response.errors).to.be.empty;
|
||||
expect(response.data.addCommentTag.errors).to.deep.equal([{'translation_key':'NOT_AUTHORIZED'}]);
|
||||
expect(response.data.addCommentTag.comment).to.be.null;
|
||||
expect(response.data.addCommentTag.comment).to.be.null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user