Merge pull request #903 from coralproject/paginate-flagged-users

Paginate flagged users and more
This commit is contained in:
Kim Gardner
2017-08-31 08:42:00 +01:00
committed by GitHub
31 changed files with 739 additions and 708 deletions
@@ -54,8 +54,9 @@ class SuspendUserDialogContainer extends Component {
const withOrganizationName = withQuery(gql`
query CoralAdmin_SuspendUserDialog {
__typename
settings {
organizationName
organizationName
}
}
`);
+27 -4
View File
@@ -1,10 +1,33 @@
import update from 'immutability-helper';
export default {
mutations: {
SetUserStatus: () => ({
refetchQueries: ['CoralAdmin_Community'],
SetUserStatus: ({variables: {status, userId}}) => ({
updateQueries: {
TalkAdmin_FlaggedAccounts: (prev) => {
if (status !== 'APPROVED') {
return prev;
}
const updated = update(prev, {
users: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
},
});
return updated;
}
}
}),
RejectUsername: () => ({
refetchQueries: ['CoralAdmin_Community'],
RejectUsername: ({variables: {input: {id: userId}}}) => ({
updateQueries: {
TalkAdmin_FlaggedAccounts: (prev) => {
const updated = update(prev, {
users: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
},
});
return updated;
}
}
}),
},
};
@@ -0,0 +1,4 @@
.actionButton {
transform: scale(.8);
margin: 0;
}
@@ -1,10 +1,11 @@
import React from 'react';
import styles from './Community.css';
import styles from './ActionButton.css';
import {Button} from 'coral-ui';
import {menuActionsMap} from '../../../utils/moderationQueueActionsMap';
import t from 'coral-framework/services/i18n';
// TODO: Needs refactoring.
const ActionButton = ({type = '', user, ...props}) => {
return (
<Button
@@ -1,299 +0,0 @@
@custom-media --big-viewport (min-width: 780px);
.container {
padding: 10px;
display: flex;
padding-bottom: 200px;
}
.leftColumn {
padding: 42px 56px;
width: 234px;
}
.mainContent {
width: calc(100% - 300px);
padding: 34px 14px;
box-sizing: border-box;
}
.mainFlaggedContent {
width: 100%;
padding: 34px 14px;
box-sizing: border-box;
}
.roleButton {
display: block;
}
.searchBox {
width: 100%;
padding: 9px;
border: 1px solid #ccc;
border-radius: 2px;
display: flex;
background: white;
box-sizing: border-box;
height: 40px;
i {
color: #A1A1A1
}
input {
display: block;
width: 100%;
height: 100%;
border: none;
font-size: 16px;
padding: 0 2px 0 15px;
box-sizing: border-box;
}
}
.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: 18px;
width: 100%;
max-width: 660px;
min-width: 400px;
margin: 0 auto;
position: relative;
transition: all 200ms;
padding: 10px 0 0;
min-height: 220px;
.container {
display: block;
padding: 0 14px;
min-height: 180px;
}
&: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: 50px 18px;
box-sizing: border-box;
}
.itemHeader {
display: flex;
align-items: center;
justify-content: space-between;
.author {
font-size: 16px;
font-weight: bold;
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;
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;
}
.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;
margin-top: 5px;
}
.flaggedByCount i {
font-size: 14px;
margin-right: 10px;
}
.flaggedBy {
display: inline;
padding: 3px;
font-size: 14px;
margin-left: 5px;
}
.flaggedByLabel {
font-weight: 600;
font-size: 14px;
}
.flaggedReasons {
margin-left: 24px;
margin-top: 10px;
}
p.flaggedByReason {
font-size: 1tpx;
margin: 0px;
line-height: 1.4;
}
.button {
composes: buttonReset from 'coral-framework/styles/reset.css';
vertical-align: text-bottom;
&:hover {
background-color: #E0E0E0;
}
}
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
import CommunityMenu from './CommunityMenu';
import People from './People';
import FlaggedAccounts from './FlaggedAccounts';
import FlaggedAccounts from '../containers/FlaggedAccounts';
import RejectUsernameDialog from './RejectUsernameDialog';
export default class Community extends Component {
@@ -54,7 +54,7 @@ export default class Community extends Component {
}
getTabContent(searchValue, props) {
const {community, root: {users}, viewUserDetail} = props;
const {community} = props;
const activeTab = props.route.path === ':id' ? 'flagged' : props.route.path;
if (activeTab === 'people') {
@@ -76,16 +76,7 @@ export default class Community extends Component {
return (
<div>
<FlaggedAccounts
commenters={users}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
showRejectUsernameDialog={props.showRejectUsernameDialog}
approveUser={props.approveUser}
rejectUsername={props.rejectUsername}
currentUser={this.props.currentUser}
viewUserDetail={viewUserDetail}
/>
<FlaggedAccounts />
<RejectUsernameDialog
open={community.rejectUsernameDialog}
handleClose={props.hideRejectUsernameDialog}
@@ -0,0 +1,21 @@
.tab {
flex: 1;
color: white;
text-transform: capitalize;
font-weight: 500;
font-size: 15px;
letter-spacing: 1px;
transition: border-bottom 200ms;
}
.tabBar {
background-color: rgba(44, 44, 44, 0.89);
z-index: 5;
}
.active {
color: white;
box-sizing: border-box;
border-bottom: solid 5px #F36451;
}
@@ -1,6 +1,6 @@
import React from 'react';
import styles from './styles.css';
import styles from './CommunityMenu.css';
import t from 'coral-framework/services/i18n';
import {Link} from 'react-router';
@@ -0,0 +1,34 @@
.container {
padding: 10px;
display: flex;
padding-bottom: 200px;
}
.mainFlaggedContent {
width: 100%;
padding: 34px 14px;
box-sizing: border-box;
}
.list {
margin: 0;
padding: 0;
}
.userLeave {
opacity: 1.0;
}
.userLeaveActive {
opacity: 0;
transition: opacity 800ms;
}
.userEnter {
opacity: 0;
}
.userEnterActive {
opacity: 1.0;
transition: opacity 800ms;
}
@@ -1,38 +1,74 @@
import React from 'react';
import t from 'coral-framework/services/i18n';
import styles from './Community.css';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
import User from './User';
import LoadMore from '../../../components/LoadMore';
import FlaggedUser from '../containers/FlaggedUser';
import {CSSTransitionGroup} from 'react-transition-group';
import styles from './FlaggedAccounts.css';
const FlaggedAccounts = (props) => {
const {commenters} = props;
const hasResults = commenters && !!commenters.length;
class FlaggedAccounts extends React.Component {
render() {
const {
users,
loadMore,
showBanUserDialog,
showSuspendUserDialog,
showRejectUsernameDialog,
approveUser,
me,
viewUserDetail,
} = this.props;
return (
<div className={styles.container}>
<div className={styles.mainFlaggedContent}>
{
hasResults
? commenters.map((commenter, index) => {
return <User
user={commenter}
key={index}
index={index}
modActionButtons={['APPROVE', 'REJECT']}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
showRejectUsernameDialog={props.showRejectUsernameDialog}
approveUser={props.approveUser}
currentUser={props.currentUser}
viewUserDetail={props.viewUserDetail}
/>;
})
: <EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
}
const hasResults = users.nodes && !!users.nodes.length;
return (
<div className={styles.container}>
<div className={styles.mainFlaggedContent}>
{
hasResults
? <CSSTransitionGroup
component={'ul'}
className={styles.list}
transitionName={{
enter: styles.userEnter,
enterActive: styles.userEnterActive,
leave: styles.userLeave,
leaveActive: styles.userLeaveActive,
}}
transitionEnter={true}
transitionLeave={true}
transitionEnterTimeout={1000}
transitionLeaveTimeout={1000}
>
{
users.nodes.map((user) => {
return (
<FlaggedUser
user={user}
key={user.id}
modActionButtons={['APPROVE', 'REJECT']}
showBanUserDialog={showBanUserDialog}
showSuspendUserDialog={showSuspendUserDialog}
showRejectUsernameDialog={showRejectUsernameDialog}
approveUser={approveUser}
me={me}
viewUserDetail={viewUserDetail}
/>
);
})
}
</CSSTransitionGroup>
: <EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
}
<LoadMore
loadMore={loadMore}
showLoadMore={users.hasNextPage}
/>
</div>
</div>
</div>
);
};
);
}
}
export default FlaggedAccounts;
@@ -0,0 +1,104 @@
@custom-media --big-viewport (min-width: 780px);
.root {
composes: mdl-shadow--2dp mdl-card from global;
border-bottom: 1px solid #e0e0e0;
font-size: 18px;
width: 100%;
max-width: 660px;
min-width: 400px;
margin: 0 auto;
position: relative;
transition: box-shadow 200ms, margin-bottom 200ms;
padding: 10px 0 0;
min-height: 220px;
&:hover {
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
}
.rootSelected {
composes: mdl-shadow--16dp from global;
}
.container {
display: block;
padding: 0 14px;
min-height: 180px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.author {
display: flex;
justify-content: flex-start;
}
.body {
display: flex;
align-items: center;
justify-content: space-between;
}
.flaggedByCount {
display: block;
text-align: left;
margin-top: 5px;
}
.flagIcon {
font-size: 14px;
margin-right: 10px;
}
.flaggedBy {
display: inline;
padding: 3px;
font-size: 14px;
margin-left: 5px;
}
.flaggedByLabel {
font-weight: 600;
font-size: 14px;
}
.flaggedReasons {
margin-left: 24px;
margin-top: 10px;
}
.flaggedByReason {
font-size: 1tpx;
margin: 0px;
line-height: 1.4;
}
@media (--big-viewport) {
.root {
border: 1px solid #e0e0e0;
margin-bottom: 30px;
&:last-child {
border-bottom: 1px solid #e0e0e0;
}
}
&.rootSelected {
border: 2px solid #333;
}
}
.button {
composes: buttonReset from 'coral-framework/styles/reset.css';
vertical-align: text-bottom;
&:hover {
background-color: #E0E0E0;
}
}
@@ -0,0 +1,145 @@
import React from 'react';
import styles from './FlaggedUser.css';
import ActionButton from './ActionButton';
// TODO: Should not rely on plugin.
import {username} from 'talk-plugin-flags/helpers/flagReasons';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
// TODO: Should work with custom flags too.
const shortReasons = {
[username.other]: t('community.other'),
[username.spam]: t('community.spam_ads'),
[username.offensive]: t('community.offensive'),
[username.nolike]: t('community.dont_like_username'),
[username.impersonating]: t('community.impersonating'),
};
// Render a single user for the list
class User extends React.Component {
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
userId: this.props.user.id,
username: this.props.user.username,
});
showBanUserDialog = () => this.props.showBanUserDialog({
userId: this.props.user.id,
username: this.props.user.username,
});
viewAuthorDetail = () => this.props.viewUserDetail(this.props.user.id);
render() {
const {
user,
modActionButtons,
viewUserDetail,
selected,
approveUser,
showRejectUsernameDialog,
me,
className,
} = this.props;
return (
<li
tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
>
<div className={styles.container}>
<div className={styles.header}>
<div className={styles.author}>
<button
onClick={this.viewAuthorDetail}
className={styles.button}>
{user.username}
</button>
{me.id !== user.id &&
<ActionsMenu icon="not_interested">
<ActionsMenuItem
disabled={user.status === 'BANNED'}
onClick={this.showSuspenUserDialog}>
Suspend User
</ActionsMenuItem>
<ActionsMenuItem
disabled={user.status === 'BANNED'}
onClick={this.showBanUserDialog}>
Ban User
</ActionsMenuItem>
</ActionsMenu>
}
</div>
</div>
<div className={styles.body}>
<div className={styles.flagged}>
<div className={styles.flaggedByCount}>
<i className={cn('material-icons', styles.flagIcon)}>flag</i>
<span className={styles.flaggedByLabel}>
{t('community.flags')}({ user.actions.length })
</span>:
{ user.action_summaries.map(
(action, i) => {
return <span className={styles.flaggedBy} key={i}>
{shortReasons[action.reason]} ({action.count})
</span>;
}
)}
</div>
<div className={styles.flaggedReasons}>
{ user.action_summaries.map(
(action_sum, i) => {
return <div key={i}>
<span className={styles.flaggedByLabel}>
{shortReasons[action_sum.reason]} ({action_sum.count})
</span>
{user.actions.map(
// find the action by action_sum.reason
(action, j) => {
if (action.reason === action_sum.reason) {
return <p className={styles.flaggedByReason} key={j}>
{action.user &&
<button onClick={() => {viewUserDetail(action.user.id);}} className={styles.button}>
{action.user.username}
</button>
}
: {action.message ? action.message : 'n/a'}
</p>;
}
return null;
}
)}
</div>;
}
)}
</div>
</div>
<div className={styles.sideActions}>
<div className={styles.actions}>
{modActionButtons.map((action, i) =>
<ActionButton key={i}
type={action.toUpperCase()}
user={user}
approveUser={approveUser}
showRejectUsernameDialog={showRejectUsernameDialog}
/>
)}
</div>
</div>
</div>
</div>
</li>
);
}
}
export default User;
@@ -1,6 +1,6 @@
import React from 'react';
import styles from './Community.css';
import styles from './styles.css';
import Table from '../containers/Table';
import {Pager, Icon} from 'coral-ui';
import EmptyCard from '../../../components/EmptyCard';
@@ -1,117 +0,0 @@
import React from 'react';
import styles from './Community.css';
import ActionButton from './ActionButton';
import {username} from 'talk-plugin-flags/helpers/flagReasons';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import t from 'coral-framework/services/i18n';
const shortReasons = {
[username.other]: t('community.other'),
[username.spam]: t('community.spam_ads'),
[username.offensive]: t('community.offensive'),
[username.nolike]: t('community.dont_like_username'),
[username.impersonating]: t('community.impersonating'),
};
// Render a single user for the list
const User = (props) => {
const {user, modActionButtons} = props;
let userStatus = user.status;
const showSuspenUserDialog = () => props.showSuspendUserDialog({
userId: user.id,
username: user.username,
});
const showBanUserDialog = () => props.showBanUserDialog({
userId: user.id,
username: user.username,
});
// 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 ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
<div className={styles.container}>
<div className={styles.itemHeader}>
<div className={styles.author}>
<button onClick={() => {props.viewUserDetail(user.id);}} className={styles.button}>{user.username}</button>
{props.currentUser.id !== user.id &&
<ActionsMenu icon="not_interested">
<ActionsMenuItem
disabled={user.status === 'BANNED'}
onClick={showSuspenUserDialog}>
Suspend User</ActionsMenuItem>
<ActionsMenuItem
disabled={user.status === 'BANNED'}
onClick={showBanUserDialog}>
Ban User
</ActionsMenuItem>
</ActionsMenu>
}
</div>
</div>
<div className={styles.itemBody}>
<div className={styles.body}>
<div className={styles.flaggedByCount}>
<i className="material-icons">flag</i><span className={styles.flaggedByLabel}>{t('community.flags')}({ user.actions.length })</span>:
{ user.action_summaries.map(
(action, i) => {
return <span className={styles.flaggedBy} key={i}>
{shortReasons[action.reason]} ({action.count})
</span>;
}
)}
</div>
<div className={styles.flaggedReasons}>
{ user.action_summaries.map(
(action_sum, i) => {
return <div key={i}>
<span className={styles.flaggedByLabel}>
{shortReasons[action_sum.reason]} ({action_sum.count})
</span>
{user.actions.map(
// find the action by action_sum.reason
(action, j) => {
if (action.reason === action_sum.reason) {
return <p className={styles.flaggedByReason} key={j}>
{action.user &&
<button onClick={() => {props.viewUserDetail(action.user.id);}} className={styles.button}>
{action.user.username}
</button>
}
: {action.message ? action.message : 'n/a'}
</p>;
}
return null;
}
)}
</div>;
}
)}
</div>
</div>
<div className={styles.sideActions}>
<div className={`actions ${styles.actions}`}>
{modActionButtons.map((action, i) =>
<ActionButton key={i}
type={action.toUpperCase()}
user={user}
approveUser={props.approveUser}
showRejectUsernameDialog={props.showRejectUsernameDialog}
/>
)}
</div>
</div>
</div>
</div>
</li>;
};
export default User;
@@ -1,119 +1,56 @@
/**
* deprecated as this file contains styles from multiple components. Please remove this file
* when styles have been refactored.
*/
@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;
.container {
padding: 10px;
display: flex;
align-items: center;
font-size: 13px;
span {
margin-left: 7px;
}
padding-bottom: 200px;
}
@media (--big-viewport) {
.tab {
flex: none;
}
.leftColumn {
padding: 42px 56px;
width: 234px;
}
.approve {
margin-top: 10px;
.mainContent {
width: calc(100% - 300px);
padding: 34px 14px;
box-sizing: border-box;
}
.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;
}
}
.roleButton {
display: block;
}
.header {
background-color: #2c2c2c;
color: white;
margin-bottom: -1px;
.searchBox {
width: 100%;
padding: 9px;
border: 1px solid #ccc;
border-radius: 2px;
display: flex;
background: white;
box-sizing: border-box;
height: 40px;
.settingsButton {
i {
vertical-align: middle;
margin-left: 10px;
margin-top: -4px;
}
i {
color: #A1A1A1
}
.moderateAsset {
a {
text-align: center;
-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;
}
}
input {
display: block;
width: 100%;
height: 100%;
border: none;
font-size: 16px;
padding: 0 2px 0 15px;
box-sizing: border-box;
}
}
@custom-media --big-viewport (min-width: 780px);
.list {
padding: 8px 0;
list-style: none;
@@ -134,15 +71,12 @@ span {
position: fixed;
bottom: 60px;
left: 25%;
margin: 0 auto;
display: flex;
justify-content: space-around;
width: 50%;
margin: 0;
}
.actionButton {
transform: scale(1.4);
}
}
}
@@ -159,6 +93,7 @@ span {
min-height: 220px;
.container {
display: block;
padding: 0 14px;
min-height: 180px;
}
@@ -184,9 +119,9 @@ span {
right: 0;
height: 100%;
top: 0;
padding: 40px 18px;
padding: 50px 18px;
box-sizing: border-box;
}
}
.itemHeader {
display: flex;
@@ -194,6 +129,8 @@ span {
justify-content: space-between;
.author {
font-size: 16px;
font-weight: bold;
min-width: 230px;
display: flex;
align-items: center;
@@ -221,18 +158,12 @@ span {
margin-left: 40px;
}
.actionButton {
transform: scale(.8);
margin: 0;
}
.body {
margin-top: 0px;
flex: 1;
color: black;
max-width: 500px;
word-wrap: break-word;
font-weight: 300;
}
.flagged {
@@ -245,11 +176,8 @@ span {
font-size: 12px;
color: #d32f2f;
}
}
.empty {
color: #444;
margin-top: 50px;
@@ -301,34 +229,14 @@ span {
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;
.banButton {
width: 114px;
letter-spacing: 1px;
&:hover {
text-decoration: underline;
opacity: .9;
cursor: pointer;
}
}
}
}
.flagBox {
max-width: 480px;
border-top: 1px solid rgba(66, 66, 66, 0.12);
h3 {
i {
vertical-align: middle;
margin-right: 10px;
font-size: 14px;
margin: 0;
font-weight: 500;
}
}
@@ -1,23 +1,15 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import {Spinner} from 'coral-ui';
import {compose} from 'react-apollo';
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import {
fetchAccounts,
updateSorting,
newPage,
showRejectUsernameDialog,
hideRejectUsernameDialog
} from '../../../actions/community';
import {viewUserDetail} from '../../../actions/userDetail';
import Community from '../components/Community';
@@ -27,85 +19,26 @@ class CommunityContainer extends Component {
this.props.fetchAccounts({});
}
approveUser = ({userId}) => {
return this.props.setUserStatus({userId, status: 'APPROVED'});
}
banUser = ({userId}) => {
return this.props.setUserStatus({userId, status: 'BANNED'});
}
render() {
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
}
if (!('users' in this.props.root)) {
return <div><Spinner/></div>;
}
return (
<Community {...this.props} approveUser={this.approveUser} banUser={this.banUser}/>
<Community {...this.props} />
);
}
}
export const withCommunityQuery = withQuery(gql`
query CoralAdmin_Community($action_type: ACTION_TYPE) {
users(query:{action_type: $action_type}){
id
username
status
roles
actions{
id
created_at
... on FlagAction {
reason
message
user {
id
username
}
}
}
action_summaries {
count
... on FlagActionSummary {
reason
}
}
}
}
`, {
options: ({params: {action_type = 'FLAG'}}) => {
return {
variables: {
action_type: action_type
}
};
}
});
const mapStateToProps = (state) => ({
community: state.community,
currentUser: state.auth.user,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
fetchAccounts,
showBanUserDialog,
showSuspendUserDialog,
showRejectUsernameDialog,
hideRejectUsernameDialog,
updateSorting,
newPage,
viewUserDetail,
}, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withCommunityQuery,
withSetUserStatus,
withRejectUsername,
)(CommunityContainer);
@@ -0,0 +1,128 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import {Spinner} from 'coral-ui';
import {withSetUserStatus} from 'coral-framework/graphql/mutations';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import {showRejectUsernameDialog} from '../../../actions/community';
import {viewUserDetail} from '../../../actions/userDetail';
import {getDefinitionName} from 'coral-framework/utils';
import {appendNewNodes} from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import FlaggedAccounts from '../components/FlaggedAccounts';
import FlaggedUser from '../containers/FlaggedUser';
class FlaggedAccountsContainer extends Component {
constructor(props) {
super(props);
}
approveUser = ({userId}) => {
return this.props.setUserStatus({userId, status: 'APPROVED'});
}
loadMore = () => {
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables: {
limit: 5,
cursor: this.props.root.users.endCursor,
},
updateQuery: (previous, {fetchMoreResult:{users}}) => {
const updated = update(previous, {
users: {
nodes: {
$apply: (nodes) => appendNewNodes(nodes, users.nodes),
},
hasNextPage: {$set: users.hasNextPage},
endCursor: {$set: users.endCursor},
},
});
return updated;
},
});
};
render() {
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
}
if (this.props.data.loading) {
return <div><Spinner/></div>;
}
return (
<FlaggedAccounts
showBanUserDialog={this.props.showBanUserDialog}
showSuspendUserDialog={this.props.showSuspendUserDialog}
showRejectUsernameDialog={this.props.showRejectUsernameDialog}
viewUserDetail={this.props.viewUserDetail}
approveUser={this.approveUser}
loadMore={this.loadMore}
data={this.props.data}
root={this.props.root}
users={this.props.root.users}
me={this.props.root.me}
/>
);
}
}
const LOAD_MORE_QUERY = gql`
query TalkAdmin_LoadMoreFlaggedAccounts($limit: Int, $cursor: Cursor) {
users(query:{action_type: FLAG, statuses: [PENDING], limit: $limit, cursor: $cursor}){
hasNextPage
endCursor
nodes {
__typename
...${getDefinitionName(FlaggedUser.fragments.user)}
}
}
}
${FlaggedUser.fragments.user}
`;
export const withFlaggedAccountsyQuery = withQuery(gql`
query TalkAdmin_FlaggedAccounts {
...${getDefinitionName(FlaggedUser.fragments.root)}
users(query:{action_type: FLAG, statuses: [PENDING], limit: 10}){
hasNextPage
endCursor
nodes {
__typename
...${getDefinitionName(FlaggedUser.fragments.user)}
}
}
me {
__typename
...${getDefinitionName(FlaggedUser.fragments.me)}
}
}
${FlaggedUser.fragments.root}
${FlaggedUser.fragments.user}
${FlaggedUser.fragments.me}
`, {
options: {
fetchPolicy: 'network-only',
},
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
showBanUserDialog,
showSuspendUserDialog,
showRejectUsernameDialog,
viewUserDetail,
}, dispatch);
export default compose(
connect(null, mapDispatchToProps),
withFlaggedAccountsyQuery,
withSetUserStatus,
)(FlaggedAccountsContainer);
@@ -0,0 +1,42 @@
import {gql} from 'react-apollo';
import FlaggedUser from '../components/FlaggedUser';
import {withFragments} from 'plugin-api/beta/client/hocs';
export default withFragments({
root: gql`
fragment TalkAdminCommunity_FlaggedUser_root on RootQuery {
__typename
}
`,
me: gql`
fragment TalkAdminCommunity_FlaggedUser_me on User {
id
}
`,
user: gql`
fragment TalkAdminCommunity_FlaggedUser_user on User {
id
username
status
roles
actions{
id
created_at
... on FlagAction {
reason
message
user {
id
username
}
}
}
action_summaries {
count
... on FlagActionSummary {
reason
}
}
}
`
})(FlaggedUser);
@@ -1,3 +1,7 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
*/
.container {
display: flex;
@@ -1,3 +1,7 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
*/
.Dashboard {
display: flex;
}
@@ -1,3 +1,7 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
*/
:root {
--row-height: 60px;
}
@@ -1,12 +1,12 @@
.Install {
.install {
max-width: 900px;
margin: 0 auto;
text-align: center;
padding: 50px 0;
}
h2 {
font-size: 2em;
font-weight: 500;
margin: 0;
}
.header {
font-size: 2em;
font-weight: 500;
margin: 0;
}
@@ -1,5 +1,5 @@
import React, {Component} from 'react';
import styles from './style.css';
import styles from './Install.css';
import {Wizard, WizardNav} from 'coral-ui';
import Layout from 'coral-admin/src/components/ui/Layout';
@@ -37,11 +37,11 @@ export default class Install extends Component {
return (
<Layout restricted={true}>
<div className={styles.Install}>
<div className={styles.install}>
{
!install.alreadyInstalled ? (
<div>
<h2>Welcome to the Coral Project</h2>
<h2 className={styles.header}>Welcome to the Coral Project</h2>
{ install.step !== 0 ? <WizardNav items={install.navItems} currentStep={install.step} icon='check'/> : null }
<Wizard
currentStep={install.step}
@@ -1,3 +1,8 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please remove this file
* when styles have been refactored.
*/
.step {
padding: 20px 0;
@@ -67,7 +67,7 @@ class Comment extends React.Component {
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
const commentType = getCommentType(comment);
let selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
const queryData = {root, comment, asset: comment.asset};
@@ -1,3 +1,8 @@
/**
* @TODO: deprecated as this file contains styles from multiple components. Please remove this file
* when styles have been refactored.
*/
@custom-media --big-viewport (min-width: 780px);
.listContainer {
@@ -444,20 +449,20 @@ span {
.commentLeave {
opacity: 1.0;
transition: opacity 800ms;
}
.commentLeaveActive {
opacity: 0;
transition: opacity 800ms;
}
.commentEnter {
opacity: 0;
transition: opacity 800ms;
}
.commentEnterActive {
opacity: 1.0;
transition: opacity 800ms;
}
.editedMarker {
@@ -1,3 +1,8 @@
/**
* @TODO: deprecated as default styles should be included in the components.
* Please remove this file when styles have been refactored.
*/
* {
font-family: inherit;
font-style: inherit;
+43 -10
View File
@@ -1,6 +1,7 @@
const DataLoader = require('dataloader');
const util = require('./util');
const union = require('lodash/union');
const UsersService = require('../../services/users');
const UserModel = require('../../models/user');
@@ -26,20 +27,25 @@ const genUserByIDs = async (context, ids) => {
* @param {Object} context graph context
* @param {Object} query query terms to apply to the users query
*/
const getUsersByQuery = (ctx, {ids, limit, cursor, statuses = null, sortOrder}) => {
const getUsersByQuery = async ({user, loaders: {Actions}}, {ids, limit, cursor, statuses, action_type, sortOrder}) => {
let users = UserModel.find();
let query = UserModel.find();
if (action_type) {
const userIds = await Actions.getByTypes({action_type, item_type: 'USERS'});
ids = ids ? union(ids, userIds) : userIds;
}
if (ids) {
users = users.find({
query = query.find({
id: {
$in: ids
}
});
}
if (statuses != null) {
users = users.where({
if (statuses) {
query = query.where({
status: {
$in: statuses
}
@@ -48,13 +54,13 @@ const getUsersByQuery = (ctx, {ids, limit, cursor, statuses = null, sortOrder})
if (cursor) {
if (sortOrder === 'DESC') {
users = users.where({
query = query.where({
created_at: {
$lt: cursor
}
});
} else {
users = users.where({
query = query.where({
created_at: {
$gt: cursor
}
@@ -62,9 +68,36 @@ const getUsersByQuery = (ctx, {ids, limit, cursor, statuses = null, sortOrder})
}
}
return users
.sort({created_at: sortOrder === 'DESC' ? -1 : 1})
.limit(limit);
// Apply the limit.
if (limit) {
query = query.limit(limit + 1);
}
// Sort by created_at.
query.sort({created_at: sortOrder === 'DESC' ? -1 : 1});
const nodes = await query.exec();
// The hasNextPage is always handled the same (ask for one more than we need,
// if there is one more, than there is more).
let hasNextPage = false;
if (limit && nodes.length > limit) {
// There was one more than we expected! Set hasNextPage = true and remove
// the last item from the array that we requested.
hasNextPage = true;
nodes.splice(limit, 1);
}
const startCursor = nodes.length ? nodes[0].created_at : null;
const endCursor = nodes.length ? nodes[nodes.length - 1].created_at : null;
return {
startCursor,
endCursor,
hasNextPage,
nodes,
};
};
/**
+1 -7
View File
@@ -92,17 +92,11 @@ const RootQuery = {
// 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.
async users(_, {query}, {user, loaders: {Users, Actions}}) {
async users(_, {query}, {user, loaders: {Users}}) {
if (user == null || !user.can(SEARCH_OTHER_USERS)) {
return null;
}
const {action_type} = query;
if (action_type) {
query.ids = await Actions.getByTypes({action_type, item_type: 'USERS'});
query.statuses = ['PENDING'];
}
return Users.getByQuery(query);
}
};
+3 -2
View File
@@ -51,7 +51,7 @@ const User = {
return tokens;
},
ignoredUsers({id}, args, {user, loaders: {Users}}) {
async ignoredUsers({id}, args, {user, loaders: {Users}}) {
// Only allow a logged in user that is either the current user or is a staff
// member to access the ignoredUsers of a given user.
@@ -64,7 +64,8 @@ const User = {
return [];
}
return Users.getByQuery({ids: user.ignoresUsers});
const connection = await Users.getByQuery({ids: user.ignoresUsers});
return connection.nodes;
},
roles({id, roles}, _, {user}) {
+23 -2
View File
@@ -121,10 +121,31 @@ type User {
suspension: SuspensionInfo
}
# UserConnection represents a paginable subset of a user list.
type UserConnection {
# Indicates that there are more users after this subset.
hasNextPage: Boolean!
# Cursor of first user in subset.
startCursor: Cursor
# Cursor of last user in subset.
endCursor: Cursor
# Subset of users.
nodes: [User!]!
}
# UsersQuery allows the ability to query users by a specific fields.
input UsersQuery {
# Users returned will only be ones which have at least one action of this.
action_type: ACTION_TYPE
# Current status of a user..
statuses: [USER_STATUS!]
# Limit the number of results to be returned.
limit: Int = 10
@@ -730,8 +751,8 @@ type RootQuery {
# role.
me: User
# Users returned based on a query.
users(query: UsersQuery): [User]
# Users returned based on a query. Requires the `ADMIN` role.
users(query: UsersQuery!): UserConnection
# a single User by id
user(id: ID!): User