diff --git a/client/coral-admin/src/containers/SuspendUserDialog.js b/client/coral-admin/src/containers/SuspendUserDialog.js index f60014832..1616acfcf 100644 --- a/client/coral-admin/src/containers/SuspendUserDialog.js +++ b/client/coral-admin/src/containers/SuspendUserDialog.js @@ -54,8 +54,9 @@ class SuspendUserDialogContainer extends Component { const withOrganizationName = withQuery(gql` query CoralAdmin_SuspendUserDialog { + __typename settings { - organizationName + organizationName } } `); diff --git a/client/coral-admin/src/graphql/index.js b/client/coral-admin/src/graphql/index.js index dea68c5e8..8fedf6ebe 100644 --- a/client/coral-admin/src/graphql/index.js +++ b/client/coral-admin/src/graphql/index.js @@ -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; + } + } }), }, }; diff --git a/client/coral-admin/src/routes/Community/components/ActionButton.css b/client/coral-admin/src/routes/Community/components/ActionButton.css new file mode 100644 index 000000000..8f328e8b8 --- /dev/null +++ b/client/coral-admin/src/routes/Community/components/ActionButton.css @@ -0,0 +1,4 @@ +.actionButton { + transform: scale(.8); + margin: 0; +} diff --git a/client/coral-admin/src/routes/Community/components/ActionButton.js b/client/coral-admin/src/routes/Community/components/ActionButton.js index 09af420f6..0ef9b8a39 100644 --- a/client/coral-admin/src/routes/Community/components/ActionButton.js +++ b/client/coral-admin/src/routes/Community/components/ActionButton.js @@ -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 ( + {me.id !== user.id && + + + Suspend User + + + Ban User + + + } + + + +
+
+
+ flag + + {t('community.flags')}({ user.actions.length }) + : + { user.action_summaries.map( + (action, i) => { + return + {shortReasons[action.reason]} ({action.count}) + ; + } + )} +
+
+ { user.action_summaries.map( + (action_sum, i) => { + return
+ + {shortReasons[action_sum.reason]} ({action_sum.count}) + + {user.actions.map( + + // find the action by action_sum.reason + (action, j) => { + if (action.reason === action_sum.reason) { + return

+ {action.user && + + } + : {action.message ? action.message : 'n/a'} +

; + } + return null; + } + )} +
; + } + )} + +
+
+
+
+ {modActionButtons.map((action, i) => + + )} +
+
+
+ + + ); + } +} + +export default User; diff --git a/client/coral-admin/src/routes/Community/components/People.js b/client/coral-admin/src/routes/Community/components/People.js index aef4d250a..ff1c2c279 100644 --- a/client/coral-admin/src/routes/Community/components/People.js +++ b/client/coral-admin/src/routes/Community/components/People.js @@ -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'; diff --git a/client/coral-admin/src/routes/Community/components/User.js b/client/coral-admin/src/routes/Community/components/User.js deleted file mode 100644 index 5225e0ee9..000000000 --- a/client/coral-admin/src/routes/Community/components/User.js +++ /dev/null @@ -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') && -
  • -
    -
    -
    - - {props.currentUser.id !== user.id && - - - Suspend User - - Ban User - - - } -
    -
    - -
    -
    -
    - flag{t('community.flags')}({ user.actions.length }): - { user.action_summaries.map( - (action, i) => { - return - {shortReasons[action.reason]} ({action.count}) - ; - } - )} -
    -
    - { user.action_summaries.map( - (action_sum, i) => { - return
    - - {shortReasons[action_sum.reason]} ({action_sum.count}) - - {user.actions.map( - - // find the action by action_sum.reason - (action, j) => { - if (action.reason === action_sum.reason) { - return

    - {action.user && - - } - : {action.message ? action.message : 'n/a'} -

    ; - } - return null; - } - )} -
    ; - } - )} - -
    -
    -
    -
    - {modActionButtons.map((action, i) => - - )} -
    -
    -
    -
    -
  • ; -}; - -export default User; diff --git a/client/coral-admin/src/routes/Community/components/styles.css b/client/coral-admin/src/routes/Community/components/styles.css index 8581c4c7d..fc94717e0 100644 --- a/client/coral-admin/src/routes/Community/components/styles.css +++ b/client/coral-admin/src/routes/Community/components/styles.css @@ -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; } } + diff --git a/client/coral-admin/src/routes/Community/containers/Community.js b/client/coral-admin/src/routes/Community/containers/Community.js index bfb634f18..a41c257d6 100644 --- a/client/coral-admin/src/routes/Community/containers/Community.js +++ b/client/coral-admin/src/routes/Community/containers/Community.js @@ -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
    {this.props.data.error.message}
    ; - } - - if (!('users' in this.props.root)) { - return
    ; - } return ( - + ); } } - -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); diff --git a/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js new file mode 100644 index 000000000..00c6fdf33 --- /dev/null +++ b/client/coral-admin/src/routes/Community/containers/FlaggedAccounts.js @@ -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
    {this.props.data.error.message}
    ; + } + + if (this.props.data.loading) { + return
    ; + } + return ( + + ); + } +} + +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); diff --git a/client/coral-admin/src/routes/Community/containers/FlaggedUser.js b/client/coral-admin/src/routes/Community/containers/FlaggedUser.js new file mode 100644 index 000000000..f92c93c1c --- /dev/null +++ b/client/coral-admin/src/routes/Community/containers/FlaggedUser.js @@ -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); diff --git a/client/coral-admin/src/routes/Configure/components/Configure.css b/client/coral-admin/src/routes/Configure/components/Configure.css index 5b02b963a..0b7319132 100644 --- a/client/coral-admin/src/routes/Configure/components/Configure.css +++ b/client/coral-admin/src/routes/Configure/components/Configure.css @@ -1,3 +1,7 @@ +/** + * @TODO: deprecated as this file contains styles from multiple components. Please refactor. + */ + .container { display: flex; diff --git a/client/coral-admin/src/routes/Dashboard/components/Dashboard.css b/client/coral-admin/src/routes/Dashboard/components/Dashboard.css index cdad990ba..586ff6c08 100644 --- a/client/coral-admin/src/routes/Dashboard/components/Dashboard.css +++ b/client/coral-admin/src/routes/Dashboard/components/Dashboard.css @@ -1,3 +1,7 @@ +/** + * @TODO: deprecated as this file contains styles from multiple components. Please refactor. + */ + .Dashboard { display: flex; } diff --git a/client/coral-admin/src/routes/Dashboard/components/Widget.css b/client/coral-admin/src/routes/Dashboard/components/Widget.css index cf83fcbdc..e94d2313f 100644 --- a/client/coral-admin/src/routes/Dashboard/components/Widget.css +++ b/client/coral-admin/src/routes/Dashboard/components/Widget.css @@ -1,3 +1,7 @@ +/** + * @TODO: deprecated as this file contains styles from multiple components. Please refactor. + */ + :root { --row-height: 60px; } diff --git a/client/coral-admin/src/routes/Install/components/style.css b/client/coral-admin/src/routes/Install/components/Install.css similarity index 50% rename from client/coral-admin/src/routes/Install/components/style.css rename to client/coral-admin/src/routes/Install/components/Install.css index 62f7ffe04..8e6f7504d 100644 --- a/client/coral-admin/src/routes/Install/components/style.css +++ b/client/coral-admin/src/routes/Install/components/Install.css @@ -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; } diff --git a/client/coral-admin/src/routes/Install/components/Install.js b/client/coral-admin/src/routes/Install/components/Install.js index f7069d67c..0d206d8f0 100644 --- a/client/coral-admin/src/routes/Install/components/Install.js +++ b/client/coral-admin/src/routes/Install/components/Install.js @@ -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 ( -
    +
    { !install.alreadyInstalled ? (
    -

    Welcome to the Coral Project

    +

    Welcome to the Coral Project

    { install.step !== 0 ? : null } 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}; diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css index 061e5351d..6f27e0b94 100644 --- a/client/coral-admin/src/routes/Moderation/components/styles.css +++ b/client/coral-admin/src/routes/Moderation/components/styles.css @@ -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 { diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 010a65ab1..a285d1ff6 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -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; diff --git a/graph/loaders/users.js b/graph/loaders/users.js index 872c4c307..ac3493479 100644 --- a/graph/loaders/users.js +++ b/graph/loaders/users.js @@ -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, + }; }; /** diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 1e34e8ecd..239b052c0 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -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); } }; diff --git a/graph/resolvers/user.js b/graph/resolvers/user.js index 57601c269..e5c5746ca 100644 --- a/graph/resolvers/user.js +++ b/graph/resolvers/user.js @@ -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}) { diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 4dff623fd..9d8e25f12 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -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