diff --git a/src/discussions/comments/CommentsView.jsx b/src/discussions/comments/CommentsView.jsx
index 77565562..9555a42b 100644
--- a/src/discussions/comments/CommentsView.jsx
+++ b/src/discussions/comments/CommentsView.jsx
@@ -18,6 +18,7 @@ import {
import { useDispatchWithState } from '../../data/hooks';
import { DiscussionContext } from '../common/context';
import { useIsOnDesktop } from '../data/hooks';
+import { EmptyPage } from '../empty-posts';
import { Post } from '../posts';
import { selectThread } from '../posts/data/selectors';
import { fetchThread, markThreadAsRead } from '../posts/data/thunks';
@@ -133,9 +134,9 @@ DiscussionCommentsView.propTypes = {
};
function CommentsView({ intl }) {
+ const [isLoading, submitDispatch] = useDispatchWithState();
const { postId } = useParams();
const thread = usePost(postId);
- const dispatch = useDispatch();
const history = useHistory();
const location = useLocation();
const isOnDesktop = useIsOnDesktop();
@@ -143,8 +144,16 @@ function CommentsView({ intl }) {
courseId, learnerUsername, category, topicId, page, inContext,
} = useContext(DiscussionContext);
+ useEffect(() => {
+ if (!thread) { submitDispatch(fetchThread(postId, courseId, true)); }
+ }, [postId]);
+
if (!thread) {
- dispatch(fetchThread(postId, true));
+ if (!isLoading) {
+ return (
+
+ );
+ }
return (
);
diff --git a/src/discussions/comments/CommentsView.test.jsx b/src/discussions/comments/CommentsView.test.jsx
index 52c01a81..2c921245 100644
--- a/src/discussions/comments/CommentsView.test.jsx
+++ b/src/discussions/comments/CommentsView.test.jsx
@@ -102,7 +102,7 @@ function renderComponent(postId) {
}
describe('CommentsView', () => {
- beforeEach(async () => {
+ beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
@@ -147,7 +147,7 @@ describe('CommentsView', () => {
)];
});
- await executeThunk(fetchThreads(courseId), store.dispatch, store.getState);
+ executeThunk(fetchThreads(courseId), store.dispatch, store.getState);
mockAxiosReturnPagedComments();
mockAxiosReturnPagedCommentsResponses();
});
@@ -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,20 +441,45 @@ 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 });
});
});
describe('for discussion thread', () => {
const findLoadMoreCommentsButton = () => screen.findByTestId('load-more-comments');
- it('shown spinner when post isn\'t loaded', async () => {
+ it('shown post not found when post id does not belong to course', async () => {
renderComponent('unloaded-id');
- expect(await screen.findByTestId('loading-indicator'))
+ expect(await screen.findByText('Thread not found', { exact: true }))
.toBeInTheDocument();
});
@@ -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 eea30cc5..733dc9b8 100644
--- a/src/discussions/comments/comment/Comment.jsx
+++ b/src/discussions/comments/comment/Comment.jsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
@@ -9,7 +9,8 @@ import { Button, useToggle } from '@edx/paragon';
import HTMLLoader from '../../../components/HTMLLoader';
import { ContentActions } from '../../../data/constants';
-import { AlertBanner, DeleteConfirmation, EndorsedAlertBanner } from '../../common';
+import { AlertBanner, Confirmation, EndorsedAlertBanner } from '../../common';
+import { DiscussionContext } from '../../common/context';
import { useUserCanAddThreadInBlackoutDate } from '../../data/hooks';
import { fetchThread } from '../../posts/data/thunks';
import CommentIcons from '../comment-icons/CommentIcons';
@@ -34,11 +35,14 @@ 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));
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
-
+ const {
+ courseId,
+ } = useContext(DiscussionContext);
useEffect(() => {
// If the comment has a parent comment, it won't have any children, so don't fetch them.
if (hasChildren && !currentPage && showFullThread) {
@@ -46,14 +50,32 @@ 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 () => {
await dispatch(editComment(comment.id, { endorsed: !comment.endorsed }, ContentActions.ENDORSE));
- await dispatch(fetchThread(comment.threadId));
+ await dispatch(fetchThread(comment.threadId, courseId));
},
[ContentActions.DELETE]: showDeleteConfirmation,
- [ContentActions.REPORT]: () => dispatch(editComment(comment.id, { flagged: !comment.abuseFlagged })),
+ [ContentActions.REPORT]: () => handleAbusedFlag(),
};
const handleLoadMoreComments = () => (
@@ -63,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 3fe4a355..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',
@@ -182,6 +207,11 @@ const messages = defineMessages({
defaultMessage: '{time} ago',
description: 'Time text for endorse banner',
},
+ noThreadFound: {
+ id: 'discussion.thread.notFound',
+ defaultMessage: 'Thread not found',
+ description: 'message to show on screen if the request thread is not found in course',
+ },
});
export default messages;
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/data/api.js b/src/discussions/posts/data/api.js
index ed8c6328..04f2b326 100644
--- a/src/discussions/posts/data/api.js
+++ b/src/discussions/posts/data/api.js
@@ -71,8 +71,8 @@ export async function getThreads(
* @param {string} threadId
* @returns {Promise<{}>}
*/
-export async function getThread(threadId) {
- const params = { requested_fields: 'profile_image' };
+export async function getThread(threadId, courseId) {
+ const params = { requested_fields: 'profile_image', course_id: courseId };
const url = `${threadsApiUrl}${threadId}/`;
const { data } = await getAuthenticatedHttpClient().get(url, { params });
return data;
diff --git a/src/discussions/posts/data/thunks.js b/src/discussions/posts/data/thunks.js
index 04269def..7b3f2665 100644
--- a/src/discussions/posts/data/thunks.js
+++ b/src/discussions/posts/data/thunks.js
@@ -154,18 +154,18 @@ export function fetchThreads(courseId, {
};
}
-export function fetchThread(threadId, isDirectLinkPost = false) {
+export function fetchThread(threadId, courseId, isDirectLinkPost = false) {
return async (dispatch) => {
try {
dispatch(fetchThreadRequest({ threadId }));
- const data = await getThread(threadId);
+ const data = await getThread(threadId, courseId);
if (isDirectLinkPost) {
dispatch(fetchThreadByDirectLinkSuccess({ ...normaliseThreads(camelCaseObject(data)), page: 1 }));
} else {
dispatch(fetchThreadSuccess(normaliseThreads(camelCaseObject(data))));
}
} catch (error) {
- if (getHttpErrorStatus(error) === 403) {
+ if (getHttpErrorStatus(error) === 403 || getHttpErrorStatus(error) === 404) {
dispatch(fetchThreadDenied());
} else {
dispatch(fetchThreadFailed());
diff --git a/src/discussions/posts/post-editor/PostEditor.jsx b/src/discussions/posts/post-editor/PostEditor.jsx
index 74c5f918..0ab48773 100644
--- a/src/discussions/posts/post-editor/PostEditor.jsx
+++ b/src/discussions/posts/post-editor/PostEditor.jsx
@@ -33,6 +33,7 @@ import {
selectUserIsGroupTa,
selectUserIsStaff,
} from '../../data/selectors';
+import { EmptyPage } from '../../empty-posts';
import { selectCoursewareTopics, selectNonCoursewareIds, selectNonCoursewareTopics } from '../../topics/data/selectors';
import {
discussionsPath, formikCompatibleHandler, isFormikFieldInvalid, useCommentsPagePath,
@@ -193,16 +194,25 @@ function PostEditor({
dispatch(fetchCourseCohorts(courseId));
}
if (editExisting) {
- dispatch(fetchThread(postId));
+ dispatchSubmit(fetchThread(postId, courseId));
}
}, [courseId, editExisting]);
if (editExisting && !post) {
- return (
-
-
-
- );
+ if (submitting) {
+ return (
+
+
+
+ );
+ }
+ if (!submitting) {
+ return (
+
+ );
+ }
}
const validationSchema = Yup.object().shape({
diff --git a/src/discussions/posts/post-editor/messages.js b/src/discussions/posts/post-editor/messages.js
index c5b56362..240228ac 100644
--- a/src/discussions/posts/post-editor/messages.js
+++ b/src/discussions/posts/post-editor/messages.js
@@ -131,6 +131,11 @@ const messages = defineMessages({
defaultMessage: 'Unnamed subcategory',
description: 'display string for topics with missing names',
},
+ noThreadFound: {
+ id: 'discussion.thread.notFound',
+ defaultMessage: 'Thread not found',
+ description: 'message to show on screen if the request thread is not found in course',
+ },
});
export default messages;
diff --git a/src/discussions/posts/post/Post.jsx b/src/discussions/posts/post/Post.jsx
index 870cfedf..3a5a43fa 100644
--- a/src/discussions/posts/post/Post.jsx
+++ b/src/discussions/posts/post/Post.jsx
@@ -1,6 +1,7 @@
-import React from 'react';
+import React, { useContext } from 'react';
import PropTypes from 'prop-types';
+import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
@@ -10,7 +11,8 @@ 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 { DiscussionContext } from '../../common/context';
import { selectModerationSettings } from '../../data/selectors';
import { selectTopic } from '../../topics/data/selectors';
import { removeThread, updateExistingThread } from '../data/thunks';
@@ -34,7 +36,29 @@ 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 { inContext } = useContext(DiscussionContext);
const actionHandlers = {
[ContentActions.EDIT_CONTENT]: () => history.push({
...location,
@@ -52,7 +76,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,30 +85,50 @@ function Post({
return (
-
{
- dispatch(removeThread(post.id));
- history.push('.');
- hideDeleteConfirmation();
- }}
+ comfirmAction={handleDeleteConfirmation}
+ closeButtonVaraint="tertiary"
+ confirmButtonText={intl.formatMessage(messages.deleteConfirmationDelete)}
/>
+ {!post.abuseFlagged && (
+
+ )}
{topicContext && topic && (
-
+
{intl.formatMessage(messages.relatedTo)}{' '}
- {`${getTopicCategoryName(topic)} / ${topic.name}`}
+ {inContext
+ ? (
+ <>
+ {topicContext.chapterName}
+ /
+ {topicContext.verticalName}
+ /
+ {topicContext.unitName}
+ >
+ )
+ : `${getTopicCategoryName(topic)} / ${topic.name}`}
)}
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) {