diff --git a/client/coral-admin/src/routes/Moderation/components/CommentCount.css b/client/coral-admin/src/components/CountBadge.css similarity index 93% rename from client/coral-admin/src/routes/Moderation/components/CommentCount.css rename to client/coral-admin/src/components/CountBadge.css index 998133d07..343692ecb 100644 --- a/client/coral-admin/src/routes/Moderation/components/CommentCount.css +++ b/client/coral-admin/src/components/CountBadge.css @@ -5,7 +5,7 @@ vertical-align: middle; padding: 1px 5px; border-radius: 2px; - margin-left: 2px; + margin-left: 5px; line-height: 18px; box-sizing: border-box; height: 18px; diff --git a/client/coral-admin/src/routes/Moderation/components/CommentCount.js b/client/coral-admin/src/components/CountBadge.js similarity index 81% rename from client/coral-admin/src/routes/Moderation/components/CommentCount.js rename to client/coral-admin/src/components/CountBadge.js index 53d611ed6..ff47cf9a6 100644 --- a/client/coral-admin/src/routes/Moderation/components/CommentCount.js +++ b/client/coral-admin/src/components/CountBadge.js @@ -1,10 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import styles from './CommentCount.css'; +import styles from './CountBadge.css'; import t from 'coral-framework/services/i18n'; -const CommentCount = ({count}) => { +const CountBadge = ({count}) => { let number = count; // shorten large counts to abbreviations @@ -21,8 +21,8 @@ const CommentCount = ({count}) => { ); }; -CommentCount.propTypes = { +CountBadge.propTypes = { count: PropTypes.number.isRequired }; -export default CommentCount; +export default CountBadge; diff --git a/client/coral-admin/src/routes/Community/components/Community.js b/client/coral-admin/src/routes/Community/components/Community.js index e59f9b6ca..bf300a166 100644 --- a/client/coral-admin/src/routes/Community/components/Community.js +++ b/client/coral-admin/src/routes/Community/components/Community.js @@ -4,6 +4,7 @@ import CommunityMenu from './CommunityMenu'; import People from './People'; import FlaggedAccounts from '../containers/FlaggedAccounts'; import RejectUsernameDialog from './RejectUsernameDialog'; +import PropTypes from 'prop-types'; export default class Community extends Component { @@ -76,7 +77,10 @@ export default class Community extends Component { return (
- + - -
- { tab } -
+ +
{tab}
); } } +Community.propTypes = { + community: PropTypes.object, + fetchAccounts: PropTypes.func, + hideRejectUsernameDialog: PropTypes.func, + updateSorting: PropTypes.func, + newPage: PropTypes.func, + route: PropTypes.object, + rejectUsername: PropTypes.func, + data: PropTypes.object, + root: PropTypes.object +}; diff --git a/client/coral-admin/src/routes/Community/components/CommunityMenu.js b/client/coral-admin/src/routes/Community/components/CommunityMenu.js index 1e226313d..3e13617fe 100644 --- a/client/coral-admin/src/routes/Community/components/CommunityMenu.js +++ b/client/coral-admin/src/routes/Community/components/CommunityMenu.js @@ -1,18 +1,21 @@ import React from 'react'; - import styles from './CommunityMenu.css'; import t from 'coral-framework/services/i18n'; import {Link} from 'react-router'; +import PropTypes from 'prop-types'; +import CountBadge from '../../../components/CountBadge'; -const CommunityMenu = () => { +const CommunityMenu = ({flaggedUsernamesCount = 0}) => { const flaggedPath = '/admin/community/flagged'; const peoplePath = '/admin/community/people'; + return (
{t('community.flaggedaccounts')} + {t('community.people')} @@ -23,4 +26,8 @@ const CommunityMenu = () => { ); }; +CommunityMenu.propTypes = { + flaggedUsernamesCount: PropTypes.number, +}; + export default CommunityMenu; diff --git a/client/coral-admin/src/routes/Community/containers/Community.js b/client/coral-admin/src/routes/Community/containers/Community.js index a41c257d6..445b7f413 100644 --- a/client/coral-admin/src/routes/Community/containers/Community.js +++ b/client/coral-admin/src/routes/Community/containers/Community.js @@ -1,9 +1,16 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {compose} from 'react-apollo'; +import {compose, gql} from 'react-apollo'; +import withQuery from 'coral-framework/hocs/withQuery'; +import PropTypes from 'prop-types'; + +import FlaggedAccounts from '../containers/FlaggedAccounts'; +import FlaggedUser from '../containers/FlaggedUser'; import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations'; +import {getDefinitionName} from 'coral-framework/utils'; + import { fetchAccounts, updateSorting, @@ -20,15 +27,57 @@ class CommunityContainer extends Component { } render() { - return ( - - ); + return ; } } const mapStateToProps = (state) => ({ community: state.community, }); +CommunityContainer.propTypes = { + community: PropTypes.object, + fetchAccounts: PropTypes.func, + hideRejectUsernameDialog: PropTypes.func, + updateSorting: PropTypes.func, + newPage: PropTypes.func, + route: PropTypes.object, + rejectUsername: PropTypes.func, + data: PropTypes.object, + root: PropTypes.object +}; + +const withData = withQuery(gql` + query TalkAdmin_FlaggedUsernamesCount { + flaggedUsernamesCount: userCount(query: { + action_type: FLAG, + statuses: [PENDING] + }) + ...${getDefinitionName(FlaggedAccounts.fragments.root)} + ...${getDefinitionName(FlaggedUser.fragments.root)} + me { + ...${getDefinitionName(FlaggedUser.fragments.me)} + __typename + } + } + ${FlaggedAccounts.fragments.root} + ${FlaggedUser.fragments.root} + ${FlaggedUser.fragments.me} + `, { + options: { + fetchPolicy: 'network-only', + }, +}); + const mapDispatchToProps = (dispatch) => bindActionCreators({ fetchAccounts, @@ -41,4 +90,5 @@ export default compose( connect(mapStateToProps, mapDispatchToProps), withSetUserStatus, withRejectUsername, + withData, )(CommunityContainer); diff --git a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js index 00c6fdf33..b7f918d8c 100644 --- a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js +++ b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js @@ -2,8 +2,9 @@ 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 {withFragments} from 'plugin-api/beta/client/hocs'; import {Spinner} from 'coral-ui'; +import PropTypes from 'prop-types'; import {withSetUserStatus} from 'coral-framework/graphql/mutations'; import {showBanUserDialog} from 'actions/banUserDialog'; @@ -24,7 +25,10 @@ class FlaggedAccountsContainer extends Component { } approveUser = ({userId}) => { - return this.props.setUserStatus({userId, status: 'APPROVED'}); + return this.props.setUserStatus({ + userId, + status: 'APPROVED' + }); } loadMore = () => { @@ -74,6 +78,16 @@ class FlaggedAccountsContainer extends Component { } } +FlaggedAccountsContainer.propTypes = { + showBanUserDialog: PropTypes.func, + showSuspendUserDialog: PropTypes.func, + showRejectUsernameDialog: PropTypes.func, + viewUserDetail: PropTypes.func, + setUserStatus: PropTypes.func, + data: PropTypes.object, + root: PropTypes.object +}; + const LOAD_MORE_QUERY = gql` query TalkAdmin_LoadMoreFlaggedAccounts($limit: Int, $cursor: Cursor) { users(query:{action_type: FLAG, statuses: [PENDING], limit: $limit, cursor: $cursor}){ @@ -88,31 +102,6 @@ const LOAD_MORE_QUERY = gql` ${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, @@ -123,6 +112,23 @@ const mapDispatchToProps = (dispatch) => export default compose( connect(null, mapDispatchToProps), - withFlaggedAccountsyQuery, withSetUserStatus, + withFragments({ + root: gql` + fragment TalkAdminCommunity_FlaggedAccounts_root on RootQuery { + users(query:{action_type: FLAG, statuses: [PENDING], limit: 10}){ + hasNextPage + endCursor + nodes { + __typename + ...${getDefinitionName(FlaggedUser.fragments.user)} + } + } + me { + id + } + } + ${FlaggedUser.fragments.user} + `, + }), )(FlaggedAccountsContainer); diff --git a/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js b/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js index 25b90cf64..27b8409c6 100644 --- a/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js +++ b/client/coral-admin/src/routes/Moderation/components/ModerationMenu.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import CommentCount from './CommentCount'; +import CountBadge from '../../../components/CountBadge'; import styles from './styles.css'; import {SelectField, Option} from 'react-mdl-selectfield'; import {Icon} from 'coral-ui'; @@ -28,7 +28,7 @@ const ModerationMenu = ({ to={getModPath(queue.key, asset.id)} className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === queue.key})} activeClassName={styles.active}> - {queue.name} + {queue.name} )}
diff --git a/graph/loaders/users.js b/graph/loaders/users.js index b943c60ed..dc58fffad 100644 --- a/graph/loaders/users.js +++ b/graph/loaders/users.js @@ -107,6 +107,40 @@ const getUsersByQuery = async ({user, loaders: {Actions}}, {ids, limit, cursor, }; }; +/** + * Retrieves the count of users based on the passed in query. + * @param {Object} context graph context + * @param {Object} query query to execute against the users collection + * to compute the counts + * @return {Promise} resolves to the counts of the users from the + * query + */ +const getCountByQuery = async ({loaders: {Actions}}, {action_type, statuses}) => { + let query = UserModel.find(); + + if (action_type) { + const userIds = await Actions.getByTypes({action_type, item_type: 'USERS'}); + + query = query.find({ + id: { + $in: userIds + } + }); + } + + if (statuses) { + query = query.where({ + status: { + $in: statuses + } + }); + } + + return UserModel + .find(query) + .count(); +}; + /** * Creates a set of loaders based on a GraphQL context. * @param {Object} context the context of the GraphQL request @@ -115,6 +149,7 @@ const getUsersByQuery = async ({user, loaders: {Actions}}, {ids, limit, cursor, module.exports = (context) => ({ Users: { getByQuery: (query) => getUsersByQuery(context, query), - getByID: new DataLoader((ids) => genUserByIDs(context, ids)) + getByID: new DataLoader((ids) => genUserByIDs(context, ids)), + getCountByQuery: (query) => getCountByQuery(context, query) } }); diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 13e41a312..e4701c901 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -50,6 +50,14 @@ const RootQuery = { return Comments.getCountByQuery(query); }, + async userCount(_, {query}, {user, loaders: {Users}}) { + if (user == null || !user.can(SEARCH_OTHER_USERS)) { + return null; + } + + return Users.getCountByQuery(query); + }, + assetMetrics(_, query, {user, loaders: {Metrics: {Assets}}}) { if (user == null || !user.can(SEARCH_ASSETS)) { return null; diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 71928f49e..70167bb67 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -343,6 +343,18 @@ input CommentCountQuery { tags: [String!] } +# UserCountQuery allows the ability to query user counts by specific +# methods. +input UserCountQuery { + + # comments returned will only be ones which have at least one action of this + # type. + action_type: ACTION_TYPE + + # Current status of a user. + statuses: [USER_STATUS] +} + type EditInfo { edited: Boolean! editableUntil: Date @@ -840,6 +852,10 @@ type RootQuery { # expensive as it is not batched. Requires the `ADMIN` role. commentCount(query: CommentCountQuery!): Int + # Return the count of users satisfied by the query. Note that this edge is + # expensive as it is not batched. This field is restricted. + userCount(query: UserCountQuery!): Int + # The currently logged in user based on the request. Requires any logged in # role. me: User