From 58f2f717438e3e3b6fade0595017951bfbbc378c Mon Sep 17 00:00:00 2001 From: Tessa Thornton Date: Thu, 26 Mar 2020 13:47:16 -0400 Subject: [PATCH] [CORL-916] conversation view (#2850) * show comment counts for stories in story table * remove debug code * add modal to view comment * load converasation view into modal * style modal header * style conversation view modal * style conversation thread in conversation modal * styles conversation view comments * show replies to replies on click * update snaps * update snap * pass settings prop through to comment content, set default replies to 5 --- .../CommentContent.css | 0 .../CommentContent.spec.tsx | 0 .../CommentContent.tsx | 0 .../{ModerateCard => Comment}/InReplyTo.css | 0 .../InReplyTo.spec.tsx | 0 .../{ModerateCard => Comment}/InReplyTo.tsx | 0 .../{ModerateCard => Comment}/Username.css | 0 .../Username.spec.tsx | 0 .../{ModerateCard => Comment}/Username.tsx | 0 .../components/Comment/UsernameButton.css | 21 ++ .../components/Comment/UsernameButton.tsx | 27 ++ .../CommentContent.spec.tsx.snap | 0 .../__snapshots__/InReplyTo.spec.tsx.snap | 0 .../__snapshots__/Username.spec.tsx.snap | 0 .../client/admin/components/Comment/index.ts | 4 + .../ConversationModal/ConversationModal.tsx | 38 +++ .../ConversationModalCommentContainer.css | 37 +++ .../ConversationModalCommentContainer.tsx | 160 ++++++++++++ .../ConversationModalContainer.css | 10 + .../ConversationModalContainer.tsx | 159 ++++++++++++ .../ConversationModalHeaderContainer.css | 17 ++ .../ConversationModalHeaderContainer.tsx | 65 +++++ .../ConversationModalQuery.css | 8 + .../ConversationModalQuery.tsx | 99 ++++++++ .../ConversationModalRepliesContainer.css | 7 + .../ConversationModalRepliesContainer.tsx | 152 ++++++++++++ .../ConversationModalRepliesQuery.tsx | 87 +++++++ .../components/ConversationModal/index.ts | 1 + .../ModerateCard/CommentRevisionContainer.tsx | 2 +- .../components/ModerateCard/ModerateCard.css | 20 -- .../ModerateCard/ModerateCard.spec.tsx | 1 + .../components/ModerateCard/ModerateCard.tsx | 35 ++- .../ModerateCard/ModerateCardContainer.tsx | 13 + .../__snapshots__/ModerateCard.spec.tsx.snap | 231 +++++++++--------- .../admin/components/Timeline/Circle.css | 29 +++ .../admin/components/Timeline/Circle.tsx | 42 ++++ .../client/admin/components/Timeline/Line.css | 9 + .../admin/components/Timeline/Line.spec.tsx | 16 ++ .../client/admin/components/Timeline/Line.tsx | 23 ++ .../Timeline/__snapshots__/Line.spec.tsx.snap | 7 + .../client/admin/components/Timeline/index.ts | 2 + .../admin/routes/Moderate/Queue/Queue.tsx | 29 +++ .../Queue/__snapshots__/Queue.spec.tsx.snap | 12 + .../__snapshots__/regularQueue.spec.tsx.snap | 189 +++++++++----- .../__snapshots__/rejectedQueue.spec.tsx.snap | 108 +++++--- .../__snapshots__/singleComment.spec.tsx.snap | 81 ++++-- .../client/ui/components/v2/Button/Button.css | 4 + .../client/ui/components/v2/Button/Button.tsx | 5 + .../ui/components/v2/Modal/ModalHeader.css | 6 + .../ui/components/v2/Modal/ModalHeader.tsx | 27 ++ .../client/ui/components/v2/Modal/index.ts | 1 + src/core/client/ui/components/v2/index.ts | 2 +- src/locales/en-US/admin.ftl | 1 + 53 files changed, 1509 insertions(+), 278 deletions(-) rename src/core/client/admin/components/{ModerateCard => Comment}/CommentContent.css (100%) rename src/core/client/admin/components/{ModerateCard => Comment}/CommentContent.spec.tsx (100%) rename src/core/client/admin/components/{ModerateCard => Comment}/CommentContent.tsx (100%) rename src/core/client/admin/components/{ModerateCard => Comment}/InReplyTo.css (100%) rename src/core/client/admin/components/{ModerateCard => Comment}/InReplyTo.spec.tsx (100%) rename src/core/client/admin/components/{ModerateCard => Comment}/InReplyTo.tsx (100%) rename src/core/client/admin/components/{ModerateCard => Comment}/Username.css (100%) rename src/core/client/admin/components/{ModerateCard => Comment}/Username.spec.tsx (100%) rename src/core/client/admin/components/{ModerateCard => Comment}/Username.tsx (100%) create mode 100644 src/core/client/admin/components/Comment/UsernameButton.css create mode 100644 src/core/client/admin/components/Comment/UsernameButton.tsx rename src/core/client/admin/components/{ModerateCard => Comment}/__snapshots__/CommentContent.spec.tsx.snap (100%) rename src/core/client/admin/components/{ModerateCard => Comment}/__snapshots__/InReplyTo.spec.tsx.snap (100%) rename src/core/client/admin/components/{ModerateCard => Comment}/__snapshots__/Username.spec.tsx.snap (100%) create mode 100644 src/core/client/admin/components/Comment/index.ts create mode 100644 src/core/client/admin/components/ConversationModal/ConversationModal.tsx create mode 100644 src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.css create mode 100644 src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.tsx create mode 100644 src/core/client/admin/components/ConversationModal/ConversationModalContainer.css create mode 100644 src/core/client/admin/components/ConversationModal/ConversationModalContainer.tsx create mode 100644 src/core/client/admin/components/ConversationModal/ConversationModalHeaderContainer.css create mode 100644 src/core/client/admin/components/ConversationModal/ConversationModalHeaderContainer.tsx create mode 100644 src/core/client/admin/components/ConversationModal/ConversationModalQuery.css create mode 100644 src/core/client/admin/components/ConversationModal/ConversationModalQuery.tsx create mode 100644 src/core/client/admin/components/ConversationModal/ConversationModalRepliesContainer.css create mode 100644 src/core/client/admin/components/ConversationModal/ConversationModalRepliesContainer.tsx create mode 100644 src/core/client/admin/components/ConversationModal/ConversationModalRepliesQuery.tsx create mode 100644 src/core/client/admin/components/ConversationModal/index.ts create mode 100644 src/core/client/admin/components/Timeline/Circle.css create mode 100644 src/core/client/admin/components/Timeline/Circle.tsx create mode 100644 src/core/client/admin/components/Timeline/Line.css create mode 100644 src/core/client/admin/components/Timeline/Line.spec.tsx create mode 100644 src/core/client/admin/components/Timeline/Line.tsx create mode 100644 src/core/client/admin/components/Timeline/__snapshots__/Line.spec.tsx.snap create mode 100644 src/core/client/admin/components/Timeline/index.ts create mode 100644 src/core/client/ui/components/v2/Modal/ModalHeader.css create mode 100644 src/core/client/ui/components/v2/Modal/ModalHeader.tsx diff --git a/src/core/client/admin/components/ModerateCard/CommentContent.css b/src/core/client/admin/components/Comment/CommentContent.css similarity index 100% rename from src/core/client/admin/components/ModerateCard/CommentContent.css rename to src/core/client/admin/components/Comment/CommentContent.css diff --git a/src/core/client/admin/components/ModerateCard/CommentContent.spec.tsx b/src/core/client/admin/components/Comment/CommentContent.spec.tsx similarity index 100% rename from src/core/client/admin/components/ModerateCard/CommentContent.spec.tsx rename to src/core/client/admin/components/Comment/CommentContent.spec.tsx diff --git a/src/core/client/admin/components/ModerateCard/CommentContent.tsx b/src/core/client/admin/components/Comment/CommentContent.tsx similarity index 100% rename from src/core/client/admin/components/ModerateCard/CommentContent.tsx rename to src/core/client/admin/components/Comment/CommentContent.tsx diff --git a/src/core/client/admin/components/ModerateCard/InReplyTo.css b/src/core/client/admin/components/Comment/InReplyTo.css similarity index 100% rename from src/core/client/admin/components/ModerateCard/InReplyTo.css rename to src/core/client/admin/components/Comment/InReplyTo.css diff --git a/src/core/client/admin/components/ModerateCard/InReplyTo.spec.tsx b/src/core/client/admin/components/Comment/InReplyTo.spec.tsx similarity index 100% rename from src/core/client/admin/components/ModerateCard/InReplyTo.spec.tsx rename to src/core/client/admin/components/Comment/InReplyTo.spec.tsx diff --git a/src/core/client/admin/components/ModerateCard/InReplyTo.tsx b/src/core/client/admin/components/Comment/InReplyTo.tsx similarity index 100% rename from src/core/client/admin/components/ModerateCard/InReplyTo.tsx rename to src/core/client/admin/components/Comment/InReplyTo.tsx diff --git a/src/core/client/admin/components/ModerateCard/Username.css b/src/core/client/admin/components/Comment/Username.css similarity index 100% rename from src/core/client/admin/components/ModerateCard/Username.css rename to src/core/client/admin/components/Comment/Username.css diff --git a/src/core/client/admin/components/ModerateCard/Username.spec.tsx b/src/core/client/admin/components/Comment/Username.spec.tsx similarity index 100% rename from src/core/client/admin/components/ModerateCard/Username.spec.tsx rename to src/core/client/admin/components/Comment/Username.spec.tsx diff --git a/src/core/client/admin/components/ModerateCard/Username.tsx b/src/core/client/admin/components/Comment/Username.tsx similarity index 100% rename from src/core/client/admin/components/ModerateCard/Username.tsx rename to src/core/client/admin/components/Comment/Username.tsx diff --git a/src/core/client/admin/components/Comment/UsernameButton.css b/src/core/client/admin/components/Comment/UsernameButton.css new file mode 100644 index 000000000..8c60902e0 --- /dev/null +++ b/src/core/client/admin/components/Comment/UsernameButton.css @@ -0,0 +1,21 @@ +$moderateCardUsernameBackgroundHover: var(--v2-colors-grey-200); + +.root { + margin-right: var(--mini-unit); + padding: var(--spacing-1); + margin-left: calc(-1 * var(--v2-spacing-1)); + + line-height: var(--v2-line-height-reset); + + &:hover { + background-color: $moderateCardUsernameBackgroundHover; + border-radius: var(--v2-round-corners); + border-style: none; + } + + &:active { + background-color: $moderateCardUsernameBackgroundHover; + border-radius: var(--v2-round-corners); + border-style: none; + } +} diff --git a/src/core/client/admin/components/Comment/UsernameButton.tsx b/src/core/client/admin/components/Comment/UsernameButton.tsx new file mode 100644 index 000000000..13d6cbbbf --- /dev/null +++ b/src/core/client/admin/components/Comment/UsernameButton.tsx @@ -0,0 +1,27 @@ +import cn from "classnames"; +import React, { FunctionComponent } from "react"; + +import { BaseButton } from "coral-ui/components/v2"; +import Username from "./Username"; + +import styles from "./UsernameButton.css"; + +interface Props { + username: string; + className?: string; + onClick: () => void; +} + +const UsernameButton: FunctionComponent = ({ + username, + onClick, + className, +}) => { + return ( + + {username} + + ); +}; + +export default UsernameButton; diff --git a/src/core/client/admin/components/ModerateCard/__snapshots__/CommentContent.spec.tsx.snap b/src/core/client/admin/components/Comment/__snapshots__/CommentContent.spec.tsx.snap similarity index 100% rename from src/core/client/admin/components/ModerateCard/__snapshots__/CommentContent.spec.tsx.snap rename to src/core/client/admin/components/Comment/__snapshots__/CommentContent.spec.tsx.snap diff --git a/src/core/client/admin/components/ModerateCard/__snapshots__/InReplyTo.spec.tsx.snap b/src/core/client/admin/components/Comment/__snapshots__/InReplyTo.spec.tsx.snap similarity index 100% rename from src/core/client/admin/components/ModerateCard/__snapshots__/InReplyTo.spec.tsx.snap rename to src/core/client/admin/components/Comment/__snapshots__/InReplyTo.spec.tsx.snap diff --git a/src/core/client/admin/components/ModerateCard/__snapshots__/Username.spec.tsx.snap b/src/core/client/admin/components/Comment/__snapshots__/Username.spec.tsx.snap similarity index 100% rename from src/core/client/admin/components/ModerateCard/__snapshots__/Username.spec.tsx.snap rename to src/core/client/admin/components/Comment/__snapshots__/Username.spec.tsx.snap diff --git a/src/core/client/admin/components/Comment/index.ts b/src/core/client/admin/components/Comment/index.ts new file mode 100644 index 000000000..601b27337 --- /dev/null +++ b/src/core/client/admin/components/Comment/index.ts @@ -0,0 +1,4 @@ +export { default as CommentContent } from "./CommentContent"; +export { default as InReplyTo } from "./InReplyTo"; +export { default as Username } from "./Username"; +export { default as UsernameButton } from "./UsernameButton"; diff --git a/src/core/client/admin/components/ConversationModal/ConversationModal.tsx b/src/core/client/admin/components/ConversationModal/ConversationModal.tsx new file mode 100644 index 000000000..75e6988d1 --- /dev/null +++ b/src/core/client/admin/components/ConversationModal/ConversationModal.tsx @@ -0,0 +1,38 @@ +import React, { FunctionComponent } from "react"; + +import { Modal } from "coral-ui/components"; + +import ConversationModalQuery from "./ConversationModalQuery"; + +interface ConversationModalProps { + open: boolean; + onClose: () => void; + commentID?: string; + onUsernameClicked: (userID: string) => void; +} + +const ConversationModal: FunctionComponent = ({ + open, + onClose, + commentID, + onUsernameClicked, +}) => { + if (!commentID) { + return null; + } + return ( + + {({ firstFocusableRef, lastFocusableRef }) => ( + + )} + + ); +}; + +export default ConversationModal; diff --git a/src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.css b/src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.css new file mode 100644 index 000000000..5427cd3ed --- /dev/null +++ b/src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.css @@ -0,0 +1,37 @@ +$conversationModalHighlightBackground: var(--v2-colors-teal-100); +$conversationModalCommentText: var(--v2-colors-mono-500); +.root { + flex: 1; +} + +.line { + border-color: var(--v2-colors-grey-400); +} + +.adornments { + margin-right: var(--v2-spacing-2); +} + +.circle { + margin-top: var(--v2-spacing-2); + margin-bottom: var(--v2-spacing-2); +} + +.highlightedCircle { + margin-top: var(--v2-spacing-1); +} + +.highlighted { + background-color: $conversationModalHighlightBackground; + padding: var(--v2-spacing-2); + padding-top: var(--v2-spacing-1); + margin-bottom: var(--v2-spacing-2); +} + +.commentText { + color: $conversationModalCommentText; +} + +.showReplies { + padding-left: var(--v2-spacing-2); +} diff --git a/src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.tsx b/src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.tsx new file mode 100644 index 000000000..0f5927b28 --- /dev/null +++ b/src/core/client/admin/components/ConversationModal/ConversationModalCommentContainer.tsx @@ -0,0 +1,160 @@ +import { Localized } from "@fluent/react/compat"; +import cn from "classnames"; +import React, { FunctionComponent, useCallback, useState } from "react"; + +import { graphql, withFragmentContainer } from "coral-framework/lib/relay"; +import { + Button, + Flex, + HorizontalGutter, + Timestamp, +} from "coral-ui/components/v2"; + +import ConversationModalRepliesQuery from "./ConversationModalRepliesQuery"; + +import { ConversationModalCommentContainer_comment } from "coral-admin/__generated__/ConversationModalCommentContainer_comment.graphql"; +import { ConversationModalCommentContainer_settings } from "coral-admin/__generated__/ConversationModalCommentContainer_settings.graphql"; + +import { CommentContent, InReplyTo, UsernameButton } from "../Comment"; +import { Circle, Line } from "../Timeline"; + +import styles from "./ConversationModalCommentContainer.css"; + +interface Props { + comment: ConversationModalCommentContainer_comment; + isHighlighted: boolean; + onUsernameClick: (id?: string) => void; + isParent?: boolean; + isReply?: boolean; + settings: ConversationModalCommentContainer_settings; +} + +const ConversationModalCommentContainer: FunctionComponent = ({ + comment, + isHighlighted, + isParent, + isReply, + onUsernameClick, + settings, +}) => { + const commentAuthorClick = useCallback(() => { + if (comment.author) { + onUsernameClick(comment.author.id); + } + }, [onUsernameClick, comment.author]); + const commentParentAuthorClick = useCallback(() => { + if (comment.parent && comment.parent.author) { + onUsernameClick(comment.parent.author.id); + } + }, [onUsernameClick, comment.parent]); + const [showReplies, setShowReplies] = useState(false); + const onShowReplies = useCallback(() => { + setShowReplies(true); + }, []); + return ( + + + + {!isReply && } + {(isParent || isReply) && } + + +
+ + {comment.author && comment.author.username && ( + + )} + {comment.createdAt} + + {comment.parent && + comment.parent.author && + comment.parent.author.username && ( + + {comment.parent.author.username} + + )} +
+ +
+ {comment.body && ( + + {comment.body} + + )} +
+
+
+ {isReply && comment.replyCount > 0 && ( +
+ {!showReplies && ( + + + + )} + {showReplies && ( + + )} +
+ )} +
+ ); +}; + +const enhanced = withFragmentContainer({ + settings: graphql` + fragment ConversationModalCommentContainer_settings on Settings { + locale + wordList { + banned + suspect + } + multisite + featureFlags + ...MarkersContainer_settings + } + `, + comment: graphql` + fragment ConversationModalCommentContainer_comment on Comment { + id + body + createdAt + author { + username + id + } + replyCount + parent { + author { + username + id + } + } + } + `, +})(ConversationModalCommentContainer); + +export default enhanced; diff --git a/src/core/client/admin/components/ConversationModal/ConversationModalContainer.css b/src/core/client/admin/components/ConversationModal/ConversationModalContainer.css new file mode 100644 index 000000000..c766250b4 --- /dev/null +++ b/src/core/client/admin/components/ConversationModal/ConversationModalContainer.css @@ -0,0 +1,10 @@ +.root { +} +.topCircle { + margin-right: var(--v2-spacing-2); +} + +.bottomCircleContainer { + width: 8px; + padding-top: var(--v2-spacing-1); +} diff --git a/src/core/client/admin/components/ConversationModal/ConversationModalContainer.tsx b/src/core/client/admin/components/ConversationModal/ConversationModalContainer.tsx new file mode 100644 index 000000000..3e12607b4 --- /dev/null +++ b/src/core/client/admin/components/ConversationModal/ConversationModalContainer.tsx @@ -0,0 +1,159 @@ +import { Localized } from "@fluent/react/compat"; +import React, { FunctionComponent } from "react"; +import { graphql, RelayPaginationProp } from "react-relay"; + +import { + useLoadMore, + withPaginationContainer, +} from "coral-framework/lib/relay"; +import { + Button, + Counter, + Flex, + HorizontalGutter, +} from "coral-ui/components/v2"; + +import { ConversationModalContainer_comment } from "coral-admin/__generated__/ConversationModalContainer_comment.graphql"; +import { ConversationModalContainer_settings } from "coral-admin/__generated__/ConversationModalContainer_settings.graphql"; +import { ConversationModalContainerPaginationQueryVariables } from "coral-admin/__generated__/ConversationModalContainerPaginationQuery.graphql"; + +import { Circle } from "../Timeline"; +import ConversationModalComment from "./ConversationModalCommentContainer"; + +import styles from "./ConversationModalContainer.css"; + +interface Props { + relay: RelayPaginationProp; + comment: ConversationModalContainer_comment; + settings: ConversationModalContainer_settings; + onClose: () => void; + onUsernameClicked: (id?: string) => void; +} + +const ConversationModalContainer: FunctionComponent = ({ + comment, + relay, + settings, + onUsernameClicked, +}) => { + const [loadMore] = useLoadMore(relay, 5); + const parents = comment.parents.edges.map(edge => edge.node); + return ( + + {comment.parentCount > parents.length && ( +
+ + + + + + + +
+ )} + {parents.map(parent => ( + + ))} + +
+ ); +}; + +// TODO: (cvle) If this could be autogenerated. +type FragmentVariables = ConversationModalContainerPaginationQueryVariables; + +const enhanced = withPaginationContainer< + Props, + ConversationModalContainerPaginationQueryVariables, + FragmentVariables +>( + { + settings: graphql` + fragment ConversationModalContainer_settings on Settings { + ...ConversationModalCommentContainer_settings + } + `, + comment: graphql` + fragment ConversationModalContainer_comment on Comment + @argumentDefinitions( + count: { type: "Int!", defaultValue: 1 } + cursor: { type: "Cursor" } + ) { + id + ...ConversationModalCommentContainer_comment + rootParent { + id + } + parents(last: $count, before: $cursor) + @connection(key: "ConversationModal_parents") { + edges { + node { + id + ...ConversationModalCommentContainer_comment + } + } + } + parentCount + } + `, + }, + { + direction: "backward", + getConnectionFromProps(props) { + return props.comment && props.comment.parents; + }, + // This is also the default implementation of `getFragmentVariables` if it isn't provided. + getFragmentVariables(prevVars, totalCount) { + return { + ...prevVars, + count: totalCount, + }; + }, + getVariables(props, { count, cursor }) { + return { + count, + cursor, + // commentID isn't specified as an @argument for the fragment, but it should be a + // variable available for the fragment under the query root. + commentID: props.comment.id, + }; + }, + query: graphql` + # Pagination query to be fetched upon calling 'loadMore'. + # Notice that we re-use our fragment, and the shape of this query matches our fragment spec. + query ConversationModalContainerPaginationQuery( + $count: Int! + $cursor: Cursor + $commentID: ID! + ) { + comment(id: $commentID) { + ...ConversationModalContainer_comment + @arguments(count: $count, cursor: $cursor) + } + } + `, + } +)(ConversationModalContainer); + +export default enhanced; diff --git a/src/core/client/admin/components/ConversationModal/ConversationModalHeaderContainer.css b/src/core/client/admin/components/ConversationModal/ConversationModalHeaderContainer.css new file mode 100644 index 000000000..7b684b1ea --- /dev/null +++ b/src/core/client/admin/components/ConversationModal/ConversationModalHeaderContainer.css @@ -0,0 +1,17 @@ +$modal-header-text: var(--v2-colors-mono-900); + +.title { + font-size: var(--v2-font-size-4); + font-family: var(--v2-font-family-primary); + font-weight: var(--v2-font-weight-primary-regular); + line-height: var(--v2-line-height-title); + color: $modal-header-text; + margin: 0; +} + +.conversationTitle { + font-weight: var(--v2-font-weight-primary-semi-bold); + text-transform: uppercase; + display: block; + margin-bottom: var(--v2-spacing-2); +} diff --git a/src/core/client/admin/components/ConversationModal/ConversationModalHeaderContainer.tsx b/src/core/client/admin/components/ConversationModal/ConversationModalHeaderContainer.tsx new file mode 100644 index 000000000..860d30d36 --- /dev/null +++ b/src/core/client/admin/components/ConversationModal/ConversationModalHeaderContainer.tsx @@ -0,0 +1,65 @@ +import { Localized } from "@fluent/react/compat"; +import { useRouter } from "found"; +import React, { FunctionComponent, RefObject, useCallback } from "react"; + +import { getModerationLink } from "coral-framework/helpers"; +import { graphql, withFragmentContainer } from "coral-framework/lib/relay"; +import { Button, HorizontalGutter, ModalHeader } from "coral-ui/components/v2"; + +import { ConversationModalHeaderContainer_comment } from "coral-admin/__generated__/ConversationModalHeaderContainer_comment.graphql"; + +import styles from "./ConversationModalHeaderContainer.css"; + +interface Props { + comment: ConversationModalHeaderContainer_comment; + onClose: () => void; + focusableRef: RefObject; +} + +const ConversationModalHeaderContainer: FunctionComponent = ({ + comment, + onClose, + focusableRef, +}) => { + const { router } = useRouter(); + const onModerate = useCallback(() => { + const link = getModerationLink({ storyID: comment.story.id }); + router.push(link); + }, [comment.id]); + return ( + + +

+ + Conversation on: + + {comment.story.metadata + ? comment.story.metadata.title + : comment.story.url} +

+ + + +
+
+ ); +}; + +const enhanced = withFragmentContainer({ + comment: graphql` + fragment ConversationModalHeaderContainer_comment on Comment { + story { + id + metadata { + title + } + url + } + id + } + `, +})(ConversationModalHeaderContainer); + +export default enhanced; diff --git a/src/core/client/admin/components/ConversationModal/ConversationModalQuery.css b/src/core/client/admin/components/ConversationModal/ConversationModalQuery.css new file mode 100644 index 000000000..55178ed52 --- /dev/null +++ b/src/core/client/admin/components/ConversationModal/ConversationModalQuery.css @@ -0,0 +1,8 @@ +.root { + padding: 0; + width: 635px; +} + +.body { + padding: var(--v2-spacing-4); +} diff --git a/src/core/client/admin/components/ConversationModal/ConversationModalQuery.tsx b/src/core/client/admin/components/ConversationModal/ConversationModalQuery.tsx new file mode 100644 index 000000000..a16a8d6b7 --- /dev/null +++ b/src/core/client/admin/components/ConversationModal/ConversationModalQuery.tsx @@ -0,0 +1,99 @@ +import { Localized } from "@fluent/react/compat"; +import React, { FunctionComponent } from "react"; + +import { + graphql, + QueryRenderData, + QueryRenderer, +} from "coral-framework/lib/relay"; +import { CallOut, Card, Spinner } from "coral-ui/components/v2"; + +import { ConversationModalQuery as QueryTypes } from "coral-admin/__generated__/ConversationModalQuery.graphql"; + +import ConversationModalContainer from "./ConversationModalContainer"; +import ConversationModalHeaderContainer from "./ConversationModalHeaderContainer"; +import ConversationModalRepliesContainer from "./ConversationModalRepliesContainer"; + +import styles from "./ConversationModalQuery.css"; + +interface Props { + commentID: string; + onClose: () => void; + firstFocusableRef: React.RefObject; + onUsernameClicked: (userID: string) => void; + lastFocusableRef: React.RefObject; +} + +const ConversationModalQuery: FunctionComponent = ({ + commentID, + onClose, + lastFocusableRef, + onUsernameClicked, +}) => { + return ( + + query={graphql` + query ConversationModalQuery($commentID: ID!) { + settings { + ...ConversationModalContainer_settings + ...ConversationModalRepliesContainer_settings + } + comment(id: $commentID) { + ...ConversationModalContainer_comment + ...ConversationModalRepliesContainer_comment + ...ConversationModalHeaderContainer_comment + } + } + `} + variables={{ commentID }} + cacheConfig={{ force: true }} + render={({ props }: QueryRenderData) => { + if (!props) { + return ( +
+ +
+ ); + } + + if (!props.comment) { + return ( +
+ + + Comment not found. + + +
+ ); + } + + return ( + + +
+ + +
+
+ ); + }} + /> + ); +}; + +export default ConversationModalQuery; diff --git a/src/core/client/admin/components/ConversationModal/ConversationModalRepliesContainer.css b/src/core/client/admin/components/ConversationModal/ConversationModalRepliesContainer.css new file mode 100644 index 000000000..6b4688b11 --- /dev/null +++ b/src/core/client/admin/components/ConversationModal/ConversationModalRepliesContainer.css @@ -0,0 +1,7 @@ +.comment { + padding-left: calc(var(--v2-spacing-4)); +} + +.footer { + padding-left: calc(var(--v2-spacing-4)); +} diff --git a/src/core/client/admin/components/ConversationModal/ConversationModalRepliesContainer.tsx b/src/core/client/admin/components/ConversationModal/ConversationModalRepliesContainer.tsx new file mode 100644 index 000000000..1caefe130 --- /dev/null +++ b/src/core/client/admin/components/ConversationModal/ConversationModalRepliesContainer.tsx @@ -0,0 +1,152 @@ +import { Localized } from "@fluent/react/compat"; +import React, { FunctionComponent, useCallback, useState } from "react"; +import { graphql, RelayPaginationProp } from "react-relay"; + +import { + useLoadMore, + withPaginationContainer, +} from "coral-framework/lib/relay"; +import { Omit } from "coral-framework/types"; +import { Button, HorizontalGutter } from "coral-ui/components/v2"; + +import { ConversationModalRepliesContainer_comment } from "coral-admin/__generated__/ConversationModalRepliesContainer_comment.graphql"; +import { ConversationModalRepliesContainer_settings } from "coral-admin/__generated__/ConversationModalRepliesContainer_settings.graphql"; +import { ConversationModalRepliesContainerPaginationQueryVariables } from "coral-admin/__generated__/ConversationModalRepliesContainerPaginationQuery.graphql"; +import ConversationModalCommentContainer from "./ConversationModalCommentContainer"; + +import styles from "./ConversationModalRepliesContainer.css"; + +interface Props { + relay: RelayPaginationProp; + comment: ConversationModalRepliesContainer_comment; + settings: ConversationModalRepliesContainer_settings; + onClose: () => void; + onUsernameClicked: (id?: string) => void; +} + +const ConversationModalRepliesContainer: FunctionComponent = ({ + comment, + relay, + settings, + onUsernameClicked, +}) => { + const [loadMore] = useLoadMore(relay, 5); + const replies = comment.replies.edges.map(edge => edge.node); + const [showReplies, setShowReplies] = useState(false); + const onShowReplies = useCallback(() => { + setShowReplies(true); + }, []); + return ( + + {showReplies && + replies.map(reply => ( +
+ +
+ ))} +
+ {!showReplies && comment.replyCount > 0 && ( + + + + )} + {showReplies && + comment.replyCount > replies.length && + replies.length > 0 && ( + + + + )} +
+
+ ); +}; + +// TODO: (cvle) If this could be autogenerated. +type FragmentVariables = Omit< + ConversationModalRepliesContainerPaginationQueryVariables, + "commentID" +>; + +const enhanced = withPaginationContainer< + Props, + ConversationModalRepliesContainerPaginationQueryVariables, + FragmentVariables +>( + { + settings: graphql` + fragment ConversationModalRepliesContainer_settings on Settings { + ...ConversationModalCommentContainer_settings + } + `, + comment: graphql` + fragment ConversationModalRepliesContainer_comment on Comment + @argumentDefinitions( + count: { type: "Int!", defaultValue: 5 } + cursor: { type: "Cursor" } + orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_ASC } + ) { + id + replies(first: $count, after: $cursor, orderBy: $orderBy) + @connection(key: "ConversationModalReplies_replies") { + edges { + node { + id + ...ConversationModalCommentContainer_comment + } + } + } + replyCount + } + `, + }, + { + direction: "forward", + getConnectionFromProps(props) { + return props.comment && props.comment.replies; + }, + // This is also the default implementation of `getFragmentVariables` if it isn't provided. + getFragmentVariables(prevVars, totalCount) { + return { + ...prevVars, + count: totalCount, + }; + }, + getVariables(props, { count, cursor }, fragmentVariables) { + return { + count, + cursor, + orderBy: fragmentVariables.orderBy, + commentID: props.comment.id, + }; + }, + query: graphql` + # Pagination query to be fetched upon calling 'loadMore'. + # Notice that we re-use our fragment, and the shape of this query matches our fragment spec. + query ConversationModalRepliesContainerPaginationQuery( + $count: Int! + $cursor: Cursor + $orderBy: COMMENT_SORT! + $commentID: ID! + ) { + comment(id: $commentID) { + ...ConversationModalRepliesContainer_comment + @arguments(count: $count, cursor: $cursor, orderBy: $orderBy) + } + } + `, + } +)(ConversationModalRepliesContainer); + +export default enhanced; diff --git a/src/core/client/admin/components/ConversationModal/ConversationModalRepliesQuery.tsx b/src/core/client/admin/components/ConversationModal/ConversationModalRepliesQuery.tsx new file mode 100644 index 000000000..a132448a7 --- /dev/null +++ b/src/core/client/admin/components/ConversationModal/ConversationModalRepliesQuery.tsx @@ -0,0 +1,87 @@ +import { Localized } from "@fluent/react/compat"; +import React, { FunctionComponent } from "react"; + +import { + graphql, + QueryRenderData, + QueryRenderer, +} from "coral-framework/lib/relay"; +import { CallOut, Spinner } from "coral-ui/components/v2"; + +import { ConversationModalRepliesQuery as QueryTypes } from "coral-admin/__generated__/ConversationModalRepliesQuery.graphql"; +import ConversationModalCommentContainer from "./ConversationModalCommentContainer"; + +interface Props { + commentID: string; + onUsernameClicked: (userID: string) => void; +} + +const ConversationModalRepliesQuery: FunctionComponent = ({ + commentID, + onUsernameClicked, +}) => { + return ( + + query={graphql` + query ConversationModalRepliesQuery($commentID: ID!) { + settings { + ...ConversationModalCommentContainer_settings + } + comment(id: $commentID) { + replies { + edges { + node { + id + ...ConversationModalCommentContainer_comment + } + } + } + } + } + `} + variables={{ commentID }} + cacheConfig={{ force: true }} + render={({ props }: QueryRenderData) => { + if (!props) { + return ( +
+ +
+ ); + } + + if (!props.comment) { + return ( +
+ + + Comment not found. + + +
+ ); + } + + const replies = props.comment.replies.edges.map(edge => edge.node); + return ( +
+ {replies.map(reply => ( +
+ +
+ ))} +
+ ); + }} + /> + ); +}; + +export default ConversationModalRepliesQuery; diff --git a/src/core/client/admin/components/ConversationModal/index.ts b/src/core/client/admin/components/ConversationModal/index.ts new file mode 100644 index 000000000..77464e11b --- /dev/null +++ b/src/core/client/admin/components/ConversationModal/index.ts @@ -0,0 +1 @@ +export { default } from "./ConversationModal"; diff --git a/src/core/client/admin/components/ModerateCard/CommentRevisionContainer.tsx b/src/core/client/admin/components/ModerateCard/CommentRevisionContainer.tsx index 8ff66b463..195023270 100644 --- a/src/core/client/admin/components/ModerateCard/CommentRevisionContainer.tsx +++ b/src/core/client/admin/components/ModerateCard/CommentRevisionContainer.tsx @@ -6,7 +6,7 @@ import { HorizontalGutter, Timestamp } from "coral-ui/components/v2"; import { CommentRevisionContainer_comment as CommentData } from "coral-admin/__generated__/CommentRevisionContainer_comment.graphql"; import { CommentRevisionContainer_settings as SettingsData } from "coral-admin/__generated__/CommentRevisionContainer_settings.graphql"; -import CommentContent from "./CommentContent"; +import { CommentContent } from "../Comment"; interface Props { comment: CommentData; diff --git a/src/core/client/admin/components/ModerateCard/ModerateCard.css b/src/core/client/admin/components/ModerateCard/ModerateCard.css index 84aafec8f..22e18525b 100644 --- a/src/core/client/admin/components/ModerateCard/ModerateCard.css +++ b/src/core/client/admin/components/ModerateCard/ModerateCard.css @@ -15,26 +15,6 @@ $moderateCardLinkTextColor: var(--v2-colors-teal-700); margin-bottom: var(--v2-spacing-4); } -.usernameButton { - margin-right: var(--mini-unit); - padding: var(--spacing-1); - margin-left: calc(-1 * var(--v2-spacing-1)); - - line-height: var(--v2-line-height-reset); - - &:hover { - background-color: $moderateCardUsernameBackgroundHover; - border-radius: var(--v2-round-corners); - border-style: none; - } - - &:active { - background-color: $moderateCardUsernameBackgroundHover; - border-radius: var(--v2-round-corners); - border-style: none; - } -} - .inReplyTo { } diff --git a/src/core/client/admin/components/ModerateCard/ModerateCard.spec.tsx b/src/core/client/admin/components/ModerateCard/ModerateCard.spec.tsx index 881497c03..d2ce6cecb 100644 --- a/src/core/client/admin/components/ModerateCard/ModerateCard.spec.tsx +++ b/src/core/client/admin/components/ModerateCard/ModerateCard.spec.tsx @@ -35,6 +35,7 @@ const baseProps: PropTypesOf = { onBan: noop, onUsernameClick: noop, onFocusOrClick: noop, + onConversationClick: noop, showStory: false, moderatedBy: null, }; diff --git a/src/core/client/admin/components/ModerateCard/ModerateCard.tsx b/src/core/client/admin/components/ModerateCard/ModerateCard.tsx index 411690904..3ac3b34fb 100644 --- a/src/core/client/admin/components/ModerateCard/ModerateCard.tsx +++ b/src/core/client/admin/components/ModerateCard/ModerateCard.tsx @@ -13,7 +13,8 @@ import { HOTKEYS } from "coral-admin/constants"; import { GetPhrasesRegExpOptions } from "coral-admin/helpers"; import { PropTypesOf } from "coral-framework/types"; import { - BaseButton, + Button, + ButtonIcon, Card, Flex, HorizontalGutter, @@ -22,14 +23,12 @@ import { Timestamp, } from "coral-ui/components/v2"; +import { CommentContent, InReplyTo, UsernameButton } from "../Comment"; import ApproveButton from "./ApproveButton"; import CommentAuthorContainer from "./CommentAuthorContainer"; -import CommentContent from "./CommentContent"; import FeatureButton from "./FeatureButton"; -import InReplyTo from "./InReplyTo"; import MarkersContainer from "./MarkersContainer"; import RejectButton from "./RejectButton"; -import Username from "./Username"; import styles from "./ModerateCard.css"; @@ -59,6 +58,7 @@ interface Props { onReject: () => void; onFeature: () => void; onUsernameClick: (id?: string) => void; + onConversationClick: (id?: string) => void; onFocusOrClick: () => void; mini?: boolean; hideUsername?: boolean; @@ -102,6 +102,7 @@ const ModerateCard: FunctionComponent = ({ moderatedBy, selected, onFocusOrClick, + onConversationClick, mini = false, hideUsername = false, deleted = false, @@ -165,6 +166,9 @@ const ModerateCard: FunctionComponent = ({ onUsernameClick(inReplyTo.id); } }, [onUsernameClick, inReplyTo]); + const viewConversationClick = useCallback(() => { + onConversationClick(); + }, [onConversationClick]); return ( = ({ {!hideUsername && username && ( <> - - {username} - + /> )} @@ -224,15 +226,12 @@ const ModerateCard: FunctionComponent = ({ {commentBody}
- - - View Context - - +
void; + onConversationClicked?: (commentID: string) => void; onSetSelected?: () => void; selected?: boolean; selectPrev?: () => void; @@ -86,6 +87,7 @@ const ModerateCardContainer: FunctionComponent = ({ selectPrev, selectNext, onUsernameClicked: usernameClicked, + onConversationClicked: conversationClicked, onSetSelected: setSelected, banUser, loadNext, @@ -160,6 +162,16 @@ const ModerateCardContainer: FunctionComponent = ({ [usernameClicked, comment] ); + const onConversationClicked = useCallback( + (id?: string) => { + if (!conversationClicked) { + return; + } + conversationClicked(id || comment.id); + }, + [conversationClicked, comment] + ); + const handleModerateStory = useCallback( (e: React.MouseEvent) => { router.push(getModerationLink({ storyID: comment.story.id })); @@ -227,6 +239,7 @@ const ModerateCardContainer: FunctionComponent = ({ onReject={handleReject} onFeature={onFeature} onUsernameClick={onUsernameClicked} + onConversationClick={onConversationClicked} selected={selected} selectPrev={selectPrev} selectNext={selectNext} diff --git a/src/core/client/admin/components/ModerateCard/__snapshots__/ModerateCard.spec.tsx.snap b/src/core/client/admin/components/ModerateCard/__snapshots__/ModerateCard.spec.tsx.snap index d27c8d605..4db3819ce 100644 --- a/src/core/client/admin/components/ModerateCard/__snapshots__/ModerateCard.spec.tsx.snap +++ b/src/core/client/admin/components/ModerateCard/__snapshots__/ModerateCard.spec.tsx.snap @@ -19,14 +19,10 @@ exports[`renders approved correctly 1`] = ` alignItems="center" > - - - Theon - - + username="Theon" + /> @@ -67,17 +63,22 @@ exports[`renders approved correctly 1`] = `
- - + question_answer + + - View Context - - + + View conversation + + +
- - - Theon - - + username="Theon" + /> @@ -200,17 +197,22 @@ exports[`renders correctly 1`] = `
- - + question_answer + + - View Context - - + + View conversation + + +
- - - Theon - - + username="Theon" + /> @@ -333,17 +331,22 @@ exports[`renders dangling correctly 1`] = `
- - + question_answer + + - View Context - - + + View conversation + + +
- - - Theon - - + username="Theon" + /> @@ -466,17 +465,22 @@ exports[`renders rejected correctly 1`] = `
- - + question_answer + + - View Context - - + + View conversation + + +
- - - Theon - - + username="Theon" + /> @@ -608,17 +608,22 @@ exports[`renders reply correctly 1`] = `
- - + question_answer + + - View Context - - + + View conversation + + +
- - - Theon - - + username="Theon" + /> @@ -741,17 +742,22 @@ exports[`renders story info 1`] = `
- - + question_answer + + - View Context - - + + View conversation + + +
- - - Theon - - + username="Theon" + /> @@ -920,17 +922,22 @@ exports[`renders tombstoned when comment is deleted 1`] = `
- - + question_answer + + - View Context - - + + View conversation + + +
= props => { + return ( +
+ + {props.hollow && ( + + )} + {!props.hollow && } + +
+ ); +}; + +Circle.defaultProps = { + size: "regular", + color: "regular", +}; + +export default Circle; diff --git a/src/core/client/admin/components/Timeline/Line.css b/src/core/client/admin/components/Timeline/Line.css new file mode 100644 index 000000000..4dee7a2c0 --- /dev/null +++ b/src/core/client/admin/components/Timeline/Line.css @@ -0,0 +1,9 @@ +.root { + border-left: 1px solid var(--palette-grey-main); + border-right: 1px solid var(--palette-grey-main); + flex: 1; +} + +.dotted { + border-left-style: dotted; +} diff --git a/src/core/client/admin/components/Timeline/Line.spec.tsx b/src/core/client/admin/components/Timeline/Line.spec.tsx new file mode 100644 index 000000000..99f81bb4b --- /dev/null +++ b/src/core/client/admin/components/Timeline/Line.spec.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { createRenderer } from "react-test-renderer/shallow"; + +import { PropTypesOf } from "coral-framework/types"; + +import Line from "./Line"; + +it("renders correctly", () => { + const props: PropTypesOf = { + className: "root", + dotted: true, + }; + const renderer = createRenderer(); + renderer.render(); + expect(renderer.getRenderOutput()).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/components/Timeline/Line.tsx b/src/core/client/admin/components/Timeline/Line.tsx new file mode 100644 index 000000000..2c7dcab11 --- /dev/null +++ b/src/core/client/admin/components/Timeline/Line.tsx @@ -0,0 +1,23 @@ +import cn from "classnames"; +import React, { FunctionComponent } from "react"; + +import styles from "./Line.css"; + +interface LineProps { + className?: string; + dotted?: boolean; +} + +const Line: FunctionComponent = props => { + return ( +
+ {props.children} +
+ ); +}; + +export default Line; diff --git a/src/core/client/admin/components/Timeline/__snapshots__/Line.spec.tsx.snap b/src/core/client/admin/components/Timeline/__snapshots__/Line.spec.tsx.snap new file mode 100644 index 000000000..c22ace86c --- /dev/null +++ b/src/core/client/admin/components/Timeline/__snapshots__/Line.spec.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+`; diff --git a/src/core/client/admin/components/Timeline/index.ts b/src/core/client/admin/components/Timeline/index.ts new file mode 100644 index 000000000..7eda1e131 --- /dev/null +++ b/src/core/client/admin/components/Timeline/index.ts @@ -0,0 +1,2 @@ +export { default as Circle } from "./Circle"; +export { default as Line } from "./Line"; diff --git a/src/core/client/admin/routes/Moderate/Queue/Queue.tsx b/src/core/client/admin/routes/Moderate/Queue/Queue.tsx index 34f2c278f..cb12592c2 100644 --- a/src/core/client/admin/routes/Moderate/Queue/Queue.tsx +++ b/src/core/client/admin/routes/Moderate/Queue/Queue.tsx @@ -2,6 +2,7 @@ import { Localized } from "@fluent/react/compat"; import React, { FunctionComponent, useCallback, useState } from "react"; import AutoLoadMore from "coral-admin/components/AutoLoadMore"; +import ConversationModal from "coral-admin/components/ConversationModal"; import ModerateCardContainer from "coral-admin/components/ModerateCard"; import UserHistoryDrawer from "coral-admin/components/UserHistoryDrawer"; import { HOTKEYS } from "coral-admin/constants"; @@ -46,6 +47,10 @@ const Queue: FunctionComponent = ({ const [userDrawerId, setUserDrawerID] = useState(""); const [selectedComment, setSelectedComment] = useState(0); const [singleView, setSingleView] = useState(false); + const [conversationModalVisible, setConversationModalVisible] = useState( + false + ); + const [conversationCommentID, setConversationCommentID] = useState(""); const toggleView = useCallback(() => { if (!singleView) { @@ -94,6 +99,23 @@ const Queue: FunctionComponent = ({ setUserDrawerID(""); }, []); + const onShowConversationModal = useCallback((commentID: string) => { + setConversationCommentID(commentID); + setConversationModalVisible(true); + }, []); + + const onHideConversationModal = useCallback(() => { + setConversationModalVisible(false); + setConversationCommentID(""); + }, []); + + const onUsernameClickedFromModal = useCallback((userID: string) => { + setUserDrawerID(userID); + setUserDrawerVisible(true); + setConversationModalVisible(false); + setConversationCommentID(""); + }, []); + return ( {Boolean(viewNewCount && viewNewCount > 0) && ( @@ -119,6 +141,7 @@ const Queue: FunctionComponent = ({ danglingLogic={danglingLogic} showStoryInfo={Boolean(allStories)} onUsernameClicked={onShowUserDrawer} + onConversationClicked={onShowConversationModal} onSetSelected={() => setSelectedComment(i)} selected={selectedComment === i} selectPrev={selectPrev} @@ -141,6 +164,12 @@ const Queue: FunctionComponent = ({ onClose={onHideUserDrawer} userID={userDrawerId} /> + ); }; 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 58b33fd30..3a7427b68 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 @@ -24,6 +24,12 @@ exports[`renders correctly with load more 1`] = ` open={false} userID="" /> + `; @@ -43,5 +49,11 @@ exports[`renders correctly without load more 1`] = ` open={false} userID="" /> + `; 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 c376462d7..d1080f22a 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 @@ -34,7 +34,7 @@ exports[`approves comment in reported queue: dangling 1`] = ` className="Box-root Flex-root Flex-flex Flex-alignCenter" >
{ /** Internal: Forwarded Ref */ forwardRef?: Ref; + + /* if true adds a link-style underline */ + underline?: boolean; } export class Button extends React.Component { @@ -82,6 +85,7 @@ export class Button extends React.Component { iconRight, adornmentLeft, adornmentRight, + underline, to, ...rest } = this.props; @@ -108,6 +112,7 @@ export class Button extends React.Component { [classes.iconRight]: iconRight, [classes.adornmentLeft]: adornmentLeft, [classes.adornmentRight]: adornmentRight, + [classes.underline]: underline, }, className ); diff --git a/src/core/client/ui/components/v2/Modal/ModalHeader.css b/src/core/client/ui/components/v2/Modal/ModalHeader.css new file mode 100644 index 000000000..a3b445dc1 --- /dev/null +++ b/src/core/client/ui/components/v2/Modal/ModalHeader.css @@ -0,0 +1,6 @@ +$modal-header-background: var(--v2-colors-white-500); + +.root { + background-color: $modal-header-background; + padding: var(--v2-spacing-4); +} diff --git a/src/core/client/ui/components/v2/Modal/ModalHeader.tsx b/src/core/client/ui/components/v2/Modal/ModalHeader.tsx new file mode 100644 index 000000000..3beb6bada --- /dev/null +++ b/src/core/client/ui/components/v2/Modal/ModalHeader.tsx @@ -0,0 +1,27 @@ +import React, { FunctionComponent, RefObject } from "react"; + +import { CardCloseButton, Flex } from "coral-ui/components/v2"; + +import styles from "./ModalHeader.css"; + +interface Props { + onClose: () => void; + focusableRef: RefObject; +} + +const ModalHeader: FunctionComponent = ({ + onClose, + focusableRef, + children, +}) => { + return ( +
+ + {children} + + +
+ ); +}; + +export default ModalHeader; diff --git a/src/core/client/ui/components/v2/Modal/index.ts b/src/core/client/ui/components/v2/Modal/index.ts index 2ddfc1688..fd37d5994 100644 --- a/src/core/client/ui/components/v2/Modal/index.ts +++ b/src/core/client/ui/components/v2/Modal/index.ts @@ -1 +1,2 @@ export { default, ModalProps } from "./Modal"; +export { default as ModalHeader } from "./ModalHeader"; diff --git a/src/core/client/ui/components/v2/index.ts b/src/core/client/ui/components/v2/index.ts index 08fb69730..f5783e6db 100644 --- a/src/core/client/ui/components/v2/index.ts +++ b/src/core/client/ui/components/v2/index.ts @@ -39,7 +39,7 @@ export { default as Label } from "./Label"; export { ListGroup, ListGroupRow } from "./ListGroup"; export { Marker, Count as MarkerCount } from "./Marker"; export { default as Message, MessageIcon } from "./Message"; -export { default as Modal, ModalProps } from "./Modal"; +export { default as Modal, ModalProps, ModalHeader } from "./Modal"; export { default as PasswordField } from "./PasswordField"; export { default as Popover } from "./Popover"; export { default as RadioButton } from "./RadioButton"; diff --git a/src/locales/en-US/admin.ftl b/src/locales/en-US/admin.ftl index 36a53526d..51185639f 100644 --- a/src/locales/en-US/admin.ftl +++ b/src/locales/en-US/admin.ftl @@ -716,6 +716,7 @@ moderate-emptyQueue-approved = There are no approved comments. moderate-comment-edited = (edited) moderate-comment-inReplyTo = Reply to moderate-comment-viewContext = View Context +moderate-comment-viewConversation = View Conversation moderate-comment-rejectButton = .aria-label = Reject moderate-comment-approveButton =