Adds graphql for users flagged in the server and client.

This commit is contained in:
gaba
2017-02-28 21:33:01 -08:00
parent ca4b03872b
commit da3c812dc1
11 changed files with 378 additions and 67 deletions
+4 -29
View File
@@ -7,10 +7,7 @@ import {
SORT_UPDATE,
COMMENTERS_NEW_PAGE,
SET_ROLE,
SET_COMMENTER_STATUS,
FETCH_FLAGGED_COMMENTERS_REQUEST,
FETCH_FLAGGED_COMMENTERS_SUCCESS,
FETCH_FLAGGED_COMMENTERS_FAILURE
SET_COMMENTER_STATUS
} from '../constants/community';
import coralApi from '../../../coral-framework/helpers/response';
@@ -18,7 +15,7 @@ import coralApi from '../../../coral-framework/helpers/response';
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,
accounts: result,
@@ -26,8 +23,8 @@ export const fetchAccounts = (query = {}) => dispatch => {
count,
limit,
totalPages
})
)
});
})
.catch(error => dispatch({type: FETCH_COMMENTERS_FAILURE, error}));
};
@@ -58,25 +55,3 @@ export const setCommenterStatus = (id, status) => (dispatch) => {
return dispatch({type: SET_COMMENTER_STATUS, id, status});
});
};
// Fetch flagged accounts to display in the moderation queue of the community.
export const fetchFlaggedAccounts = (query = {}) => dispatch => {
dispatch(requestFetchFlaggedAccounts());
coralApi(`/users?${qs.stringify(query)}`)
.then(({result, page, count, limit, totalPages}) =>
dispatch({
type: FETCH_FLAGGED_COMMENTERS_SUCCESS,
flaggedAccounts: result,
page,
count,
limit,
totalPages
})
)
.catch(error => dispatch({type: FETCH_FLAGGED_COMMENTERS_FAILURE, error}));
};
const requestFetchFlaggedAccounts = () => ({
type: FETCH_FLAGGED_COMMENTERS_REQUEST
});
@@ -4,6 +4,7 @@ import {
fetchAccounts,
updateSorting,
newPage,
fetchFlaggedAccounts,
} from '../../actions/community';
import CommunityMenu from './components/CommunityMenu';
@@ -12,11 +13,16 @@ import People from './People';
import FlaggedAccounts from './FlaggedAccounts';
class CommunityContainer extends Component {
// static propTypes = {
//
// // list of actions (approve, reject, ban) associated with the users
// modActions: PropTypes.arrayOf(PropTypes.string).isRequired,
// }
constructor(props) {
super(props);
console.log('DEBUG CONSTRUCTOR CommunityContainer ', props);
this.state = {
searchValue: ''
};
@@ -49,6 +55,11 @@ class CommunityContainer extends Component {
asc: community.ascPeople,
...query
}));
}
componentWillMount() {
this.props.dispatch(fetchFlaggedAccounts());
}
componentDidMount() {
@@ -88,8 +99,6 @@ class CommunityContainer extends Component {
commenters={community.flaggedAccounts}
isFetching={community.isFetchingFlagged}
error={community.errorFlagged}
totalPages={community.totalPagesFlagged}
page={community.pageFlagged}
{...this}
/>
);
@@ -6,18 +6,28 @@ const lang = new I18n(translations);
import styles from './Community.css';
// import Loading from './Loading';
import Loading from './Loading';
import EmptyCard from '../../components/EmptyCard';
import User from './components/User';
// actions={commenter.actions}
const FlaggedAccounts = ({...props}) => {
const {commenters, isFetching, error, totalPages, page} = props;
const hasResults = !isFetching && !!commenters.length;
const {commenters, isFetching} = props;
const hasResults = !isFetching && commenters && !!commenters.length;
console.log('debug props', props);
console.log('debug commenters', commenters);
console.log('debug error', error);
console.log('debug totalPages', totalPages);
console.log('debug page', page);
// const menuOptions = {
// 'reject': {status: 'REJECTED', icon: 'close', key: 'r'},
// 'approve': {status: 'ACCEPTED', icon: 'done', key: 't'},
// 'ban': {status: 'BANNED', icon: 'not interested'}
// };
//
//
// onClickAction={this.onClickAction}
// onClickShowBanDialog={this.onClickShowBanDialog}
// acceptCommenter={props.acceptCommenter}
// rejectCommenter={props.rejectCommenter}
// menuOptions={menuOptions}
return (
<div className={styles.container}>
@@ -25,7 +35,12 @@ const FlaggedAccounts = ({...props}) => {
{ isFetching && <Loading /> }
{
hasResults
? <div></div>
? commenters.map((commenter, index) => {
return <User
user={commenter}
key={index}
index={index} />;
})
: <EmptyCard>{lang.t('community.no-flagged-accounts')}</EmptyCard>
}
</div>
@@ -0,0 +1,187 @@
@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;
&: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: block;
.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,15 +1,17 @@
import React from 'react';
import styles from './ModerationList.css';
import styles from '../UserModerationList.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
import translations from '../../../translations.json';
import {Icon} from 'react-mdl';
import ActionButton from './ActionButton';
const lang = new I18n(translations);
// Render a single comment for the list
// import {Icon} from 'react-mdl';
// import ActionButton from './ActionButton';
// Render a single user for the list
const User = props => {
const {action, user} = props;
const {user} = props;
let userStatus = user.status;
// Do not display unless the user status is 'pending' or 'banned'.
@@ -19,32 +21,45 @@ const User = props => {
<div className={styles.itemHeader}>
<div className={styles.author}>
<span>{user.username}</span>
</div>
</div>
</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}`}>
{props.modActions.map(
{/* props.modActions.map(
(action, i) =>
<ActionButton
return <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);
@@ -2,6 +2,7 @@ import {graphql} from 'react-apollo';
import MOST_FLAGS from './mostFlags.graphql';
import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
import USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql';
export const mostFlags = graphql(MOST_FLAGS, {
options: () => {
@@ -28,3 +29,5 @@ export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
};
}
});
export const modUserFlaggedQuery = graphql(USER_FLAGGED_QUERY);
@@ -0,0 +1,19 @@
query Users {
usersFlagged {
id
username
status
roles
actions{
id
reason
user {
username
}
}
action_summaries {
count
reason
}
}
}
+12 -4
View File
@@ -14,9 +14,13 @@
"banned": "Banned",
"banned-user": "Banned User",
"loading": "Loading results",
"flaggedaccounts": "Account Flags",
"flaggedaccounts": "Flagged Usernames",
"people": "People",
"no-flagged-accounts": "The Account Flags queue is currently empty."
"no-flagged-accounts": "The Account Flags queue is currently empty.",
"This user is impersonating": "Impersonation",
"This looks like an ad/marketing": "Spam/Ads",
"This username is offensive": "Offensive",
"Other": "Other"
},
"modqueue": {
"likes": "likes",
@@ -149,9 +153,13 @@
"banned": "Suspendido",
"banned-user": "Usuario Suspendido",
"loading": "Cargando resultados",
"flaggedaccounts": "Cuentas Reportadas",
"flaggedaccounts": "Nombres de Usuario Reportados",
"people": "Gente",
"no-flagged-accounts": "No hay ninguna cuenta reportada."
"no-flagged-accounts": "No hay ninguna cuenta reportada.",
"This user is impersonating": "Suplantación",
"This looks like an ad/marketing": "Spam/Propaganda",
"This username is offensive": "Ofensivo",
"Other": "Otros"
},
"modqueue": {
"likes": "gustos",
+48
View File
@@ -3,11 +3,58 @@ 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();
// Only administrators can search for users
if (user == null || !user.hasRoles('ADMIN')) {
return null;
}
users = users.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 +62,7 @@ const genUserByIDs = (context, ids) => UsersService
*/
module.exports = (context) => ({
Users: {
getByQuery: (query) => getUsersByQuery(context, query),
getByID: new DataLoader((ids) => genUserByIDs(context, ids))
}
});
+16
View File
@@ -72,6 +72,22 @@ 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.
usersFlagged(_, args, {user, loaders: {Users, Actions}}) {
if (user == null || !user.hasRoles('ADMIN')) {
return null;
}
return Actions.getByTypes({action_type: 'FLAG', item_type: 'USERS'})
.then((ids) => {
// Perform the query using the available resolver.
return Users.getByQuery({ids});
});
}
};
+18 -2
View File
@@ -31,10 +31,10 @@ type User {
username: String!
# Action summaries against the user.
action_summaries: [ActionSummary]
action_summaries: [FlagActionSummary]
# Actions completed on the parent.
actions: [Action]
actions: [FlagAction]
# the current roles of the user.
roles: [USER_ROLES]
@@ -60,6 +60,19 @@ type Tag {
created_at: Date!
}
# UsersQuery allows the ability to query users by a specific methods.
input UsersQuery {
# 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
################################################################################
@@ -497,6 +510,9 @@ type RootQuery {
# Metrics related to user actions are saturated into the assets returned. The
# sort will affect if it will allow
metrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Asset]
# Users returned based on a query.
usersFlagged(query: UsersQuery): [User]
}
################################################################################