diff --git a/src/discussions/comments/CommentsView.test.jsx b/src/discussions/comments/CommentsView.test.jsx
index d0389e0e..2c921245 100644
--- a/src/discussions/comments/CommentsView.test.jsx
+++ b/src/discussions/comments/CommentsView.test.jsx
@@ -381,6 +381,7 @@ describe('CommentsView', () => {
});
expect(testLocation.pathname).toBe(`/${courseId}/posts/${discussionPostId}/edit`);
});
+
it('should allow pinning the post', async () => {
renderComponent(discussionPostId);
await act(async () => {
@@ -394,6 +395,7 @@ describe('CommentsView', () => {
});
assertLastUpdateData({ pinned: false });
});
+
it('should allow reporting the post', async () => {
renderComponent(discussionPostId);
await act(async () => {
@@ -405,6 +407,11 @@ describe('CommentsView', () => {
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /report/i }));
});
+ expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).toBeInTheDocument();
+ await act(async () => {
+ fireEvent.click(screen.queryByRole('button', { name: /Confirm/i }));
+ });
+ expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).not.toBeInTheDocument();
assertLastUpdateData({ abuse_flagged: true });
});
@@ -423,12 +430,8 @@ describe('CommentsView', () => {
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ voted: true });
});
- it.each([
- ['endorsing comments', 'Endorse', { endorsed: true }],
- ['reporting comments', 'Report', { abuse_flagged: true }],
- ])('handles %s', async (label, buttonLabel, patchData) => {
+ it('handles endorsing comments', async () => {
renderComponent(discussionPostId);
-
// Wait for the content to load
await screen.findByText('comment number 7', { exact: false });
@@ -438,11 +441,36 @@ describe('CommentsView', () => {
await act(async () => {
fireEvent.click(actionButtons[1]);
});
+
await act(async () => {
- fireEvent.click(screen.getByRole('button', { name: buttonLabel }));
+ fireEvent.click(screen.getByRole('button', { name: /Endorse/i }));
});
expect(axiosMock.history.patch).toHaveLength(2);
- expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject(patchData);
+ expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ endorsed: true });
+ });
+
+ it('handles reporting comments', async () => {
+ renderComponent(discussionPostId);
+ // Wait for the content to load
+ await screen.findByText('comment number 7', { exact: false });
+
+ // There should be three buttons, one for the post, the second for the
+ // comment and the third for a response to that comment
+ const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i });
+ await act(async () => {
+ fireEvent.click(actionButtons[1]);
+ });
+
+ await act(async () => {
+ fireEvent.click(screen.getByRole('button', { name: /Report/i }));
+ });
+ expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).toBeInTheDocument();
+ await act(async () => {
+ fireEvent.click(screen.queryByRole('button', { name: /Confirm/i }));
+ });
+ expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).not.toBeInTheDocument();
+ expect(axiosMock.history.patch).toHaveLength(2);
+ expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ abuse_flagged: true });
});
});
@@ -624,12 +652,8 @@ describe('CommentsView', () => {
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ voted: true });
});
- it.each([
- ['endorsing comments', 'Endorse', { endorsed: true }],
- ['reporting comments', 'Report', { abuse_flagged: true }],
- ])('handles %s', async (label, buttonLabel, patchData) => {
+ it('handles endorsing comments', async () => {
renderComponent(discussionPostId);
-
// Wait for the content to load
await screen.findByText('comment number 7', { exact: false });
@@ -639,11 +663,36 @@ describe('CommentsView', () => {
await act(async () => {
fireEvent.click(actionButtons[1]);
});
+
await act(async () => {
- fireEvent.click(screen.getByRole('button', { name: buttonLabel }));
+ fireEvent.click(screen.getByRole('button', { name: /Endorse/i }));
});
expect(axiosMock.history.patch).toHaveLength(2);
- expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject(patchData);
+ expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ endorsed: true });
+ });
+
+ it('handles reporting comments', async () => {
+ renderComponent(discussionPostId);
+ // Wait for the content to load
+ await screen.findByText('comment number 7', { exact: false });
+
+ // There should be three buttons, one for the post, the second for the
+ // comment and the third for a response to that comment
+ const actionButtons = screen.queryAllByRole('button', { name: /actions menu/i });
+ await act(async () => {
+ fireEvent.click(actionButtons[1]);
+ });
+
+ await act(async () => {
+ fireEvent.click(screen.getByRole('button', { name: /Report/i }));
+ });
+ expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).toBeInTheDocument();
+ await act(async () => {
+ fireEvent.click(screen.queryByRole('button', { name: /Confirm/i }));
+ });
+ expect(screen.queryByRole('dialog', { name: /Report \w+/i, exact: false })).not.toBeInTheDocument();
+ expect(axiosMock.history.patch).toHaveLength(2);
+ expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ abuse_flagged: true });
});
});
diff --git a/src/discussions/comments/comment/Comment.jsx b/src/discussions/comments/comment/Comment.jsx
index d6876339..8dba0d77 100644
--- a/src/discussions/comments/comment/Comment.jsx
+++ b/src/discussions/comments/comment/Comment.jsx
@@ -9,8 +9,7 @@ import { Button, useToggle } from '@edx/paragon';
import HTMLLoader from '../../../components/HTMLLoader';
import { ContentActions } from '../../../data/constants';
-import { AlertBanner, DeleteConfirmation, EndorsedAlertBanner } from '../../common';
-import { DiscussionContext } from '../../common/context';
+import { AlertBanner, Confirmation, EndorsedAlertBanner } from '../../common';
import { selectBlackoutDate } from '../../data/selectors';
import { fetchThread } from '../../posts/data/thunks';
import { inBlackoutDateRange } from '../../utils';
@@ -36,6 +35,7 @@ function Comment({
const inlineReplies = useSelector(selectCommentResponses(comment.id));
const [isEditing, setEditing] = useState(false);
const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false);
+ const [isReporting, showReportConfirmation, hideReportConfirmation] = useToggle(false);
const [isReplying, setReplying] = useState(false);
const hasMorePages = useSelector(selectCommentHasMorePages(comment.id));
const currentPage = useSelector(selectCommentCurrentPage(comment.id));
@@ -50,6 +50,24 @@ function Comment({
}
}, [comment.id]);
+ const handleAbusedFlag = () => {
+ if (comment.abuseFlagged) {
+ dispatch(editComment(comment.id, { flagged: !comment.abuseFlagged }));
+ } else {
+ showReportConfirmation();
+ }
+ };
+
+ const handleDeleteConfirmation = () => {
+ dispatch(removeComment(comment.id));
+ hideDeleteConfirmation();
+ };
+
+ const handleReportConfirmation = () => {
+ dispatch(editComment(comment.id, { flagged: !comment.abuseFlagged }));
+ hideReportConfirmation();
+ };
+
const actionHandlers = {
[ContentActions.EDIT_CONTENT]: () => setEditing(true),
[ContentActions.ENDORSE]: async () => {
@@ -57,7 +75,7 @@ function Comment({
await dispatch(fetchThread(comment.threadId, courseId));
},
[ContentActions.DELETE]: showDeleteConfirmation,
- [ContentActions.REPORT]: () => dispatch(editComment(comment.id, { flagged: !comment.abuseFlagged })),
+ [ContentActions.REPORT]: () => handleAbusedFlag(),
};
const handleLoadMoreComments = () => (
@@ -67,16 +85,25 @@ function Comment({
return (
-
{
- dispatch(removeComment(comment.id));
- hideDeleteConfirmation();
- }}
+ comfirmAction={handleDeleteConfirmation}
+ closeButtonVaraint="tertiary"
+ confirmButtonText={intl.formatMessage(messages.deleteConfirmationDelete)}
/>
+ {!comment.abuseFlagged && (
+
+ )}
diff --git a/src/discussions/comments/comment/Reply.jsx b/src/discussions/comments/comment/Reply.jsx
index f44e887d..bece7975 100644
--- a/src/discussions/comments/comment/Reply.jsx
+++ b/src/discussions/comments/comment/Reply.jsx
@@ -10,7 +10,7 @@ import { Avatar, useToggle } from '@edx/paragon';
import HTMLLoader from '../../../components/HTMLLoader';
import { AvatarOutlineAndLabelColors, ContentActions } from '../../../data/constants';
import {
- ActionsDropdown, AlertBanner, AuthorLabel, DeleteConfirmation,
+ ActionsDropdown, AlertBanner, AuthorLabel, Confirmation,
} from '../../common';
import timeLocale from '../../common/time-locale';
import { useAlertBannerVisible } from '../../data/hooks';
@@ -29,6 +29,26 @@ function Reply({
const dispatch = useDispatch();
const [isEditing, setEditing] = useState(false);
const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false);
+ const [isReporting, showReportConfirmation, hideReportConfirmation] = useToggle(false);
+
+ const handleAbusedFlag = () => {
+ if (reply.abuseFlagged) {
+ dispatch(editComment(reply.id, { flagged: !reply.abuseFlagged }));
+ } else {
+ showReportConfirmation();
+ }
+ };
+
+ const handleDeleteConfirmation = () => {
+ dispatch(removeComment(reply.id));
+ hideDeleteConfirmation();
+ };
+
+ const handleReportConfirmation = () => {
+ dispatch(editComment(reply.id, { flagged: !reply.abuseFlagged }));
+ hideReportConfirmation();
+ };
+
const actionHandlers = {
[ContentActions.EDIT_CONTENT]: () => setEditing(true),
[ContentActions.ENDORSE]: () => dispatch(editComment(
@@ -37,7 +57,7 @@ function Reply({
ContentActions.ENDORSE,
)),
[ContentActions.DELETE]: showDeleteConfirmation,
- [ContentActions.REPORT]: () => dispatch(editComment(reply.id, { flagged: !reply.abuseFlagged })),
+ [ContentActions.REPORT]: () => handleAbusedFlag(),
};
const authorAvatars = useSelector(selectAuthorAvatars(reply.author));
const colorClass = AvatarOutlineAndLabelColors[reply.authorLabel];
@@ -45,17 +65,25 @@ function Reply({
return (
-
{
- dispatch(removeComment(reply.id));
- hideDeleteConfirmation();
- }}
+ comfirmAction={handleDeleteConfirmation}
+ closeButtonVaraint="tertiary"
+ confirmButtonText={intl.formatMessage(messages.deleteConfirmationDelete)}
/>
-
+ {!reply.abuseFlagged && (
+
+ )}
{hasAnyAlert && (
diff --git a/src/discussions/comments/messages.js b/src/discussions/comments/messages.js
index 85838798..31f08651 100644
--- a/src/discussions/comments/messages.js
+++ b/src/discussions/comments/messages.js
@@ -148,6 +148,31 @@ const messages = defineMessages({
defaultMessage: 'Are you sure you want to permanently delete this comment?',
description: 'Text displayed in confirmation dialog when deleting a comment',
},
+ deleteConfirmationDelete: {
+ id: 'discussions.delete.confirmation.button.delete',
+ defaultMessage: 'Delete',
+ description: 'Delete button shown on delete confirmation dialog',
+ },
+ reportResponseTitle: {
+ id: 'discussions.editor.response.response.title',
+ defaultMessage: 'Report inappropriate content?',
+ description: 'Title of confirmation dialog shown when reporting a response',
+ },
+ reportResponseDescription: {
+ id: 'discussions.editor.response.description',
+ defaultMessage: 'The discussion moderation team will review this content and take appropriate action.',
+ description: 'Text displayed in confirmation dialog when deleting a response',
+ },
+ reportCommentTitle: {
+ id: 'discussions.editor.report.comment.title',
+ defaultMessage: 'Report inappropriate content?',
+ description: 'Title of confirmation dialog shown when reporting a comment',
+ },
+ reportCommentDescription: {
+ id: 'discussions.editor.report.comment.description',
+ defaultMessage: 'The discussion moderation team will review this content and take appropriate action.',
+ description: 'Text displayed in confirmation dialog when deleting a response',
+ },
editReasonCode: {
id: 'discussions.editor.comments.editReasonCode',
defaultMessage: 'Reason for editing',
diff --git a/src/discussions/common/ActionsDropdown.jsx b/src/discussions/common/ActionsDropdown.jsx
index 55faeeaf..09310b0a 100644
--- a/src/discussions/common/ActionsDropdown.jsx
+++ b/src/discussions/common/ActionsDropdown.jsx
@@ -64,7 +64,7 @@ function ActionsDropdown({
>
{actions.map(action => (
- {action.action === ContentActions.DELETE
+ {(action.action === ContentActions.DELETE)
&& }
{canSeeReportedBanner && (
-
+
{intl.formatMessage(messages.abuseFlaggedMessage)}
)}
diff --git a/src/discussions/common/DeleteConfirmation.jsx b/src/discussions/common/Confirmation.jsx
similarity index 56%
rename from src/discussions/common/DeleteConfirmation.jsx
rename to src/discussions/common/Confirmation.jsx
index 3bda8b72..06f84b31 100644
--- a/src/discussions/common/DeleteConfirmation.jsx
+++ b/src/discussions/common/Confirmation.jsx
@@ -6,13 +6,16 @@ import { ActionRow, Button, ModalDialog } from '@edx/paragon';
import messages from '../messages';
-function DeleteConfirmation({
+function Confirmation({
intl,
isOpen,
title,
description,
onClose,
- onDelete,
+ comfirmAction,
+ closeButtonVaraint,
+ confirmButtonVariant,
+ confirmButtonText,
}) {
return (
@@ -26,11 +29,11 @@ function DeleteConfirmation({
-
- {intl.formatMessage(messages.deleteConfirmationCancel)}
+
+ {intl.formatMessage(messages.confirmationCancel)}
-
@@ -38,13 +41,22 @@ function DeleteConfirmation({
);
}
-DeleteConfirmation.propTypes = {
+Confirmation.propTypes = {
intl: intlShape.isRequired,
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
- onDelete: PropTypes.func.isRequired,
+ comfirmAction: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
+ closeButtonVaraint: PropTypes.string,
+ confirmButtonVariant: PropTypes.string,
+ confirmButtonText: PropTypes.string,
};
-export default injectIntl(DeleteConfirmation);
+Confirmation.defaultProps = {
+ closeButtonVaraint: 'default',
+ confirmButtonVariant: 'primary',
+ confirmButtonText: '',
+};
+
+export default injectIntl(Confirmation);
diff --git a/src/discussions/common/index.js b/src/discussions/common/index.js
index 4bee420d..f4ca7213 100644
--- a/src/discussions/common/index.js
+++ b/src/discussions/common/index.js
@@ -1,5 +1,5 @@
export { default as ActionsDropdown } from './ActionsDropdown';
export { default as AlertBanner } from './AlertBanner';
export { default as AuthorLabel } from './AuthorLabel';
-export { default as DeleteConfirmation } from './DeleteConfirmation';
+export { default as Confirmation } from './Confirmation';
export { default as EndorsedAlertBanner } from './EndorsedAlertBanner';
diff --git a/src/discussions/messages.js b/src/discussions/messages.js
index 0d84cd9c..51e0b8c8 100644
--- a/src/discussions/messages.js
+++ b/src/discussions/messages.js
@@ -31,6 +31,11 @@ const messages = defineMessages({
defaultMessage: 'Delete',
description: 'Action to delete a post or comment',
},
+ confirmationConfirm: {
+ id: 'discussions.confirmation.button.confirm',
+ defaultMessage: 'Confirm',
+ description: 'Confirm button shown on confirmation dialog',
+ },
closeAction: {
id: 'discussions.actions.close',
defaultMessage: 'Close',
@@ -71,16 +76,11 @@ const messages = defineMessages({
defaultMessage: 'Unmark as answered',
description: 'Action to unmark a comment as answering a post',
},
- deleteConfirmationCancel: {
- id: 'discussions.delete.confirmation.button.cancel',
+ confirmationCancel: {
+ id: 'discussions.modal.confirmation.button.cancel',
defaultMessage: 'Cancel',
description: 'Cancel button shown on delete confirmation dialog',
},
- deleteConfirmationDelete: {
- id: 'discussions.delete.confirmation.button.delete',
- defaultMessage: 'Delete',
- description: 'Delete button shown on delete confirmation dialog',
- },
emptyAllTopics: {
id: 'discussions.empty.allTopics',
defaultMessage:
diff --git a/src/discussions/posts/post/Post.jsx b/src/discussions/posts/post/Post.jsx
index 870cfedf..95e0aa7f 100644
--- a/src/discussions/posts/post/Post.jsx
+++ b/src/discussions/posts/post/Post.jsx
@@ -10,7 +10,7 @@ import { Hyperlink, useToggle } from '@edx/paragon';
import HTMLLoader from '../../../components/HTMLLoader';
import { ContentActions } from '../../../data/constants';
import { selectorForUnitSubsection, selectTopicContext } from '../../../data/selectors';
-import { AlertBanner, DeleteConfirmation } from '../../common';
+import { AlertBanner, Confirmation } from '../../common';
import { selectModerationSettings } from '../../data/selectors';
import { selectTopic } from '../../topics/data/selectors';
import { removeThread, updateExistingThread } from '../data/thunks';
@@ -34,7 +34,28 @@ function Post({
const topicContext = useSelector(selectTopicContext(post.topicId));
const { reasonCodesEnabled } = useSelector(selectModerationSettings);
const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false);
+ const [isReporting, showReportConfirmation, hideReportConfirmation] = useToggle(false);
const [isClosing, showClosePostModal, hideClosePostModal] = useToggle(false);
+
+ const handleAbusedFlag = () => {
+ if (post.abuseFlagged) {
+ dispatch(updateExistingThread(post.id, { flagged: !post.abuseFlagged }));
+ } else {
+ showReportConfirmation();
+ }
+ };
+
+ const handleDeleteConfirmation = () => {
+ dispatch(removeThread(post.id));
+ history.push('.');
+ hideDeleteConfirmation();
+ };
+
+ const handleReportConfirmation = () => {
+ dispatch(updateExistingThread(post.id, { flagged: !post.abuseFlagged }));
+ hideReportConfirmation();
+ };
+
const actionHandlers = {
[ContentActions.EDIT_CONTENT]: () => history.push({
...location,
@@ -52,7 +73,7 @@ function Post({
},
[ContentActions.COPY_LINK]: () => { navigator.clipboard.writeText(`${window.location.origin}/${courseId}/posts/${post.id}`); },
[ContentActions.PIN]: () => dispatch(updateExistingThread(post.id, { pinned: !post.pinned })),
- [ContentActions.REPORT]: () => dispatch(updateExistingThread(post.id, { flagged: !post.abuseFlagged })),
+ [ContentActions.REPORT]: () => handleAbusedFlag(),
};
const getTopicCategoryName = topicData => (
@@ -61,17 +82,25 @@ function Post({
return (
-
{
- dispatch(removeThread(post.id));
- history.push('.');
- hideDeleteConfirmation();
- }}
+ comfirmAction={handleDeleteConfirmation}
+ closeButtonVaraint="tertiary"
+ confirmButtonText={intl.formatMessage(messages.deleteConfirmationDelete)}
/>
+ {!post.abuseFlagged && (
+
+ )}
diff --git a/src/discussions/posts/post/messages.js b/src/discussions/posts/post/messages.js
index e74fda5c..f9c76ba4 100644
--- a/src/discussions/posts/post/messages.js
+++ b/src/discussions/posts/post/messages.js
@@ -71,6 +71,21 @@ const messages = defineMessages({
id: 'discussions.editor.delete.post.description',
defaultMessage: 'Are you sure you want to permanently delete this post?',
},
+ deleteConfirmationDelete: {
+ id: 'discussions.post.delete.confirmation.button.delete',
+ defaultMessage: 'Delete',
+ description: 'Delete button shown on delete confirmation dialog',
+ },
+ reportPostTitle: {
+ id: 'discussions.editor.report.post.title',
+ defaultMessage: 'Report inappropriate content?',
+ description: 'Title of confirmation dialog shown when reporting a post',
+ },
+ reportPostDescription: {
+ id: 'discussions.editor.report.post.description',
+ defaultMessage: 'The discussion moderation team will review this content and take appropriate action.',
+ description: 'Text displayed in confirmation dialog when deleting a post',
+ },
closePostModalTitle: {
id: 'discussions.post.closePostModal.title',
defaultMessage: 'Close post',
diff --git a/src/index.scss b/src/index.scss
index 738c50d4..b35a84d3 100755
--- a/src/index.scss
+++ b/src/index.scss
@@ -242,11 +242,11 @@ header {
}
.zindex-5000 {
- z-index: 5000;
+ z-index: 5000;
}
#paragon-portal-root .pgn__modal-layer {
- z-index: 1500 !important;
+ z-index: 5000 !important;
}
@media only screen and (max-width: 767px) {