Skip to content

Commit

Permalink
feat: added draft functionality for comment and responses (#727)
Browse files Browse the repository at this point in the history
* feat: added draft functionality for comment and responses

* fix: fixed comment update issue:

* test: added draft test case

* test: added mock conditions for tinymce

* refactor: refactor code

* test: added test cases

* refactor: refactor hook file

* refactor: fixed review issues

* refactor: memoize function

* refactor: refactor code

* test: added update comment test case

* refactor: refactor remove hook method

* test: fixed test cases issue
  • Loading branch information
sundasnoreen12 authored Jul 24, 2024
1 parent 422fbf6 commit db883ca
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 8 deletions.
1 change: 1 addition & 0 deletions src/discussions/common/ActionsDropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ const ActionsDropdown = ({
handleActions(action.action);
}}
className="d-flex justify-content-start actions-dropdown-item"
data-testId={action.id}
>
<Icon
src={action.icon}
Expand Down
145 changes: 145 additions & 0 deletions src/discussions/post-comments/PostCommentsView.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,151 @@ describe('ThreadView', () => {
expect(screen.queryByTestId('reply-comment-3')).not.toBeInTheDocument();
});

it('successfully added comment in the draft.', async () => {
await waitFor(() => renderComponent(discussionPostId));

await act(async () => {
fireEvent.click(screen.queryByText('Add comment'));
});

await waitFor(() => {
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Draft comment!' } });
});

await act(async () => {
fireEvent.click(screen.queryByText('Cancel'));
});

await act(async () => {
fireEvent.click(screen.queryByText('Add comment'));
});

expect(screen.queryByText('Draft comment!')).toBeInTheDocument();
});

it('successfully updated comment in the draft.', async () => {
await waitFor(() => renderComponent(discussionPostId));

const comment = screen.queryByTestId('reply-comment-2');
const actionBtn = comment.querySelector('button[aria-label="Actions menu"]');

await act(async () => {
fireEvent.click(actionBtn);
});

await act(async () => {
fireEvent.click(screen.queryByTestId('edit'));
});

await waitFor(() => {
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Draft comment!' } });
});

await act(async () => {
fireEvent.click(screen.queryByText('Cancel'));
});

await act(async () => {
fireEvent.click(actionBtn);
});

await act(async () => {
fireEvent.click(screen.queryByTestId('edit'));
});

await act(async () => {
fireEvent.click(screen.queryByText('Submit'));
});

await waitFor(() => expect(screen.queryByText('Draft comment!')).toBeInTheDocument());
});

it('successfully removed comment from the draft.', async () => {
await waitFor(() => renderComponent(discussionPostId));

await act(async () => {
fireEvent.click(screen.queryByText('Add comment'));
});

await waitFor(() => {
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Draft comment 123!' } });
});

await act(async () => {
fireEvent.click(screen.queryByText('Submit'));
});

await act(async () => {
fireEvent.click(screen.queryAllByText('Add comment')[0]);
});

expect(screen.queryByTestId('tinymce-editor').value).toBe('');
});

it('successfully added response in the draft.', async () => {
await waitFor(() => renderComponent(discussionPostId));

await act(async () => {
fireEvent.click(screen.queryByText('Add response'));
});

await waitFor(() => {
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Draft Response!' } });
});

await act(async () => {
fireEvent.click(screen.queryByText('Cancel'));
});

await act(async () => {
fireEvent.click(screen.queryByText('Add response'));
});

expect(screen.queryByText('Draft Response!')).toBeInTheDocument();
});

it('successfully removed response from the draft.', async () => {
await waitFor(() => renderComponent(discussionPostId));

await act(async () => {
fireEvent.click(screen.queryByText('Add response'));
});

await waitFor(() => {
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Draft Response!' } });
});

await act(async () => {
fireEvent.click(screen.queryByText('Submit'));
});

await act(async () => {
fireEvent.click(screen.queryByText('Add response'));
});

expect(screen.queryByTestId('tinymce-editor').value).toBe('');
});

it('successfully maintain response for the specific post in the draft.', async () => {
await waitFor(() => renderComponent(discussionPostId));

await act(async () => {
fireEvent.click(screen.queryByText('Add response'));
});

await waitFor(() => {
fireEvent.change(screen.queryByTestId('tinymce-editor'), { target: { value: 'Hello, world!' } });
});

await waitFor(() => renderComponent('thread-2'));

await act(async () => {
fireEvent.click(screen.queryAllByText('Add response')[0]);
});

expect(screen.queryByText('Hello, world!')).toBeInTheDocument();
});

it('pressing load more button will load next page of replies', async () => {
await waitFor(() => renderComponent(discussionPostId));

Expand Down
47 changes: 43 additions & 4 deletions src/discussions/post-comments/comments/comment/CommentEditor.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {
useCallback, useContext, useEffect, useRef,
useCallback, useContext, useEffect, useRef, useState,
} from 'react';
import PropTypes from 'prop-types';

Expand All @@ -22,7 +22,9 @@ import {
selectUserIsGroupTa,
selectUserIsStaff,
} from '../../../data/selectors';
import { formikCompatibleHandler, isFormikFieldInvalid } from '../../../utils';
import { extractContent, formikCompatibleHandler, isFormikFieldInvalid } from '../../../utils';
import { useDraftContent } from '../../data/hooks';
import { setDraftComments, setDraftResponses } from '../../data/slices';
import { addComment, editComment } from '../../data/thunks';
import messages from '../../messages';

Expand All @@ -45,6 +47,8 @@ const CommentEditor = ({
const userIsStaff = useSelector(selectUserIsStaff);
const { editReasons } = useSelector(selectModerationSettings);
const [submitting, dispatch] = useDispatchWithState();
const [editorContent, setEditorContent] = useState();
const { addDraftContent, getDraftContent, removeDraftContent } = useDraftContent();

const canDisplayEditReason = (edit
&& (userHasModerationPrivileges || userIsGroupTa || userIsStaff)
Expand All @@ -62,7 +66,7 @@ const CommentEditor = ({
});

const initialValues = {
comment: rawBody,
comment: editorContent,
editReasonCode: lastEdit?.reasonCode || (userIsStaff && canDisplayEditReason ? 'violates-guidelines' : undefined),
};

Expand All @@ -71,6 +75,15 @@ const CommentEditor = ({
onCloseEditor();
}, [onCloseEditor, initialValues]);

const deleteEditorContent = useCallback(async () => {
const { updatedResponses, updatedComments } = removeDraftContent(parentId, id, threadId);
if (parentId) {
await dispatch(setDraftComments(updatedComments));
} else {
await dispatch(setDraftResponses(updatedResponses));
}
}, [parentId, id, threadId, setDraftComments, setDraftResponses]);

const saveUpdatedComment = useCallback(async (values, { resetForm }) => {
if (id) {
const payload = {
Expand All @@ -86,6 +99,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.
Expand All @@ -97,11 +111,33 @@ const CommentEditor = ({
}
}, [formRef]);

useEffect(() => {
const draftHtml = getDraftContent(parentId, threadId, id) || rawBody;
setEditorContent(draftHtml);
}, [parentId, threadId, id]);

const saveDraftContent = async (content) => {
const draftDataContent = extractContent(content);

const { updatedResponses, updatedComments } = addDraftContent(
draftDataContent,
parentId,
id,
threadId,
);
if (parentId) {
await dispatch(setDraftComments(updatedComments));
} else {
await dispatch(setDraftResponses(updatedResponses));
}
};

return (
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={saveUpdatedComment}
enableReinitialize
>
{({
values,
Expand Down Expand Up @@ -151,7 +187,10 @@ const CommentEditor = ({
id={editorId}
value={values.comment}
onEditorChange={formikCompatibleHandler(handleChange, 'comment')}
onBlur={formikCompatibleHandler(handleBlur, 'comment')}
onBlur={(content) => {
formikCompatibleHandler(handleChange, 'comment');
saveDraftContent(content);
}}
/>
{isFormikFieldInvalid('comment', {
errors,
Expand Down
74 changes: 73 additions & 1 deletion src/discussions/post-comments/data/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
} from 'react';

import { useDispatch, useSelector } from 'react-redux';
import { v4 as uuidv4 } from 'uuid';

import { sendTrackEvent } from '@edx/frontend-platform/analytics';

Expand All @@ -13,7 +14,8 @@ import { selectThread } from '../../posts/data/selectors';
import { markThreadAsRead } from '../../posts/data/thunks';
import { filterPosts } from '../../utils';
import {
selectCommentSortOrder, selectThreadComments, selectThreadCurrentPage, selectThreadHasMorePages,
selectCommentSortOrder, selectDraftComments, selectDraftResponses,
selectThreadComments, selectThreadCurrentPage, selectThreadHasMorePages,
} from './selectors';
import { fetchThreadComments } from './thunks';

Expand Down Expand Up @@ -102,3 +104,73 @@ export function useCommentsCount(postId) {

return commentsLength;
}

export const useDraftContent = () => {
const comments = useSelector(selectDraftComments);
const responses = useSelector(selectDraftResponses);

const getObjectByParentId = (data, parentId, isComment, id) => Object.values(data)
.find(draft => (isComment ? draft.parentId === parentId && (id ? draft.id === id : draft.isNewContent === true)
: draft.threadId === parentId && (id ? draft.id === id : draft.isNewContent === true)));

const updateDraftData = (draftData, newDraftObject) => ({
...draftData,
[newDraftObject.id]: newDraftObject,
});

const addDraftContent = (content, parentId, id, threadId) => {
const data = parentId ? comments : responses;
const draftParentId = parentId || threadId;
const isComment = !!parentId;
const existingObj = getObjectByParentId(data, draftParentId, isComment, id);
const newObject = existingObj
? { ...existingObj, content }
: {
threadId,
content,
parentId,
id: id || uuidv4(),
isNewContent: !id,
};

const updatedComments = parentId ? updateDraftData(comments, newObject) : comments;
const updatedResponses = !parentId ? updateDraftData(responses, newObject) : responses;

return { updatedComments, updatedResponses };
};

const getDraftContent = (parentId, threadId, id) => {
if (id) {
return parentId ? comments?.[id]?.content : responses?.[id]?.content;
}

const data = parentId ? comments : responses;
const draftParentId = parentId || threadId;
const isComment = !!parentId;

return getObjectByParentId(data, draftParentId, isComment, id)?.content;
};

const removeItem = (draftData, objId) => {
const { [objId]: _, ...newDraftData } = draftData;
return newDraftData;
};

const updateContent = (items, itemId, parentId, isComment) => {
const itemObj = itemId ? items[itemId] : getObjectByParentId(items, parentId, isComment, itemId);
return itemObj ? removeItem(items, itemObj.id) : items;
};

const removeDraftContent = (parentId, id, threadId) => {
const updatedResponses = !parentId ? updateContent(responses, id, threadId, false) : responses;
const updatedComments = parentId ? updateContent(comments, id, parentId, true) : comments;

return { updatedResponses, updatedComments };
};

return {
addDraftContent,
getDraftContent,
removeDraftContent,
};
};
4 changes: 4 additions & 0 deletions src/discussions/post-comments/data/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ export const selectCommentCurrentPage = commentId => (
export const selectCommentsStatus = state => state.comments.status;

export const selectCommentSortOrder = state => state.comments.sortOrder;

export const selectDraftComments = state => state.comments.draftComments;

export const selectDraftResponses = state => state.comments.draftResponses;
Loading

0 comments on commit db883ca

Please sign in to comment.