From a1c399a18f451981597ac6c36b990534a2a699fc Mon Sep 17 00:00:00 2001 From: Rodrigo Iloro Date: Tue, 14 Nov 2017 15:43:14 -0300 Subject: [PATCH] Comments: add single comment view (#19635) * Comments: sidebar menu item is selected on both list and single comment view * Comments: updates link on the author date to the single comment view * Comments: adds moderation data component --- assets/stylesheets/_components.scss | 1 + .../data/moderate-comment/index.jsx | 110 ++++++++++++++++++ client/my-sites/comment/comment-permalink.jsx | 46 ++++++++ client/my-sites/comment/main.jsx | 82 +++++++++++++ client/my-sites/comment/style.scss | 14 +++ .../comments/comment/comment-author.jsx | 20 +++- client/my-sites/comments/controller.js | 33 ++++-- client/my-sites/sidebar/manage-menu.jsx | 1 + 8 files changed, 295 insertions(+), 12 deletions(-) create mode 100644 client/components/data/moderate-comment/index.jsx create mode 100644 client/my-sites/comment/comment-permalink.jsx create mode 100644 client/my-sites/comment/main.jsx create mode 100644 client/my-sites/comment/style.scss diff --git a/assets/stylesheets/_components.scss b/assets/stylesheets/_components.scss index 93f9abb653c854..9a1a0acac9ab70 100644 --- a/assets/stylesheets/_components.scss +++ b/assets/stylesheets/_components.scss @@ -321,6 +321,7 @@ @import 'my-sites/ads/style'; @import 'my-sites/all-sites/style'; @import 'my-sites/all-sites-icon/style'; +@import 'my-sites/comment/style'; @import 'my-sites/comments/style'; @import 'my-sites/comments/comment/style'; @import 'my-sites/current-site/style'; diff --git a/client/components/data/moderate-comment/index.jsx b/client/components/data/moderate-comment/index.jsx new file mode 100644 index 00000000000000..6500ee638c30e7 --- /dev/null +++ b/client/components/data/moderate-comment/index.jsx @@ -0,0 +1,110 @@ +/** @format */ +/** + * External dependencies + * + */ +import { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { get } from 'lodash'; + +/** + * Internal dependencies + */ +import { + bumpStat, + composeAnalytics, + recordTracksEvent, + withAnalytics, +} from 'state/analytics/actions'; +import { changeCommentStatus, deleteComment } from 'state/comments/actions'; +import { getSiteComment } from 'state/selectors'; + +class ModerateComment extends Component { + static propTypes = { + siteId: PropTypes.number, + postId: PropTypes.number, + commentId: PropTypes.number.isRequired, + newStatus: PropTypes.string, + currentStatus: PropTypes.string, + updateCommentStatus: PropTypes.func.isRequired, + destroyComment: PropTypes.func.isRequired, + }; + + componentDidMount() { + this.moderate( this.props ); + } + + componentWillReceiveProps( nextProps ) { + if ( + this.props.siteId === nextProps.siteId && + this.props.postId === nextProps.postId && + this.props.commentId === nextProps.commentId && + this.props.newStatus === nextProps.newStatus + ) { + return; + } + + this.moderate( nextProps ); + } + + moderate( { + siteId, + postId, + commentId, + newStatus, + currentStatus, + updateCommentStatus, + destroyComment, + } ) { + if ( ! siteId || ! postId || ! commentId || ! newStatus || newStatus === currentStatus ) { + return; + } + + if ( 'delete' === newStatus ) { + destroyComment(); + return; + } + + updateCommentStatus(); + } + + render() { + return null; + } +} + +const mapStateToProps = ( state, { siteId, commentId } ) => { + const comment = getSiteComment( state, siteId, commentId ); + + return { + currentStatus: get( comment, 'status' ), + }; +}; + +const mapDispatchToProps = ( dispatch, { siteId, postId, commentId, newStatus } ) => ( { + updateCommentStatus: () => + dispatch( + withAnalytics( + composeAnalytics( + recordTracksEvent( 'calypso_comment_management_change_status_from_email', { + status: newStatus, + } ), + bumpStat( 'calypso_comment_management', 'comment_status_changed_to_' + newStatus ) + ), + changeCommentStatus( siteId, postId, commentId, newStatus ) + ) + ), + destroyComment: () => + dispatch( + withAnalytics( + composeAnalytics( + recordTracksEvent( 'calypso_comment_management_delete' ), + bumpStat( 'calypso_comment_management', 'comment_deleted' ) + ), + deleteComment( siteId, postId, commentId ) + ) + ), +} ); + +export default connect( mapStateToProps, mapDispatchToProps )( ModerateComment ); diff --git a/client/my-sites/comment/comment-permalink.jsx b/client/my-sites/comment/comment-permalink.jsx new file mode 100644 index 00000000000000..3182e20ba34301 --- /dev/null +++ b/client/my-sites/comment/comment-permalink.jsx @@ -0,0 +1,46 @@ +/** @format */ +/** + * External dependencies + */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { localize } from 'i18n-calypso'; +import { get, isUndefined } from 'lodash'; + +/** + * Internal dependencies + */ +import Card from 'components/card'; +import SectionHeader from 'components/section-header'; +import ExternalLink from 'components/external-link'; +import { getSiteComment } from 'state/selectors'; + +const CommentPermalink = ( { isLoading, permaLink, translate } ) => + ! isLoading && ( + + + + { permaLink } + + + ); + +CommentPermalink.propTypes = { + siteId: PropTypes.number, + commentId: PropTypes.number.isRequired, + isLoading: PropTypes.bool.isRequired, + permaLink: PropTypes.string, + translate: PropTypes.func.isRequired, +}; + +const mapStateToProps = ( state, { siteId, commentId } ) => { + const comment = getSiteComment( state, siteId, commentId ); + + return { + isLoading: isUndefined( comment ), + permaLink: get( comment, 'URL', '' ), + }; +}; + +export default connect( mapStateToProps )( localize( CommentPermalink ) ); diff --git a/client/my-sites/comment/main.jsx b/client/my-sites/comment/main.jsx new file mode 100644 index 00000000000000..8f4efe5761d295 --- /dev/null +++ b/client/my-sites/comment/main.jsx @@ -0,0 +1,82 @@ +/** @format */ +/** + * External dependencies + */ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { localize } from 'i18n-calypso'; +import { get } from 'lodash'; + +/** + * Internal dependencies + */ +import Main from 'components/main'; +import EmptyContent from 'components/empty-content'; +import DocumentHead from 'components/data/document-head'; +import ModerateComment from 'components/data/moderate-comment'; +import Comment from 'my-sites/comments/comment'; +import CommentPermalink from 'my-sites/comment/comment-permalink'; +import CommentListHeader from 'my-sites/comments/comment-list/comment-list-header'; +import PageViewTracker from 'lib/analytics/page-view-tracker'; +import { preventWidows } from 'lib/formatting'; +import { getSiteComment, canCurrentUser } from 'state/selectors'; +import getSiteId from 'state/selectors/get-site-id'; + +export class CommentView extends Component { + static propTypes = { + siteId: PropTypes.number, + postId: PropTypes.number, + commentId: PropTypes.number.isRequired, + action: PropTypes.string, + canModerateComments: PropTypes.bool.isRequired, + translate: PropTypes.func.isRequired, + }; + + render() { + const { siteId, postId, commentId, action, canModerateComments, translate } = this.props; + + return ( + // eslint-disable-next-line wpcalypso/jsx-classname-namespace +
+ + + { canModerateComments && ( + + ) } + + { ! canModerateComments && ( + + ) } + { canModerateComments && } + { canModerateComments && } +
+ ); + } +} + +const mapStateToProps = ( state, ownProps ) => { + const { commentId, siteFragment } = ownProps; + + const siteId = getSiteId( state, siteFragment ); + const comment = getSiteComment( state, siteId, commentId ); + const postId = get( comment, 'post.ID' ); + + const canModerateComments = canCurrentUser( state, siteId, 'moderate_comments' ) !== false; + + return { + siteId, + postId, + canModerateComments, + }; +}; + +export default connect( mapStateToProps )( localize( CommentView ) ); diff --git a/client/my-sites/comment/style.scss b/client/my-sites/comment/style.scss new file mode 100644 index 00000000000000..1809898078bc3b --- /dev/null +++ b/client/my-sites/comment/style.scss @@ -0,0 +1,14 @@ +.comment__comment-permalink { + padding: 0; + margin-top: 16px; + + .section-header { + margin: 0; + } + + .external-link { + display: block; + padding: 24px; + word-wrap: break-word; + } +} diff --git a/client/my-sites/comments/comment/comment-author.jsx b/client/my-sites/comments/comment/comment-author.jsx index f3ee2a09cf877e..2243abcc12ce6e 100644 --- a/client/my-sites/comments/comment/comment-author.jsx +++ b/client/my-sites/comments/comment/comment-author.jsx @@ -12,6 +12,7 @@ import { get } from 'lodash'; /** * Internal dependencies */ +import { isEnabled } from 'config'; import Emojify from 'components/emojify'; import ExternalLink from 'components/external-link'; import Gravatar from 'components/gravatar'; @@ -19,7 +20,7 @@ import CommentPostLink from 'my-sites/comments/comment/comment-post-link'; import { decodeEntities } from 'lib/formatting'; import { urlToDomainAndPath } from 'lib/url'; import { getSiteComment } from 'state/selectors'; -import { getSelectedSiteId } from 'state/ui/selectors'; +import { getSelectedSiteId, getSelectedSiteSlug } from 'state/ui/selectors'; export class CommentAuthor extends Component { static propTypes = { @@ -86,9 +87,15 @@ export class CommentAuthor extends Component {
- - { relativeDate } - + { isEnabled( 'comments/management/comment-view' ) ? ( + + { relativeDate } + + ) : ( + + { relativeDate } + + ) } { authorUrl && ( @@ -107,6 +114,7 @@ export class CommentAuthor extends Component { const mapStateToProps = ( state, { commentId } ) => { const siteId = getSelectedSiteId( state ); + const siteSlug = getSelectedSiteSlug( state ); const comment = getSiteComment( state, siteId, commentId ); const authorAvatarUrl = get( comment, 'author.avatar_URL' ); const authorDisplayName = decodeEntities( get( comment, 'author.name' ) ); @@ -119,7 +127,9 @@ const mapStateToProps = ( state, { commentId } ) => { commentContent: get( comment, 'content' ), commentDate: get( comment, 'date' ), commentType: get( comment, 'type', 'comment' ), - commentUrl: get( comment, 'URL' ), + commentUrl: isEnabled( 'comments/management/comment-view' ) + ? `/comment/${ siteSlug }/${ commentId }` + : get( comment, 'URL' ), gravatarUser, }; }; diff --git a/client/my-sites/comments/controller.js b/client/my-sites/comments/controller.js index 86593f0969a67c..87c4a5b34bbeb5 100644 --- a/client/my-sites/comments/controller.js +++ b/client/my-sites/comments/controller.js @@ -2,7 +2,6 @@ /** * External dependencies */ -import { renderWithReduxStore } from 'lib/react-helpers'; import React from 'react'; import page from 'page'; import { each, isNaN, startsWith } from 'lodash'; @@ -10,8 +9,11 @@ import { each, isNaN, startsWith } from 'lodash'; /** * Internal dependencies */ -import CommentsManagement from './main'; +import { isEnabled } from 'config'; +import { renderWithReduxStore } from 'lib/react-helpers'; import route, { addQueryArgs } from 'lib/route'; +import CommentsManagement from './main'; +import CommentView from 'my-sites/comment/main'; import { removeNotice } from 'state/notices/actions'; import { getNotices } from 'state/notices/selectors'; @@ -22,6 +24,23 @@ const sanitizeInt = number => { return ! isNaN( integer ) && integer > 0 ? integer : false; }; +const sanitizeQueryAction = action => { + if ( ! action ) { + return null; + } + + const validActions = { + approve: 'approved', + unapprove: 'unapproved', + trash: 'trash', + spam: 'spam', + delete: 'delete', + }; + return validActions.hasOwnProperty( action.toLowerCase() ) + ? validActions[ action.toLowerCase() ] + : null; +}; + const changePage = path => pageNumber => { if ( window ) { window.scrollTo( 0, 0 ); @@ -46,7 +65,6 @@ export const siteComments = context => { renderWithReduxStore( { renderWithReduxStore( { ); }; -export const comment = ( { params, path, store } ) => { +export const comment = ( { query, params, path, store } ) => { const siteFragment = route.getSiteFragment( path ); const commentId = sanitizeInt( params.comment ); - if ( ! commentId ) { + if ( ! commentId || ! isEnabled( 'comments/management/m3-design' ) ) { return siteFragment ? page.redirect( `/comments/all/${ siteFragment }` ) : page.redirect( '/comments/all' ); } + const action = sanitizeQueryAction( query.action ); + renderWithReduxStore( - , + , 'primary', store ); diff --git a/client/my-sites/sidebar/manage-menu.jsx b/client/my-sites/sidebar/manage-menu.jsx index 07d2dbe7a5c1ac..55a90f2f8d96f7 100644 --- a/client/my-sites/sidebar/manage-menu.jsx +++ b/client/my-sites/sidebar/manage-menu.jsx @@ -111,6 +111,7 @@ class ManageMenu extends PureComponent { queryable: true, config: 'comments/management', link: '/comments', + paths: [ '/comment', '/comments' ], wpAdminLink: 'edit-comments.php', showOnAllMySites: false, },