From c7b7f8e490d0596868d7b013fda350269b8f34f0 Mon Sep 17 00:00:00 2001 From: sundasnoreen12 Date: Thu, 4 Jul 2024 16:08:12 +0500 Subject: [PATCH 01/13] feat: added draft functionality for comment and responses --- src/discussions/data/selectors.js | 2 + .../comments/comment/CommentEditor.jsx | 49 +++++++++++++++++-- src/discussions/post-comments/data/hooks.js | 47 ++++++++++++++++++ src/discussions/post-comments/data/slices.js | 11 +++++ 4 files changed, 105 insertions(+), 4 deletions(-) diff --git a/src/discussions/data/selectors.js b/src/discussions/data/selectors.js index 3fab39e9e..b0ef1e840 100644 --- a/src/discussions/data/selectors.js +++ b/src/discussions/data/selectors.js @@ -90,3 +90,5 @@ export const selectIsUserLearner = createSelector( ) || false ), ); + +export const selectDraft = state => state.comments.draft || null; diff --git a/src/discussions/post-comments/comments/comment/CommentEditor.jsx b/src/discussions/post-comments/comments/comment/CommentEditor.jsx index 59327c71f..175a1d555 100644 --- a/src/discussions/post-comments/comments/comment/CommentEditor.jsx +++ b/src/discussions/post-comments/comments/comment/CommentEditor.jsx @@ -1,5 +1,5 @@ import React, { - useCallback, useContext, useEffect, useRef, + useCallback, useContext, useEffect, useRef, useState, } from 'react'; import PropTypes from 'prop-types'; @@ -17,12 +17,15 @@ import PostPreviewPanel from '../../../../components/PostPreviewPanel'; import useDispatchWithState from '../../../../data/hooks'; import DiscussionContext from '../../../common/context'; import { + selectDraft, selectModerationSettings, selectUserHasModerationPrivileges, selectUserIsGroupTa, selectUserIsStaff, } from '../../../data/selectors'; import { formikCompatibleHandler, isFormikFieldInvalid } from '../../../utils'; +import { useAddDraftContent, useRemoveDraftContent } from '../../data/hooks'; +import { setDraftContent } from '../../data/slices'; import { addComment, editComment } from '../../data/thunks'; import messages from '../../messages'; @@ -44,7 +47,9 @@ const CommentEditor = ({ const userIsGroupTa = useSelector(selectUserIsGroupTa); const userIsStaff = useSelector(selectUserIsStaff); const { editReasons } = useSelector(selectModerationSettings); + const { responses, comments } = useSelector(selectDraft); const [submitting, dispatch] = useDispatchWithState(); + const [editorContent, setEditorContent] = useState(); const canDisplayEditReason = (edit && (userHasModerationPrivileges || userIsGroupTa || userIsStaff) @@ -71,6 +76,11 @@ const CommentEditor = ({ onCloseEditor(); }, [onCloseEditor, initialValues]); + const DeleteEditorContent = async () => { + const { updatedResponses, updatedComments } = useRemoveDraftContent(responses, comments, parentId, id, threadId); + await dispatch(setDraftContent({ responses: updatedResponses, comments: updatedComments })); + }; + const saveUpdatedComment = useCallback(async (values, { resetForm }) => { if (id) { const payload = { @@ -86,6 +96,7 @@ const CommentEditor = ({ editorRef.current.plugins.autosave.removeDraft(); } handleCloseEditor(resetForm); + DeleteEditorContent(); }, [id, threadId, parentId, enableInContextSidebar, handleCloseEditor]); // The editorId is used to autosave contents to localstorage. This format means that the autosave is scoped to // the current comment id, or the current comment parent or the curren thread. @@ -97,6 +108,32 @@ const CommentEditor = ({ } }, [formRef]); + useEffect(() => { + const draftObject = parentId + ? comments.find(x => x.parentId === parentId && x.id === id) + : responses.find(x => x.threadId === threadId && x.id === id); + + setEditorContent(draftObject ? draftObject.content : ''); + }, [responses, comments, parentId, threadId, id]); + + const SaveDraftComment = async () => { + const content = editorRef.current.getContent(); + const { updatedResponses, updatedComments } = useAddDraftContent( + content, + responses, + comments, + parentId, + id, + threadId, + ); + await dispatch(setDraftContent({ responses: updatedResponses, comments: updatedComments })); + }; + + const handleEditorChange = (content, setFieldValue) => { + setEditorContent(content); + setFieldValue('comment', content); + }; + return ( (
{canDisplayEditReason && ( @@ -149,9 +187,12 @@ const CommentEditor = ({ } } id={editorId} - value={values.comment} - onEditorChange={formikCompatibleHandler(handleChange, 'comment')} - onBlur={formikCompatibleHandler(handleBlur, 'comment')} + value={editorContent || values.comment} + onEditorChange={(content) => handleEditorChange(content, setFieldValue)} + onBlur={() => { + formikCompatibleHandler(handleChange, 'comment'); + SaveDraftComment(); + }} /> {isFormikFieldInvalid('comment', { errors, diff --git a/src/discussions/post-comments/data/hooks.js b/src/discussions/post-comments/data/hooks.js index d53c38846..d332eaaa7 100644 --- a/src/discussions/post-comments/data/hooks.js +++ b/src/discussions/post-comments/data/hooks.js @@ -102,3 +102,50 @@ export function useCommentsCount(postId) { return commentsLength; } + +const removeItem = (list, condition) => { + const index = list.findIndex(condition); + if (index > -1) { + list.splice(index, 1); + } +}; + +export const useRemoveDraftContent = (responses, comments, parentId, id, threadId) => { + const updatedResponses = [...responses]; + const updatedComments = [...comments]; + + if (!parentId) { + removeItem(updatedResponses, x => x.threadId === threadId && x.id === id); + } else { + removeItem(updatedComments, x => x.parentId === parentId && x.id === id); + } + + return { updatedResponses, updatedComments }; +}; + +const updateList = (list, condition, newItem) => { + const index = list.findIndex(condition); + if (index === -1) { + return [...list, newItem]; + } + return list.map((item, i) => (i === index ? { + ...item, content: newItem.content, id: newItem.id, parentId: newItem.parentId, + } : item)); +}; + +export const useAddDraftContent = (content, responses, comments, parentId, id, threadId) => { + let updatedResponses = [...responses]; + let updatedComments = [...comments]; + + if (!parentId) { + updatedResponses = updateList(updatedResponses, (x) => x.threadId === threadId && x.id === id, { + threadId, content, parentId, id, + }); + } else { + updatedComments = updateList(updatedComments, (x) => x.parentId === parentId && x.id === id, { + threadId, content, parentId, id, + }); + } + + return { updatedResponses, updatedComments }; +}; diff --git a/src/discussions/post-comments/data/slices.js b/src/discussions/post-comments/data/slices.js index 321662e1b..c2c7d6019 100644 --- a/src/discussions/post-comments/data/slices.js +++ b/src/discussions/post-comments/data/slices.js @@ -22,6 +22,10 @@ const commentsSlice = createSlice({ pagination: {}, responsesPagination: {}, sortOrder: true, + draft: { + responses: [], + comments: [], + }, }, reducers: { fetchCommentsRequest: (state) => ( @@ -257,6 +261,12 @@ const commentsSlice = createSlice({ sortOrder: payload, } ), + setDraftContent: (state, { payload }) => ( + { + ...state, + draft: payload, + } + ), }, }); @@ -282,6 +292,7 @@ export const { deleteCommentRequest, deleteCommentSuccess, setCommentSortOrder, + setDraftContent, } = commentsSlice.actions; export const commentsReducer = commentsSlice.reducer; From bc30a628315bd48e0065cff2a3a1f90b7ce9a783 Mon Sep 17 00:00:00 2001 From: sundasnoreen12 Date: Thu, 4 Jul 2024 22:48:29 +0500 Subject: [PATCH 02/13] fix: fixed comment update issue: --- src/discussions/data/selectors.js | 2 +- .../post-comments/comments/comment/CommentEditor.jsx | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/discussions/data/selectors.js b/src/discussions/data/selectors.js index b0ef1e840..bc5952fdb 100644 --- a/src/discussions/data/selectors.js +++ b/src/discussions/data/selectors.js @@ -91,4 +91,4 @@ export const selectIsUserLearner = createSelector( ), ); -export const selectDraft = state => state.comments.draft || null; +export const selectDraft = state => state.comments.draft || null; diff --git a/src/discussions/post-comments/comments/comment/CommentEditor.jsx b/src/discussions/post-comments/comments/comment/CommentEditor.jsx index 175a1d555..b7467dadd 100644 --- a/src/discussions/post-comments/comments/comment/CommentEditor.jsx +++ b/src/discussions/post-comments/comments/comment/CommentEditor.jsx @@ -82,14 +82,16 @@ const CommentEditor = ({ }; const saveUpdatedComment = useCallback(async (values, { resetForm }) => { + const clonedValues = { ...values }; + clonedValues.comment = editorContent; if (id) { const payload = { - ...values, - editReasonCode: values.editReasonCode || undefined, + ...clonedValues, + editReasonCode: clonedValues.editReasonCode || undefined, }; await dispatch(editComment(id, payload)); } else { - await dispatch(addComment(values.comment, threadId, parentId, enableInContextSidebar)); + await dispatch(addComment(clonedValues.comment, threadId, parentId, enableInContextSidebar)); } /* istanbul ignore if: TinyMCE is mocked so this cannot be easily tested */ if (editorRef.current) { From fe11e53e50cbaa348553f9bf3d5b957644a4ff83 Mon Sep 17 00:00:00 2001 From: sundasnoreen12 Date: Mon, 8 Jul 2024 00:44:22 +0500 Subject: [PATCH 03/13] test: added draft test case --- .../post-comments/PostCommentsView.test.jsx | 24 +++++++++++++ src/setupTest.jsx | 34 +++++++++++++------ 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/discussions/post-comments/PostCommentsView.test.jsx b/src/discussions/post-comments/PostCommentsView.test.jsx index 4f15c596c..28a89d145 100644 --- a/src/discussions/post-comments/PostCommentsView.test.jsx +++ b/src/discussions/post-comments/PostCommentsView.test.jsx @@ -671,6 +671,30 @@ describe('ThreadView', () => { expect(screen.queryByTestId('reply-comment-3')).not.toBeInTheDocument(); }); + it('successfully added response in the draft.', async () => { + await waitFor(() => renderComponent(discussionPostId)); + + let addResponseBtn = screen.queryByText('Add response'); + await act(async () => { + fireEvent.click(addResponseBtn); + }); + + await waitFor(() => { + const editor = screen.queryByTestId('tinymce-editor'); + fireEvent.change(editor, { target: { value: 'Hello, world!' } }); + }); + const cancelBtn = screen.queryByText('Cancel'); + await act(async () => { + fireEvent.click(cancelBtn); + }); + addResponseBtn = screen.queryByText('Add response'); + await act(async () => { + fireEvent.click(addResponseBtn); + }); + + expect(screen.queryByText('Hello, world!')).toBeInTheDocument(); + }); + it('pressing load more button will load next page of replies', async () => { await waitFor(() => renderComponent(discussionPostId)); diff --git a/src/setupTest.jsx b/src/setupTest.jsx index 261398e07..421cff4fa 100755 --- a/src/setupTest.jsx +++ b/src/setupTest.jsx @@ -1,5 +1,9 @@ import PropTypes from 'prop-types'; +import { useDispatch } from 'react-redux'; + +import { setDraftContent } from './discussions/post-comments/data/slices'; + import '@testing-library/jest-dom/extend-expect'; import 'babel-polyfill'; @@ -23,21 +27,29 @@ Object.defineProperty(window, 'matchMedia', { const MockEditor = ({ onBlur, onEditorChange, -}) => ( -