From 0b20542f05545dc62c8ba68f4f748bd976660409 Mon Sep 17 00:00:00 2001 From: Nick Funk Date: Wed, 10 Jul 2019 10:59:28 -0600 Subject: [PATCH] [CORL-233] Create a user drawer with comment streams (#2395) * Create preliminary user history drawer with user fragment CORL-233 * Set font size for the user details to match spec at 16 px CORL-233 * Update username font style/size, add horizontal rule for user history drawer CORL-233 * Set user detail min width to align the copy buttons in user history drawer CORL-233 * Set min width of the user history drawer to match spec CORL-233 * Remove min width for detail area and card to allow responsive sizing Allows the copy buttons to sit just to the right of the user details text. CORL-233 * Update snapshots to handle clickable username in moderation comment cards CORL-233 * Create initial pagination of all comments in user history drawer Shows all comments with a rubbish "load more" button as we haven't decided if we want to do a load more button or infinite scrolling solution. Flex layout helps it create a scroll bar when needed to traverse through the comment stream. CORL-233 * Create preliminary user history drawer with user fragment CORL-233 * Set font size for the user details to match spec at 16 px CORL-233 * Update username font style/size, add horizontal rule for user history drawer CORL-233 * Set user detail min width to align the copy buttons in user history drawer CORL-233 * Set min width of the user history drawer to match spec CORL-233 * Remove min width for detail area and card to allow responsive sizing Allows the copy buttons to sit just to the right of the user details text. CORL-233 * Update snapshots to handle clickable username in moderation comment cards CORL-233 * Let the user history drawer fill full height of page CORL-233 * Add a close button to the user history drawer CORL-233 * Preliminarily get all and rejected comments showing in the user drawer CORL-233 * Create preliminary user history drawer with user fragment CORL-233 * Set font size for the user details to match spec at 16 px CORL-233 * Update username font style/size, add horizontal rule for user history drawer CORL-233 * Set user detail min width to align the copy buttons in user history drawer CORL-233 * Set min width of the user history drawer to match spec CORL-233 * Remove min width for detail area and card to allow responsive sizing Allows the copy buttons to sit just to the right of the user details text. CORL-233 * Update snapshots to handle clickable username in moderation comment cards CORL-233 * Let the user history drawer fill full height of page CORL-233 * Add a close button to the user history drawer CORL-233 * Set the user history drawer to fill full height of window CORL-233 * Convert ModerateCardContainer to a FunctionComponent CORL-233 * Show spinner while all comments are loading in user history drawer CORL-233 * Style tabs within the user history drawer to be secondary CORL-233 * Center load more button in the comments for the user history drawer CORL-233 * Fix scroll bars on nested flexboxes in the user history drawer CORL-233 * Tweak styling for the user history drawer comments section Add margins where necessary. Style the Load More button to match the comment feed elsewhere. CORL-233 * Fix background of copy buttons to match the user history drawer CORL-233 * Preliminarily get rejected comments showing in the user history tabs Still need to consolidate any duplication between the all and rejected comments containers/queries, but this works correctly as is. CORL-233 * Show hover and active background color on moderate card usernames CORL-233 * Hide load more buttons on comment streams when no comments available CORL-233 * Handle when no comments are available in user history drawer streams CORL-233 * Actually show all comments in the user history all comments tab Previously was filtering out rejected and other pre-mod status-ed comments. CORL-233 * Style the user history drawer cards to match the spec CORL-233 * Add localizations around user history drawer tabs CORL-233 * Allow a mini mode for moderate cards that are shown in the user drawer CORL-233 * Style user drawer comment tabs to match the spec CORL-233 * Update snapshots to account for mini moderate cards Updates the fact that a separator on regular sized cards now has an additional class name to turn on the divider's visible border. CORL-233 * Add user drawer to the queue instead of childed to the moderate card Prevents the user drawer from prematurely closing when we approve all items in the moderate queue. CORL-233 * Update snapshots to account for user drawer on queues CORL-233 * Set mini and username defaults more cleanly on ModerateCardContainer CORL-233 * Rename usernameClicked to onUsernameClicked CORL-233 * Use callback on loadMore calls within user drawer queries CORL-233 * Localize the no comment text for the user drawer comment streams CORL-233 * Localize the user not found callout in the user drawer comment streams CORL-233 * Remove superfluous curly braces CORL-233 * Extract user history drawer internals into a distinct query component CORL-233 * Move comments conditional below useCallback initializations CORL-233 * feat: fixed consistency issues * Use a concrete tab type on user drawer comment stream tabs CORL-233 * Use
instead of CORL-233 * Clarify logic for showing horizontal rule after comments in user drawer CORL-233 * Remover bottom border on user drawer tab bar CORL-233 --- .../admin/routes/Configure/HorizontalRule.tsx | 10 +- .../Moderate/ModerateCard/FeatureButton.css | 5 +- .../Moderate/ModerateCard/ModerateCard.css | 53 +++++- .../ModerateCard/ModerateCard.spec.tsx | 1 + .../Moderate/ModerateCard/ModerateCard.tsx | 52 +++++- .../ModerateCard/ModerateCardContainer.tsx | 140 ++++++++------ .../__snapshots__/ModerateCard.spec.tsx.snap | 114 ++++++++---- .../admin/routes/Moderate/Queue/Queue.tsx | 118 +++++++----- .../Queue/__snapshots__/Queue.spec.tsx.snap | 10 + .../UserHistoryAllComments.css | 15 ++ .../UserHistoryAllComments.tsx | 159 ++++++++++++++++ .../UserHistoryAllCommentsContainer.css | 10 + .../UserHistoryAllCommentsContainer.tsx | 72 +++++++ .../UserHistoryDrawerContainer.css | 14 ++ .../UserHistoryDrawerContainer.tsx | 35 ++++ .../UserHistoryDrawerQuery.css | 94 ++++++++++ .../UserHistoryDrawerQuery.tsx | 137 ++++++++++++++ .../UserHistoryRejectedComments.css | 15 ++ .../UserHistoryRejectedComments.tsx | 161 ++++++++++++++++ .../UserHistoryRejectedCommentsContainer.css | 10 + .../UserHistoryRejectedCommentsContainer.tsx | 72 +++++++ .../UserHistoryDrawer/UserHistoryTabs.css | 59 ++++++ .../UserHistoryDrawer/UserHistoryTabs.tsx | 85 +++++++++ .../__snapshots__/regularQueue.spec.tsx.snap | 175 +++++++++++++----- .../__snapshots__/rejectedQueue.spec.tsx.snap | 100 +++++++--- .../__snapshots__/singleComment.spec.tsx.snap | 75 ++++++-- .../server/graph/tenant/loaders/Comments.ts | 27 ++- .../server/graph/tenant/resolvers/User.ts | 4 + .../server/graph/tenant/schema/schema.graphql | 12 ++ src/core/server/models/comment/index.ts | 51 +++++ src/locales/en-US/admin.ftl | 9 + 31 files changed, 1652 insertions(+), 242 deletions(-) create mode 100644 src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllComments.css create mode 100644 src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllComments.tsx create mode 100644 src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllCommentsContainer.css create mode 100644 src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllCommentsContainer.tsx create mode 100644 src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerContainer.css create mode 100644 src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerContainer.tsx create mode 100644 src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerQuery.css create mode 100644 src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerQuery.tsx create mode 100644 src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedComments.css create mode 100644 src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedComments.tsx create mode 100644 src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedCommentsContainer.css create mode 100644 src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedCommentsContainer.tsx create mode 100644 src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryTabs.css create mode 100644 src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryTabs.tsx diff --git a/src/core/client/admin/routes/Configure/HorizontalRule.tsx b/src/core/client/admin/routes/Configure/HorizontalRule.tsx index 4f77ba14f..e3b229f7b 100644 --- a/src/core/client/admin/routes/Configure/HorizontalRule.tsx +++ b/src/core/client/admin/routes/Configure/HorizontalRule.tsx @@ -1,9 +1,15 @@ +import cn from "classnames"; import React, { FunctionComponent } from "react"; import styles from "./HorizontalRule.css"; -const HorizontalRule: FunctionComponent = ({ children }) => ( -
+interface Props { + className?: string; + children?: any; +} + +const HorizontalRule: FunctionComponent = ({ children, className }) => ( +
); export default HorizontalRule; diff --git a/src/core/client/admin/routes/Moderate/ModerateCard/FeatureButton.css b/src/core/client/admin/routes/Moderate/ModerateCard/FeatureButton.css index 827519178..5fbeb7ecb 100644 --- a/src/core/client/admin/routes/Moderate/ModerateCard/FeatureButton.css +++ b/src/core/client/admin/routes/Moderate/ModerateCard/FeatureButton.css @@ -1,8 +1,11 @@ .root { + position: absolute; + right: 0px; + top: 2px; + border: 1px solid var(--palette-grey-lighter); box-sizing: border-box; border-radius: var(--round-corners); - float: right; padding: 2px var(--spacing-2) 2px var(--spacing-2); display: flex; justify-content: center; diff --git a/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCard.css b/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCard.css index e75c41a5b..3e28fadf1 100644 --- a/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCard.css +++ b/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCard.css @@ -1,9 +1,29 @@ .topBar { + position: relative; margin-bottom: var(--mini-unit); } +.topBarMini { + margin-bottom: var(--spacing-4); +} + .username { margin-right: var(--mini-unit); + padding: var(--spacing-1); + margin-left: calc(-1 * var(--spacing-1)); + line-height: calc(16rem / var(--rem-base)); + + &:hover { + background-color: var(--palette-grey-lightest); + border-radius: 2px; + border-style: none; + } + + &:active { + background-color: var(--palette-grey-lighter); + border-radius: 2px; + border-style: none; + } } .footer { @@ -19,16 +39,32 @@ } .aside { - padding-top: 25px; flex-shrink: 0; flex-grow: 0; box-sizing: border-box; + padding-top: 25px; } .asideWithoutReplyTo { padding-top: 10px; } +.asideMini { + /* + No reply to field, so we align below the fattest element + which is the feature button. + */ + padding-top: calc((14rem / var(--rem-base)) + 7px + var(--spacing-4)); +} + +.asideMiniWithReplyTo { + /* + We have reply to field, so we align below the fattest + element plus the reply to field. + */ + padding-top: calc(2 * (14rem / var(--rem-base)) + 5px + var(--spacing-4)); +} + .decision { font-size: calc(14rem / var(--rem-base)); font-weight: var(--font-weight-medium); @@ -42,10 +78,13 @@ .separator { flex-shrink: 0; flex-grow: 0; - border-right: 1px solid var(--palette-divider); margin: 0 calc(2 * var(--mini-unit)); } +.ruledSeparator { + border-right: 1px solid var(--palette-divider); +} + .root { transition: background 100ms, box-shadow 100ms; } @@ -103,3 +142,13 @@ text-align: center; padding-top: 1px; } + +.borderless { + border-width: 0px; + box-shadow: none; +} + +.miniButton { + width: 40px; + height: 40px; +} \ No newline at end of file diff --git a/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCard.spec.tsx b/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCard.spec.tsx index 5e3c351a7..42c064e68 100644 --- a/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCard.spec.tsx +++ b/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCard.spec.tsx @@ -24,6 +24,7 @@ const baseProps: PropTypesOf = { onApprove: noop, onReject: noop, onFeature: noop, + onUsernameClick: noop, showStory: false, moderatedBy: null, }; diff --git a/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCard.tsx b/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCard.tsx index 0dec4b678..7336708fe 100644 --- a/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCard.tsx +++ b/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCard.tsx @@ -3,7 +3,13 @@ import { Localized } from "fluent-react/compat"; import React, { FunctionComponent } from "react"; import { PropTypesOf } from "coral-framework/types"; -import { Card, Flex, HorizontalGutter, TextLink } from "coral-ui/components"; +import { + BaseButton, + Card, + Flex, + HorizontalGutter, + TextLink, +} from "coral-ui/components"; import ApproveButton from "./ApproveButton"; import CommentContent from "./CommentContent"; @@ -36,6 +42,9 @@ interface Props { onApprove: () => void; onReject: () => void; onFeature: () => void; + onUsernameClick: () => void; + mini?: boolean; + hideUsername?: boolean; /** * If set to true, it means this comment is about to be removed * from the queue. This will trigger some styling changes to @@ -59,25 +68,40 @@ const ModerateCard: FunctionComponent = ({ onApprove, onReject, onFeature, + onUsernameClick, dangling, showStory, storyTitle, storyHref, onModerateStory, moderatedBy, + mini = false, + hideUsername = false, }) => (
-
-
- {username} +
+ + {!hideUsername && ( + + {username} + + )} {createdAt} -
+ {inReplyTo && (
{inReplyTo} @@ -126,28 +150,36 @@ const ModerateCard: FunctionComponent = ({
-
+
- -
DECISION
-
+ {!mini && ( + +
DECISION
+
+ )} {moderatedBy} diff --git a/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCardContainer.tsx b/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCardContainer.tsx index 2debfa6f3..4fd86f61c 100644 --- a/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCardContainer.tsx +++ b/src/core/client/admin/routes/Moderate/ModerateCard/ModerateCardContainer.tsx @@ -1,5 +1,5 @@ import { Match, Router, withRouter } from "found"; -import React from "react"; +import React, { FunctionComponent, useCallback } from "react"; import { graphql } from "react-relay"; import { @@ -37,6 +37,9 @@ interface Props { match: Match; router: Router; showStoryInfo: boolean; + mini?: boolean; + hideUsername?: boolean; + onUsernameClicked?: (userID: string) => void; } function getStatus(comment: ModerateCardContainer_comment) { @@ -54,67 +57,82 @@ function isFeatured(comment: ModerateCardContainer_comment) { return comment.tags.some(t => t.code === GQLTAG.FEATURED); } -class ModerateCardContainer extends React.Component { - private handleApprove = () => { - this.props.approveComment({ - commentID: this.props.comment.id, - commentRevisionID: this.props.comment.revision.id, - storyID: this.props.match.params.storyID, +const ModerateCardContainer: FunctionComponent = ({ + comment, + settings, + danglingLogic, + showStoryInfo, + viewer, + match, + router, + approveComment, + rejectComment, + featureComment, + unfeatureComment, + mini, + hideUsername, + onUsernameClicked: usernameClicked, +}) => { + const handleApprove = useCallback(() => { + approveComment({ + commentID: comment.id, + commentRevisionID: comment.revision.id, + storyID: match.params.storyID, }); - }; + }, [approveComment, comment, match]); - private handleReject = () => { - this.props.rejectComment({ - commentID: this.props.comment.id, - commentRevisionID: this.props.comment.revision.id, - storyID: this.props.match.params.storyID, + const handleReject = useCallback(() => { + rejectComment({ + commentID: comment.id, + commentRevisionID: comment.revision.id, + storyID: match.params.storyID, }); - }; + }, [rejectComment, comment, match]); - private onFeature = () => { - const featured = isFeatured(this.props.comment); + const handleFeature = useCallback(() => { + featureComment({ + commentID: comment.id, + commentRevisionID: comment.revision.id, + storyID: match.params.storyID, + }); + }, [featureComment, comment, match]); + + const handleUnfeature = useCallback(() => { + unfeatureComment({ + commentID: comment.id, + storyID: match.params.storyID, + }); + }, [unfeatureComment, comment, match]); + + const onFeature = useCallback(() => { + const featured = isFeatured(comment); if (featured) { - this.handleUnfeature(); + handleUnfeature(); } else { - this.handleFeature(); + handleFeature(); } - }; + }, [comment]); - private handleFeature = () => { - this.props.featureComment({ - commentID: this.props.comment.id, - commentRevisionID: this.props.comment.revision.id, - storyID: this.props.match.params.storyID, - }); - }; - - private handleUnfeature = () => { - this.props.unfeatureComment({ - commentID: this.props.comment.id, - storyID: this.props.match.params.storyID, - }); - }; - - private handleModerateStory = (e: React.MouseEvent) => { - this.props.router.push( - getModerationLink("default", this.props.comment.story.id) - ); - if (e.preventDefault) { - e.preventDefault(); + const onUsernameClicked = useCallback(() => { + if (!usernameClicked) { + return; } - }; + usernameClicked(comment.author!.id); + }, [usernameClicked, comment]); - public render() { - const { - comment, - settings, - danglingLogic, - showStoryInfo, - viewer, - } = this.props; - const dangling = danglingLogic(comment.status); - return ( + const handleModerateStory = useCallback( + (e: React.MouseEvent) => { + router.push(getModerationLink("default", comment.story.id)); + if (e.preventDefault) { + e.preventDefault(); + } + }, + [router, comment] + ); + + return ( + <> { body={comment.body!} inReplyTo={comment.parent && comment.parent.author!.username!} comment={comment} - dangling={dangling} + dangling={danglingLogic(comment.status)} status={getStatus(comment)} featured={isFeatured(comment)} viewContextHref={comment.permalink} suspectWords={settings.wordList.suspect} bannedWords={settings.wordList.banned} - onApprove={this.handleApprove} - onReject={this.handleReject} - onFeature={this.onFeature} + onApprove={handleApprove} + onReject={handleReject} + onFeature={onFeature} + onUsernameClick={onUsernameClicked} moderatedBy={ } @@ -142,18 +161,21 @@ class ModerateCardContainer extends React.Component { ) } storyHref={getModerationLink("default", comment.story.id)} - onModerateStory={this.handleModerateStory} + onModerateStory={handleModerateStory} + mini={mini} + hideUsername={hideUsername} /> - ); - } -} + + ); +}; const enhanced = withFragmentContainer({ comment: graphql` fragment ModerateCardContainer_comment on Comment { id author { + id username } statusLiveUpdated diff --git a/src/core/client/admin/routes/Moderate/ModerateCard/__snapshots__/ModerateCard.spec.tsx.snap b/src/core/client/admin/routes/Moderate/ModerateCard/__snapshots__/ModerateCard.spec.tsx.snap index 499d99940..0a159e574 100644 --- a/src/core/client/admin/routes/Moderate/ModerateCard/__snapshots__/ModerateCard.spec.tsx.snap +++ b/src/core/client/admin/routes/Moderate/ModerateCard/__snapshots__/ModerateCard.spec.tsx.snap @@ -12,12 +12,17 @@ exports[`renders approved correctly 1`] = `
-
- + - Theon - + + Theon + + 2018-11-29T16:01:51.897Z @@ -25,7 +30,7 @@ exports[`renders approved correctly 1`] = ` featured={false} onClick={[Function]} /> -
+
-
- + - Theon - + + Theon + + 2018-11-29T16:01:51.897Z @@ -126,7 +138,7 @@ exports[`renders correctly 1`] = ` featured={false} onClick={[Function]} /> -
+
@@ -213,12 +227,17 @@ exports[`renders dangling correctly 1`] = `
-
- + - Theon - + + Theon + + 2018-11-29T16:01:51.897Z @@ -226,7 +245,7 @@ exports[`renders dangling correctly 1`] = ` featured={false} onClick={[Function]} /> -
+
-
- + - Theon - + + Theon + + 2018-11-29T16:01:51.897Z @@ -328,7 +354,7 @@ exports[`renders rejected correctly 1`] = ` featured={false} onClick={[Function]} /> -
+
@@ -416,12 +444,17 @@ exports[`renders reply correctly 1`] = `
-
- + - Theon - + + Theon + + 2018-11-29T16:01:51.897Z @@ -429,7 +462,7 @@ exports[`renders reply correctly 1`] = ` featured={false} onClick={[Function]} /> -
+
Julian @@ -475,7 +508,7 @@ exports[`renders reply correctly 1`] = `
@@ -521,12 +556,17 @@ exports[`renders story info 1`] = `
-
- + - Theon - + + Theon + + 2018-11-29T16:01:51.897Z @@ -534,7 +574,7 @@ exports[`renders story info 1`] = ` featured={false} onClick={[Function]} /> -
+
diff --git a/src/core/client/admin/routes/Moderate/Queue/Queue.tsx b/src/core/client/admin/routes/Moderate/Queue/Queue.tsx index 923864255..a9bb85f84 100644 --- a/src/core/client/admin/routes/Moderate/Queue/Queue.tsx +++ b/src/core/client/admin/routes/Moderate/Queue/Queue.tsx @@ -1,5 +1,5 @@ import { Localized } from "fluent-react/compat"; -import React, { FunctionComponent } from "react"; +import React, { FunctionComponent, useCallback, useState } from "react"; import { CSSTransition, TransitionGroup } from "react-transition-group"; import AutoLoadMore from "coral-admin/components/AutoLoadMore"; @@ -7,6 +7,7 @@ import { Button, Flex, HorizontalGutter } from "coral-ui/components"; import { PropTypesOf } from "coral-ui/types"; import ModerateCardContainer from "../ModerateCard"; +import UserHistoryDrawerContainer from "../UserHistoryDrawer/UserHistoryDrawerContainer"; import styles from "./Queue.css"; @@ -38,53 +39,76 @@ const Queue: FunctionComponent = ({ viewer, viewNewCount, onViewNew, -}) => ( - - {Boolean(viewNewCount && viewNewCount > 0) && ( - - - + + + )} + + {comments.map(c => ( + - View {viewNewCount} new comments - - - - )} - - {comments.map(c => ( - - + + ))} + + {hasMore && ( + + - - ))} - - {hasMore && ( - - - - )} - {comments.length === 0 && emptyElement} - -); + + )} + {comments.length === 0 && emptyElement} + + + ); +}; export default Queue; diff --git a/src/core/client/admin/routes/Moderate/Queue/__snapshots__/Queue.spec.tsx.snap b/src/core/client/admin/routes/Moderate/Queue/__snapshots__/Queue.spec.tsx.snap index 7fb64a625..c6a730204 100644 --- a/src/core/client/admin/routes/Moderate/Queue/__snapshots__/Queue.spec.tsx.snap +++ b/src/core/client/admin/routes/Moderate/Queue/__snapshots__/Queue.spec.tsx.snap @@ -19,6 +19,11 @@ exports[`renders correctly with load more 1`] = ` onLoadMore={[Function]} /> + `; @@ -33,5 +38,10 @@ exports[`renders correctly without load more 1`] = ` enter={false} exit={true} /> + `; diff --git a/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllComments.css b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllComments.css new file mode 100644 index 000000000..f90971cce --- /dev/null +++ b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllComments.css @@ -0,0 +1,15 @@ +.footer { + text-align: center; +} + +.loadMore { + width: 100%; + background-color: var(--palette-common-white); + + border-width: 1px; + border-style: solid; + border-color: var(--palette-grey-lighter); + border-radius: 2px; + + margin-top: var(--spacing-1); +} diff --git a/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllComments.tsx b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllComments.tsx new file mode 100644 index 000000000..4dea65b99 --- /dev/null +++ b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllComments.tsx @@ -0,0 +1,159 @@ +import { + useLoadMore, + withPaginationContainer, +} from "coral-framework/lib/relay"; +import { Localized } from "fluent-react/compat"; +import React, { FunctionComponent, useCallback } from "react"; +import { graphql, RelayPaginationProp } from "react-relay"; + +import { Button, CallOut, Typography } from "coral-ui/components/"; + +import { UserHistoryAllComments_settings } from "coral-admin/__generated__/UserHistoryAllComments_settings.graphql"; +import { UserHistoryAllComments_user } from "coral-admin/__generated__/UserHistoryAllComments_user.graphql"; +import { UserHistoryAllComments_viewer } from "coral-admin/__generated__/UserHistoryAllComments_viewer.graphql"; +import { UserHistoryAllCommentsPaginationQueryVariables } from "coral-admin/__generated__/UserHistoryAllCommentsPaginationQuery.graphql"; + +import { ModerateCardContainer } from "../ModerateCard"; + +import styles from "./UserHistoryAllComments.css"; + +interface Props { + user: UserHistoryAllComments_user; + viewer: UserHistoryAllComments_viewer; + settings: UserHistoryAllComments_settings; + relay: RelayPaginationProp; +} + +const UserHistoryAllComments: FunctionComponent = ({ + user, + viewer, + settings, + relay, +}) => { + const [loadMore, isLoadingMore] = useLoadMore(relay, 5); + + const onLoadMore = useCallback(() => { + if (!loadMore || isLoadingMore) { + return; + } + + loadMore(); + }, [loadMore]); + + const hasMore = relay.hasMore(); + const comments = user ? user.allComments.edges.map(edge => edge.node) : []; + + if (comments.length === 0) { + return ( + + + + {user.username} has not submitted any comments. + + + + ); + } + + return ( + <> + {comments.map((c, index) => ( +
+ false} + hideUsername + showStoryInfo + mini + /> + {// Don't show horizontal rule after last comment + index !== comments.length - 1 &&
} +
+ ))} + {hasMore && ( +
+ +
+ )} + + ); +}; + +type FragmentVariables = UserHistoryAllCommentsPaginationQueryVariables; + +const enhanced = withPaginationContainer< + Props, + UserHistoryAllCommentsPaginationQueryVariables, + FragmentVariables +>( + { + viewer: graphql` + fragment UserHistoryAllComments_viewer on User { + ...ModerateCardContainer_viewer + } + `, + settings: graphql` + fragment UserHistoryAllComments_settings on Settings { + ...ModerateCardContainer_settings + } + `, + user: graphql` + fragment UserHistoryAllComments_user on User + @argumentDefinitions( + count: { type: "Int!", defaultValue: 5 } + cursor: { type: "Cursor" } + ) { + username + allComments(first: $count, after: $cursor) + @connection(key: "UserHistoryAll_allComments") { + edges { + node { + id + ...ModerateCardContainer_comment + } + } + } + } + `, + }, + { + direction: "forward", + getConnectionFromProps(props) { + return props.user && props.user.allComments; + }, + getFragmentVariables(prevVars, totalCount) { + return { + ...prevVars, + count: totalCount, + }; + }, + getVariables(props, { count, cursor }, fragmentVariables) { + return { + count, + cursor, + userID: fragmentVariables.userID, + }; + }, + query: graphql` + query UserHistoryAllCommentsPaginationQuery( + $userID: ID! + $count: Int! + $cursor: Cursor + ) { + user(id: $userID) { + ...UserHistoryAllComments_user + @arguments(count: $count, cursor: $cursor) + } + } + `, + } +)(UserHistoryAllComments); + +export default enhanced; diff --git a/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllCommentsContainer.css b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllCommentsContainer.css new file mode 100644 index 000000000..db131eca4 --- /dev/null +++ b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllCommentsContainer.css @@ -0,0 +1,10 @@ +.root { + text-align: center; +} + +.callout { + width: 100%; + font-family: var(--font-family-sans-serif); + align-content: center; + text-align: center; +} \ No newline at end of file diff --git a/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllCommentsContainer.tsx b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllCommentsContainer.tsx new file mode 100644 index 000000000..1fafad613 --- /dev/null +++ b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryAllCommentsContainer.tsx @@ -0,0 +1,72 @@ +import { Localized } from "fluent-react/compat"; +import React, { FunctionComponent } from "react"; +import { ReadyState } from "react-relay"; + +import { CallOut, Spinner } from "coral-ui/components"; + +import { UserHistoryAllCommentsContainerQuery as QueryTypes } from "coral-admin/__generated__/UserHistoryAllCommentsContainerQuery.graphql"; +import { graphql, QueryRenderer } from "coral-framework/lib/relay"; + +import UserHistoryAllComments from "./UserHistoryAllComments"; + +import styles from "./UserHistoryAllCommentsContainer.css"; + +interface Props { + userID: string; +} + +const UserHistoryAllCommentsContainer: FunctionComponent = ({ + userID, +}) => { + return ( + + query={graphql` + query UserHistoryAllCommentsContainerQuery($userID: ID!) { + user(id: $userID) { + ...UserHistoryAllComments_user + } + viewer { + ...UserHistoryAllComments_viewer + } + settings { + ...UserHistoryAllComments_settings + } + } + `} + variables={{ userID }} + cacheConfig={{ force: true }} + render={({ error, props }: ReadyState) => { + if (!props) { + return ( +
+ +
+ ); + } + + if (!props.user) { + return ( +
+ + + User not found. + + +
+ ); + } + + return ( + + ); + }} + /> + ); +}; + +export default UserHistoryAllCommentsContainer; diff --git a/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerContainer.css b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerContainer.css new file mode 100644 index 000000000..9a86fc42f --- /dev/null +++ b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerContainer.css @@ -0,0 +1,14 @@ +.root { + position: fixed; + top:0; + right: 0; + + width: 624px; + height: 100%; + + display: flex; + flex-flow: column nowrap; + flex-direction: column; + + background-color: var(--palette-common-white); +} \ No newline at end of file diff --git a/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerContainer.tsx b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerContainer.tsx new file mode 100644 index 000000000..cb577e3ae --- /dev/null +++ b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerContainer.tsx @@ -0,0 +1,35 @@ +import React, { FunctionComponent } from "react"; + +import { Card, Modal } from "coral-ui/components"; + +import styles from "./UserHistoryDrawerContainer.css"; +import UserHistoryDrawerQuery from "./UserHistoryDrawerQuery"; + +interface UserHistoryDrawerContainerProps { + open: boolean; + onClose: () => void; + userID?: string; +} + +const UserHistoryDrawerContainer: FunctionComponent< + UserHistoryDrawerContainerProps +> = ({ open, onClose, userID }) => { + return ( + + {({ firstFocusableRef, lastFocusableRef }) => ( + + {userID && ( + + )} + + )} + + ); +}; + +export default UserHistoryDrawerContainer; diff --git a/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerQuery.css b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerQuery.css new file mode 100644 index 000000000..0766fbb5a --- /dev/null +++ b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerQuery.css @@ -0,0 +1,94 @@ +.root { + position: fixed; + top:0; + right: 0; + + width: 624px; + height: 100%; + + display: flex; + flex-flow: column nowrap; + flex-direction: column; + + background-color: var(--palette-common-white); +} + +.comments { + flex: auto; + overflow: hidden; +} + +.close { + position: absolute; + width: 40px; + height: 40px; + left: -40px; + + padding: 0px; + + background-color: var(--palette-common-white); + + border-width: 2px; + border-right-width: 0px; + border-style: solid; + border-color: var(--palette-grey-main); + border-radius: 4px 0px 0px 4px; +} + +.username { + font-family: var(--font-family-serif); + font-size: calc(24rem / var(--rem-base)); + font-weight: 500; + font-family: var(--font-family-serif); + line-height: calc(36em / 24); + letter-spacing: calc(0.2em / 24); + margin-bottom: var(--spacing-1); +} + +.userDetails { + margin-bottom: var(--spacing-3); +} + +.userDetail { + margin-bottom: var(--spacing-1); +} + +.userDetailValue { + margin-right: var(--spacing-2); +} + +.icon { + margin-right: var(--spacing-2); +} + +.copy { + border: 1px solid var(--palette-primary-main); + background-color: transparent; + color: var(--palette-primary-main); + padding-top: 2px; + padding-bottom: 2px; + + &:active { + background-color: var(--palette-primary-lightest); + border-color: var(--palette-primary-main); + color: var(--palette-primary-main); + } +} + +.callout { + width: 100%; + font-family: var(--font-family-sans-serif); + align-content: center; + text-align: center; +} + +hr { + border: 0; + border-bottom: 1px solid var(--palette-divider); + padding-top: 1px; + width: 100%; +} + +.divider { + border-bottom: 1px solid var(--palette-grey-lighter); +} \ No newline at end of file diff --git a/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerQuery.tsx b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerQuery.tsx new file mode 100644 index 000000000..9406cbc40 --- /dev/null +++ b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryDrawerQuery.tsx @@ -0,0 +1,137 @@ +import { graphql, QueryRenderer } from "coral-framework/lib/relay"; +import { Localized } from "fluent-react/compat"; +import React, { FunctionComponent } from "react"; +import { ReadyState } from "react-relay"; + +import { CopyButton } from "coral-framework/components"; +import { + Button, + CallOut, + Flex, + Icon, + Spinner, + Typography, +} from "coral-ui/components"; + +import { UserHistoryDrawerQuery as QueryTypes } from "coral-admin/__generated__/UserHistoryDrawerQuery.graphql"; + +import UserHistoryTabs from "./UserHistoryTabs"; + +import styles from "./UserHistoryDrawerQuery.css"; + +interface Props { + userID: string; + onClose: () => void; + firstFocusableRef: React.RefObject; + lastFocusableRef: React.RefObject; +} + +const UserHistoryDrawerQuery: FunctionComponent = ({ + userID, + onClose, +}) => { + return ( + + query={graphql` + query UserHistoryDrawerQuery($userID: ID!) { + user(id: $userID) { + id + username + email + createdAt + } + } + `} + variables={{ userID }} + cacheConfig={{ force: true }} + render={({ error, props }: ReadyState) => { + if (!props) { + return ( +
+ +
+ ); + } + + if (!props.user) { + return ( +
+ + + User not found. + + +
+ ); + } + + const user = props.user; + + return ( + <> + + + {user.username} + +
+ + + mail_outline + + + {user.email} + + + + + + date_range + + + {new Date(user.createdAt).toLocaleDateString("en-us", { + month: "long", + day: "numeric", + year: "numeric", + })} + + + + + people_outline + + + {user.id} + + + +
+
+
+ +
+ + ); + }} + /> + ); +}; + +export default UserHistoryDrawerQuery; diff --git a/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedComments.css b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedComments.css new file mode 100644 index 000000000..f90971cce --- /dev/null +++ b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedComments.css @@ -0,0 +1,15 @@ +.footer { + text-align: center; +} + +.loadMore { + width: 100%; + background-color: var(--palette-common-white); + + border-width: 1px; + border-style: solid; + border-color: var(--palette-grey-lighter); + border-radius: 2px; + + margin-top: var(--spacing-1); +} diff --git a/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedComments.tsx b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedComments.tsx new file mode 100644 index 000000000..704522ccb --- /dev/null +++ b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedComments.tsx @@ -0,0 +1,161 @@ +import { + useLoadMore, + withPaginationContainer, +} from "coral-framework/lib/relay"; +import { Localized } from "fluent-react/compat"; +import React, { FunctionComponent, useCallback } from "react"; +import { graphql, RelayPaginationProp } from "react-relay"; + +import { Button, CallOut, Typography } from "coral-ui/components"; + +import { UserHistoryRejectedComments_settings } from "coral-admin/__generated__/UserHistoryRejectedComments_settings.graphql"; +import { UserHistoryRejectedComments_user } from "coral-admin/__generated__/UserHistoryRejectedComments_user.graphql"; +import { UserHistoryRejectedComments_viewer } from "coral-admin/__generated__/UserHistoryRejectedComments_viewer.graphql"; +import { UserHistoryRejectedCommentsPaginationQueryVariables } from "coral-admin/__generated__/UserHistoryRejectedCommentsPaginationQuery.graphql"; + +import { ModerateCardContainer } from "../ModerateCard"; + +import styles from "./UserHistoryRejectedComments.css"; + +interface Props { + user: UserHistoryRejectedComments_user; + viewer: UserHistoryRejectedComments_viewer; + settings: UserHistoryRejectedComments_settings; + relay: RelayPaginationProp; +} + +const UserHistoryRejectedComments: FunctionComponent = ({ + user, + viewer, + settings, + relay, +}) => { + const [loadMore, isLoadingMore] = useLoadMore(relay, 5); + + const onLoadMore = useCallback(() => { + if (!loadMore || isLoadingMore) { + return; + } + + loadMore(); + }, [loadMore]); + + const hasMore = relay.hasMore(); + const comments = user + ? user.rejectedComments.edges.map(edge => edge.node) + : []; + + if (comments.length === 0) { + return ( + + + + {user.username} does not have any rejected comments. + + + + ); + } + + return ( + <> + {comments.map((c, index) => ( +
+ false} + hideUsername + showStoryInfo + mini + /> + {// Don't show horizontal rule after last comment + index !== comments.length - 1 &&
} +
+ ))} + {hasMore && ( +
+ +
+ )} + + ); +}; + +type FragmentVariables = UserHistoryRejectedCommentsPaginationQueryVariables; + +const enhanced = withPaginationContainer< + Props, + UserHistoryRejectedCommentsPaginationQueryVariables, + FragmentVariables +>( + { + viewer: graphql` + fragment UserHistoryRejectedComments_viewer on User { + ...ModerateCardContainer_viewer + } + `, + settings: graphql` + fragment UserHistoryRejectedComments_settings on Settings { + ...ModerateCardContainer_settings + } + `, + user: graphql` + fragment UserHistoryRejectedComments_user on User + @argumentDefinitions( + count: { type: "Int!", defaultValue: 5 } + cursor: { type: "Cursor" } + ) { + username + rejectedComments(first: $count, after: $cursor) + @connection(key: "UserHistoryRejected_rejectedComments") { + edges { + node { + id + ...ModerateCardContainer_comment + } + } + } + } + `, + }, + { + direction: "forward", + getConnectionFromProps(props) { + return props.user && props.user.rejectedComments; + }, + getFragmentVariables(prevVars, totalCount) { + return { + ...prevVars, + count: totalCount, + }; + }, + getVariables(props, { count, cursor }, fragmentVariables) { + return { + count, + cursor, + userID: fragmentVariables.userID, + }; + }, + query: graphql` + query UserHistoryRejectedCommentsPaginationQuery( + $userID: ID! + $count: Int! + $cursor: Cursor + ) { + user(id: $userID) { + ...UserHistoryRejectedComments_user + @arguments(count: $count, cursor: $cursor) + } + } + `, + } +)(UserHistoryRejectedComments); + +export default enhanced; diff --git a/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedCommentsContainer.css b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedCommentsContainer.css new file mode 100644 index 000000000..db131eca4 --- /dev/null +++ b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedCommentsContainer.css @@ -0,0 +1,10 @@ +.root { + text-align: center; +} + +.callout { + width: 100%; + font-family: var(--font-family-sans-serif); + align-content: center; + text-align: center; +} \ No newline at end of file diff --git a/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedCommentsContainer.tsx b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedCommentsContainer.tsx new file mode 100644 index 000000000..cd351e6f4 --- /dev/null +++ b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryRejectedCommentsContainer.tsx @@ -0,0 +1,72 @@ +import { Localized } from "fluent-react/compat"; +import React, { FunctionComponent } from "react"; +import { ReadyState } from "react-relay"; + +import { CallOut, Spinner } from "coral-ui/components"; + +import { UserHistoryRejectedCommentsContainerQuery as QueryTypes } from "coral-admin/__generated__/UserHistoryRejectedCommentsContainerQuery.graphql"; +import { graphql, QueryRenderer } from "coral-framework/lib/relay"; + +import UserHistoryRejectedComments from "./UserHistoryRejectedComments"; + +import styles from "./UserHistoryRejectedCommentsContainer.css"; + +interface Props { + userID: string; +} + +const UserHistoryRejectedCommentsContainer: FunctionComponent = ({ + userID, +}) => { + return ( + + query={graphql` + query UserHistoryRejectedCommentsContainerQuery($userID: ID!) { + user(id: $userID) { + ...UserHistoryRejectedComments_user + } + viewer { + ...UserHistoryRejectedComments_viewer + } + settings { + ...UserHistoryRejectedComments_settings + } + } + `} + variables={{ userID }} + cacheConfig={{ force: true }} + render={({ error, props }: ReadyState) => { + if (!props) { + return ( +
+ +
+ ); + } + + if (!props.user) { + return ( +
+ + + User not found. + + +
+ ); + } + + return ( + + ); + }} + /> + ); +}; + +export default UserHistoryRejectedCommentsContainer; diff --git a/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryTabs.css b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryTabs.css new file mode 100644 index 000000000..1ab501b06 --- /dev/null +++ b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryTabs.css @@ -0,0 +1,59 @@ +.root { + height: 100%; + + display: flex; + flex-direction: column; + align-items: stretch; + min-height: 0; + + overflow: hidden; +} + +.tabBar { + flex: none; + border-bottom: 0px; +} + +.tab { + color: var(--palette-grey-dark); + height: 100%; + display: inline-flex; + align-items: center; + text-transform: uppercase; + text-decoration: none; + box-sizing: border-box; + border-bottom: 3px solid transparent; + + font-size: calc(14rem / var(--rem-base)); +} + +.activeTab { + color: var(--palette-primary-main); +} + +.tabIcon { + margin-right: var(--spacing-2); +} + +.tabContent { + flex: auto; + overflow: hidden; +} + +.container { + height: calc(100% - 2 * var(--spacing-2)); + + display: flex; + flex-direction: column; + align-items: stretch; + min-height: 0; + + overflow: hidden; + + margin-bottom: var(--spacing-2); + margin-top: var(--spacing-2); +} + +.scrollable { + overflow: auto; +} \ No newline at end of file diff --git a/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryTabs.tsx b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryTabs.tsx new file mode 100644 index 000000000..06c856d25 --- /dev/null +++ b/src/core/client/admin/routes/Moderate/UserHistoryDrawer/UserHistoryTabs.tsx @@ -0,0 +1,85 @@ +import cn from "classnames"; +import { Localized } from "fluent-react/compat"; +import React, { FunctionComponent, useCallback, useState } from "react"; + +import { Icon, Tab, TabBar, TabContent, TabPane } from "coral-ui/components"; + +import UserHistoryAllCommentsContainer from "./UserHistoryAllCommentsContainer"; +import UserHistoryRejectedCommentsContainer from "./UserHistoryRejectedCommentsContainer"; + +import styles from "./UserHistoryTabs.css"; + +type TabType = "ALL" | "REJECTED"; + +interface Props { + userID: string; +} + +const UserHistoryTabs: FunctionComponent = ({ userID }) => { + const [currentTab, setCurrentTab] = useState("ALL"); + + const onTabChanged = useCallback( + (tab: TabType) => { + setCurrentTab(tab); + }, + [setCurrentTab] + ); + + return ( +
+ + +
+ + forum + + + All Comments + +
+
+ +
+ + cancel + + + Rejected + +
+
+
+ + +
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+ ); +}; + +export default UserHistoryTabs; diff --git a/src/core/client/admin/test/moderate/__snapshots__/regularQueue.spec.tsx.snap b/src/core/client/admin/test/moderate/__snapshots__/regularQueue.spec.tsx.snap index e0817c02e..c2c2a5035 100644 --- a/src/core/client/admin/test/moderate/__snapshots__/regularQueue.spec.tsx.snap +++ b/src/core/client/admin/test/moderate/__snapshots__/regularQueue.spec.tsx.snap @@ -27,12 +27,25 @@ exports[`approves comment in reported queue: dangling 1`] = `
-
- +
-
- +
-
- +
-
- +
-
- +
-
- +
-
- +
-
- +
-
- +
-
- +
-
- +
-
- +
-
- +
-
- +
({ first = 10, orderBy = GQLCOMMENT_SORT.CREATED_AT_DESC, after, - }: StoryToCommentsArgs + }: UserToCommentsArgs ) => retrieveCommentUserConnection(ctx.mongo, ctx.tenant.id, userID, { first, orderBy, after, }).then(primeCommentsFromConnection(ctx)), + forUserAll: ( + userID: string, + // Apply the graph schema defaults at the loader. + { first = 10, after }: UserToAllCommentsArgs + ) => + retrieveAllCommentsUserConnection(ctx.mongo, ctx.tenant.id, userID, { + first, + orderBy: GQLCOMMENT_SORT.CREATED_AT_DESC, + after, + }).then(primeCommentsFromConnection(ctx)), + forUserRejected: ( + userID: string, + // Apply the graph schema defaults at the loader. + { first = 10, after }: UserToRejectedCommentsArgs + ) => + retrieveRejectedCommentUserConnection(ctx.mongo, ctx.tenant.id, userID, { + first, + orderBy: GQLCOMMENT_SORT.CREATED_AT_DESC, + after, + }).then(primeCommentsFromConnection(ctx)), taggedForStory: ( storyID: string, tag: GQLTAG, diff --git a/src/core/server/graph/tenant/resolvers/User.ts b/src/core/server/graph/tenant/resolvers/User.ts index 7db2cbba2..cf67255fd 100644 --- a/src/core/server/graph/tenant/resolvers/User.ts +++ b/src/core/server/graph/tenant/resolvers/User.ts @@ -34,6 +34,10 @@ const maybeLoadOnlyIgnoredUserID = ( export const User: GQLUserTypeResolver = { comments: ({ id }, input, ctx) => ctx.loaders.Comments.forUser(id, input), + allComments: ({ id }, input, ctx) => + ctx.loaders.Comments.forUserAll(id, input), + rejectedComments: ({ id }, input, ctx) => + ctx.loaders.Comments.forUserRejected(id, input), commentModerationActionHistory: ({ id }, input, ctx) => ctx.loaders.CommentModerationActions.forModerator(input, id), status: ({ id, status }): UserStatusInput => ({ diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 33296d57e..d6049c6dc 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -1521,6 +1521,18 @@ type User { permit: [SUSPENDED, BANNED] ) + """ + allComments are comments regardless of visibility status. + """ + allComments(first: Int = 10, after: Cursor): CommentsConnection! + @auth(roles: [ADMIN, MODERATOR]) + + """ + rejectedComments are comments that have been rejected. + """ + rejectedComments(first: Int = 10, after: Cursor): CommentsConnection! + @auth(roles: [ADMIN, MODERATOR]) + """ commentModerationActionHistory returns a CommentModerationActionConnection that this User has created. diff --git a/src/core/server/models/comment/index.ts b/src/core/server/models/comment/index.ts index 8d7b6b8e0..09bfe6060 100644 --- a/src/core/server/models/comment/index.ts +++ b/src/core/server/models/comment/index.ts @@ -657,6 +657,57 @@ export const retrieveCommentUserConnection = ( }, }); +/** + * retrieveAllCommentUserConnection returns a Connection for a given User's + * comments regardless of comment status. + * + * @param mongo database connection + * @param tenantID the Tenant's ID + * @param userID the User id for the comment to retrieve + * @param input connection configuration + */ +export const retrieveAllCommentsUserConnection = ( + mongo: Db, + tenantID: string, + userID: string, + input: CommentConnectionInput +) => + retrieveCommentConnection(mongo, tenantID, { + ...input, + filter: { + ...input.filter, + authorID: userID, + }, + }); + +/** + * retrieveRejectedCommentUserConnection returns a Connection for a given User's + * rejected comments. + * + * @param mongo database connection + * @param tenantID the Tenant's ID + * @param userID the User id for the comment to retrieve + * @param input connection configuration + */ +export const retrieveRejectedCommentUserConnection = ( + mongo: Db, + tenantID: string, + userID: string, + input: CommentConnectionInput +) => + retrieveStatusCommentConnection( + mongo, + tenantID, + [GQLCOMMENT_STATUS.REJECTED], + { + ...input, + filter: { + ...input.filter, + authorID: userID, + }, + } + ); + /** * retrieveVisibleCommentConnection will retrieve a connection that contains * comments that are visible. diff --git a/src/locales/en-US/admin.ftl b/src/locales/en-US/admin.ftl index 2b10007bf..7eb2858d9 100644 --- a/src/locales/en-US/admin.ftl +++ b/src/locales/en-US/admin.ftl @@ -388,6 +388,15 @@ moderate-searchBar-comboBoxTextField = moderate-searchBar-goTo = Go to moderate-searchBar-seeAllResults = See all results +### Moderate User History Drawer + +moderate-user-drawer-tab-all-comments = All Comments +moderate-user-drawer-tab-rejected-comments = Rejected +moderate-user-drawer-load-more = Load More +moderate-user-drawer-all-no-comments = {$username} has not submitted any comments. +moderate-user-drawer-rejected-no-comments = {$username} does not have any rejected comments. +moderate-user-drawer-user-not-found = User not found. + ## Create Username createUsername-createUsernameHeader = Create Username