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) {