diff --git a/news/4074.feature b/news/4074.feature
new file mode 100644
index 0000000000..97c90d3e17
--- /dev/null
+++ b/news/4074.feature
@@ -0,0 +1 @@
+Refactor Comment -@Tishasoumya-02
diff --git a/src/components/theme/Comments/Comment.stories.jsx b/src/components/theme/Comments/Comment.stories.jsx
new file mode 100644
index 0000000000..b2d327c08f
--- /dev/null
+++ b/src/components/theme/Comments/Comment.stories.jsx
@@ -0,0 +1,84 @@
+import { injectIntl } from 'react-intl';
+import React from 'react';
+import CommentsComponent from './Comments';
+import { RealStoreWrapper as Wrapper } from '@plone/volto/storybook';
+
+const IntlCommentsComponent = injectIntl(CommentsComponent);
+
+function StoryComponent(args) {
+ return (
+
+
+
+
+ );
+}
+
+export const CommentsModal = StoryComponent.bind({});
+CommentsModal.args = {
+ author_name: 'admin',
+ creation_date: '2017-11-06T19:36:01',
+ text: { data: 'Plone 6' },
+};
+export default {
+ title: 'Public components/Comments/Comments Modal',
+ component: CommentsComponent,
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ argTypes: {
+ creation_date: {
+ control: 'date',
+ description: 'Date comment was created',
+ },
+ author_name: {
+ control: 'text',
+ description: 'Comment author name',
+ },
+ text: {
+ data: {
+ control: 'date',
+ },
+ description: 'Comment text',
+ },
+ },
+};
diff --git a/src/components/theme/Comments/Comments.jsx b/src/components/theme/Comments/Comments.jsx
index 988e36226b..6e284fa42f 100644
--- a/src/components/theme/Comments/Comments.jsx
+++ b/src/components/theme/Comments/Comments.jsx
@@ -1,8 +1,12 @@
-/**
- * Comments components.
- * @module components/theme/Comments/Comments
- */
+import { useEffect, useState, useMemo, useCallback } from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+import { Portal } from 'react-portal';
+import { useDispatch, useSelector, shallowEqual } from 'react-redux';
+import { compose } from 'redux';
+import { Button, Comment, Container, Icon } from 'semantic-ui-react';
+import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
import {
addComment,
deleteComment,
@@ -10,16 +14,12 @@ import {
listMoreComments,
} from '@plone/volto/actions';
import { Avatar, CommentEditModal, Form } from '@plone/volto/components';
-import { flattenToAppURL, getBaseUrl, getColor } from '@plone/volto/helpers';
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
-import { Portal } from 'react-portal';
-import { connect } from 'react-redux';
-import { compose } from 'redux';
-import { Button, Comment, Container, Icon } from 'semantic-ui-react';
-import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
-// import { Button, Grid, Segment, Container } from 'semantic-ui-react';
+import {
+ flattenToAppURL,
+ getBaseUrl,
+ getColor,
+ usePrevious,
+} from '@plone/volto/helpers';
const messages = defineMessages({
comment: {
@@ -65,10 +65,7 @@ const messages = defineMessages({
defaultMessage: 'Load more...',
},
});
-/**
- * Schema for the Form components to show an input field with it's label
- * @param {Object} intl
- */
+
const makeFormSchema = (intl) => ({
fieldsets: [
{
@@ -87,193 +84,114 @@ const makeFormSchema = (intl) => ({
required: ['comment1'],
});
-/**
- * Comments container class.
- * @class Comments
- * @extends Component
- */
-class Comments extends Component {
- /**
- * Property types.
- * @property {Object} propTypes Property types.
- * @static
- */
- static propTypes = {
- addComment: PropTypes.func.isRequired,
- deleteComment: PropTypes.func.isRequired,
- listComments: PropTypes.func.isRequired,
- listMoreComments: PropTypes.func.isRequired,
- pathname: PropTypes.string.isRequired,
- items: PropTypes.arrayOf(
- PropTypes.shape({
- author_name: PropTypes.string,
- creation_date: PropTypes.string,
- text: PropTypes.shape({
- data: PropTypes.string,
- 'mime-type': PropTypes.string,
- }),
- is_deletable: PropTypes.bool,
- is_editable: PropTypes.bool,
- }),
- ).isRequired,
- addRequest: PropTypes.shape({
- loading: PropTypes.bool,
- loaded: PropTypes.bool,
- }).isRequired,
- deleteRequest: PropTypes.shape({
- loading: PropTypes.bool,
- loaded: PropTypes.bool,
- }).isRequired,
- };
+const useComments = () => {
+ const items = useSelector((state) => state.comments.items, shallowEqual);
+ const next = useSelector((state) => state.comments.next, shallowEqual);
+ const items_total = useSelector(
+ (state) => state.comments.items_total,
+ shallowEqual,
+ );
+ const permissions = useSelector(
+ (state) => state.comments.permissions || {},
+ shallowEqual,
+ );
+ const addRequest = useSelector((state) => state.comments.add, shallowEqual);
+ const deleteRequest = useSelector(
+ (state) => state.comments.delete,
+ shallowEqual,
+ );
- /**
- * Constructor
- * @method constructor
- * @param {Object} props Component properties
- * @constructs Comments
- */
- constructor(props) {
- super(props);
- this.onSubmit = this.onSubmit.bind(this);
- this.onDelete = this.onDelete.bind(this);
- this.onEdit = this.onEdit.bind(this);
- this.onEditOk = this.onEditOk.bind(this);
- this.onEditCancel = this.onEditCancel.bind(this);
- this.setReplyTo = this.setReplyTo.bind(this);
- this.loadMoreComments = this.loadMoreComments.bind(this);
- this.state = {
- showEdit: false,
- editId: null,
- editText: null,
- replyTo: null,
- collapsedComments: {},
- };
- }
+ return { items, next, items_total, permissions, addRequest, deleteRequest };
+};
- componentDidMount() {
- this.props.listComments(getBaseUrl(this.props.pathname));
- }
+const Comments = (props) => {
+ const intl = useIntl();
+ const dispatch = useDispatch();
+ const { pathname } = props;
+ const [showEdit, setshowEdit] = useState(false);
+ const [editId, seteditId] = useState(null);
+ const [editText, seteditText] = useState(null);
+ const [replyTo, setreplyTo] = useState(null);
+ const [collapsedComments, setcollapsedComments] = useState({});
+ const {
+ items,
+ next,
+ items_total,
+ permissions,
+ addRequest,
+ deleteRequest,
+ } = useComments();
- /**
- * Component will receive props
- * @method componentWillReceiveProps
- * @param {Object} nextProps Next properties
- * @returns {undefined}
- */
- UNSAFE_componentWillReceiveProps(nextProps) {
+ const prevpathname = usePrevious(pathname);
+
+ const prevaddRequestLoading = usePrevious(addRequest.loading);
+ const prevdeleteRequestLoading = usePrevious(deleteRequest.loading);
+
+ useEffect(() => {
if (
- nextProps.pathname !== this.props.pathname ||
- (this.props.addRequest.loading && nextProps.addRequest.loaded) ||
- (this.props.deleteRequest.loading && nextProps.deleteRequest.loaded)
+ pathname !== prevpathname ||
+ (prevaddRequestLoading && addRequest.loaded) ||
+ (prevdeleteRequestLoading && deleteRequest.loaded)
) {
- this.props.listComments(getBaseUrl(nextProps.pathname));
+ dispatch(listComments(getBaseUrl(pathname)));
}
- }
+ }, [
+ dispatch,
+ pathname,
+ prevpathname,
+ prevaddRequestLoading,
+ addRequest.loaded,
+ prevdeleteRequestLoading,
+ deleteRequest.loaded,
+ ]);
- /**
- * Submit handler
- * @method onSubmit
- * @param {Object} formData Form data.
- * @returns {undefined}
- */
- onSubmit(formData) {
- this.props.addComment(
- getBaseUrl(this.props.pathname),
- formData.comment,
- this.state.replyTo,
- );
- this.setState({ replyTo: null });
- }
-
- /**
- * The id of the comment that will receive a reply
- * @param {string} commentId
- */
- setReplyTo(commentId) {
- this.setState({ replyTo: commentId });
- }
+ const onSubmit = (formData) => {
+ dispatch(addComment(getBaseUrl(pathname), formData.comment, replyTo));
+ setreplyTo(null);
+ };
- /**
- * Calls the action listMoreComments passing the received url for next array of comments
- */
- loadMoreComments() {
- this.props.listMoreComments(this.props.next);
- }
+ const setReplyTo = (commentId) => {
+ setreplyTo(commentId);
+ };
- /**
- * Delete handler
- * @method onDelete
- * @param {Object} event Event object.
- * @param {string} value Delete value.
- * @returns {undefined}
- */
- onDelete(value) {
- this.props.deleteComment(value);
- }
+ const loadMoreComments = () => {
+ dispatch(listMoreComments(next));
+ };
- /**
- * Will hide all replies to the specific comment
- * including replies to any of the replies
- * @param {string} commentId
- */
- hideReply(commentId) {
- this.setState((prevState) => {
- const hasComment = prevState.collapsedComments[commentId];
- const { collapsedComments } = prevState;
+ const onDelete = (value) => {
+ dispatch(deleteComment(value));
+ };
+ const prevcollapsedComments = usePrevious(collapsedComments);
- return {
- collapsedComments: {
- ...collapsedComments,
- [commentId]: !hasComment,
- },
- };
- });
- }
+ const hideReply = (commentId) => {
+ const hasComment = prevcollapsedComments[commentId];
+ setcollapsedComments((prevState) => ({
+ ...prevState,
+ [commentId]: !hasComment,
+ }));
+ };
- /**
- * Edit handler
- * @method onEdit
- * @param {Object} event Event object.
- * @param {string} value Delete value.
- * @returns {undefined}
- */
- onEdit(value) {
- this.setState({
- showEdit: true,
- editId: value.id,
- editText: value.text,
- });
- }
+ const onEdit = useCallback((value) => {
+ setshowEdit(true);
+ seteditText(value.text);
+ seteditId(value.id);
+ }, []);
- /**
- * On edit ok
- * @method onEditOk
- * @returns {undefined}
- */
- onEditOk() {
- this.setState({
- showEdit: false,
- editId: null,
- editText: null,
- });
- this.props.listComments(getBaseUrl(this.props.pathname));
- }
+ const onEditOk = () => {
+ setshowEdit(false);
+ seteditId(null);
+ seteditText(null);
+ dispatch(listComments(getBaseUrl(pathname)));
+ };
- /**
- * On edit cancel
- * @method onEditCancel
- * @returns {undefined}
- */
- onEditCancel(ev) {
- this.setState({
- showEdit: false,
- editId: null,
- editText: null,
- replyTo: null,
- });
- }
+ const onEditCancel = useCallback(() => {
+ setshowEdit(false);
+ seteditId(null);
+ seteditText(null);
+ setreplyTo(null);
+ }, []);
- addRepliesAsChildrenToComments(items) {
+ const addRepliesAsChildrenToComments = (items) => {
let initialValue = {};
const allCommentsWithCildren = items.reduce((accumulator, item) => {
return {
@@ -288,205 +206,182 @@ class Comments extends Component {
}
});
return allCommentsWithCildren;
- }
+ };
- /**
- * Render method.
- * @method render
- * @returns {string} Markup for the component.
- */
- render() {
- const { items, permissions } = this.props;
- const moment = this.props.moment.default;
- const { collapsedComments } = this.state;
- // object with comment ids, to easily verify if any comment has children
- const allCommentsWithCildren = this.addRepliesAsChildrenToComments(items);
- // all comments that are not a reply will be shown in the first iteration
- const allPrimaryComments = items.filter((comment) => !comment.in_reply_to);
+ const moment = props.moment.default;
- // recursively makes comments with their replies nested
- // each iteration will show replies to the specific comment using allCommentsWithCildren
- const commentElement = (comment) => (
-
-
-
- {comment.author_name}
-
-
- {' '}
-
- {moment(comment.creation_date).fromNow()}
-
-
-
-
+ const allCommentsWithCildren = useMemo(
+ () => addRepliesAsChildrenToComments(items),
+ [items],
+ );
+ // all comments that are not a reply will be shown in the first iteration
+ const allPrimaryComments = items.filter((comment) => !comment.in_reply_to);
+
+ // recursively makes comments with their replies nested
+ // each iteration will show replies to the specific comment using allCommentsWithCildren
+ const commentElement = (comment) => (
+
+
+
+ {comment.author_name}
+
+
{' '}
- {comment.text['mime-type'] === 'text/html' ? (
-
- ) : (
- comment.text.data
- )}
-
-
- {comment.can_reply && (
- this.setReplyTo(comment.comment_id)}
- >
-
-
- )}
- {comment.is_editable && (
-
- this.onEdit({
- id: flattenToAppURL(comment['@id']),
- text: comment.text.data,
- })
- }
- aria-label={this.props.intl.formatMessage(messages.edit)}
- value={{
+
+ {moment(comment.creation_date).fromNow()}
+
+
+
+
+ {' '}
+ {comment.text['mime-type'] === 'text/html' ? (
+
+ ) : (
+ comment.text.data
+ )}
+
+
+ {comment.can_reply && (
+ setReplyTo(comment.comment_id)}
+ >
+
+
+ )}
+ {comment.is_editable && (
+
+ onEdit({
id: flattenToAppURL(comment['@id']),
text: comment.text.data,
- }}
- >
-
-
- )}
- {comment.is_deletable && (
- this.onDelete(flattenToAppURL(comment['@id']))}
- color="red"
- >
-
-
-
- )}
+ })
+ }
+ aria-label={intl.formatMessage(messages.edit)}
+ value={{
+ id: flattenToAppURL(comment['@id']),
+ text: comment.text.data,
+ }}
+ >
+
+
+ )}
+ {comment.is_deletable && (
this.hideReply(comment.comment_id)}
+ aria-label={intl.formatMessage(messages.delete)}
+ onClick={() => onDelete(flattenToAppURL(comment['@id']))}
+ color="red"
>
- {allCommentsWithCildren[comment.comment_id].children.length >
- 0 ? (
- this.state.collapsedComments[comment.comment_id] ? (
- <>
-
-
- >
- ) : (
- <>
-
-
- >
- )
- ) : null}
+
+
-
-
-
+ )}
+ hideReply(comment.comment_id)}>
+ {allCommentsWithCildren[comment.comment_id].children.length > 0 ? (
+ collapsedComments[comment.comment_id] ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )
+ ) : null}
+
+
+
+
+
+ {allCommentsWithCildren[comment.comment_id].children.length > 0
+ ? allCommentsWithCildren[comment.comment_id].children.map(
+ (child, index) => (
+
+ {commentElement(child)}
+
+ ),
+ )
+ : null}
+
+ );
- {allCommentsWithCildren[comment.comment_id].children.length > 0
- ? allCommentsWithCildren[comment.comment_id].children.map(
- (child, index) => (
-
- {commentElement(child)}
-
- ),
- )
- : null}
-
- );
+ if (!permissions.view_comments) return '';
- if (!permissions.view_comments) return '';
+ return (
+
+
+ {permissions.can_reply && (
+
+ )}
+ {/* all comments */}
+
+ {allPrimaryComments.map((item) => commentElement(item))}
+
- return (
-
-
- {permissions.can_reply && (
-
- )}
- {/* all comments */}
-
- {allPrimaryComments.map((item) => commentElement(item))}
-
+ {/* load more button */}
+ {items_total > items.length && (
+
+ )}
- {/* load more button */}
- {this.props.items_total > this.props.items.length && (
-
- )}
+ {replyTo && (
+
+
+
+ )}
+
+ );
+};
- {this.state.replyTo && (
-
-
-
- )}
-
- );
- }
-}
+Comments.propTypes = {
+ pathname: PropTypes.string.isRequired,
+};
-export default compose(
- injectIntl,
- injectLazyLibs(['moment']),
- connect(
- (state) => ({
- items: state.comments.items,
- next: state.comments.next,
- items_total: state.comments.items_total,
- permissions: state.comments.permissions || {},
- addRequest: state.comments.add,
- deleteRequest: state.comments.delete,
- }),
- { addComment, deleteComment, listComments, listMoreComments },
- ),
-)(Comments);
+export default compose(injectLazyLibs(['moment']))(Comments);