feat: updated redux structure using updated comments api (#670)
* feat: updated redux structure and commentsview component * test: fixed test cases * fix: fixed lint error
This commit is contained in:
@@ -9,9 +9,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import Spinner from '../../components/Spinner';
|
||||
import {
|
||||
EndorsementStatus, PostsPages, ThreadType,
|
||||
} from '../../data/constants';
|
||||
import { PostsPages } from '../../data/constants';
|
||||
import useDispatchWithState from '../../data/hooks';
|
||||
import DiscussionContext from '../common/context';
|
||||
import { useIsOnDesktop } from '../data/hooks';
|
||||
@@ -127,15 +125,7 @@ const PostCommentsView = () => {
|
||||
</div>
|
||||
<Suspense fallback={(<Spinner />)}>
|
||||
{!!commentsCount && <CommentsSort />}
|
||||
{type === ThreadType.DISCUSSION && (
|
||||
<CommentsView endorsed={EndorsementStatus.DISCUSSION} />
|
||||
)}
|
||||
{type === ThreadType.QUESTION && (
|
||||
<>
|
||||
<CommentsView endorsed={EndorsementStatus.ENDORSED} />
|
||||
<CommentsView endorsed={EndorsementStatus.UNENDORSED} />
|
||||
</>
|
||||
)}
|
||||
<CommentsView threadType={type} />
|
||||
</Suspense>
|
||||
</PostCommentsContext.Provider>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { getApiBaseUrl } from '../../data/constants';
|
||||
import { getApiBaseUrl, ThreadType } from '../../data/constants';
|
||||
import { initializeStore } from '../../store';
|
||||
import executeThunk from '../../test-utils';
|
||||
import { getCohortsApiUrl } from '../cohorts/data/api';
|
||||
@@ -50,10 +50,10 @@ let testLocation;
|
||||
let container;
|
||||
let unmount;
|
||||
|
||||
async function mockAxiosReturnPagedComments(threadId, endorsed = false, page = 1, count = 2) {
|
||||
async function mockAxiosReturnPagedComments(threadId, threadType = ThreadType.DISCUSSION, page = 1, count = 2) {
|
||||
axiosMock.onGet(commentsApiUrl).reply(200, Factory.build('commentsResult', { can_delete: true }, {
|
||||
threadId,
|
||||
endorsed,
|
||||
threadType,
|
||||
pageSize: 1,
|
||||
count,
|
||||
childCount: page === 1 ? 2 : 0,
|
||||
@@ -76,6 +76,7 @@ async function mockAxiosReturnPagedCommentsResponses() {
|
||||
Factory.build('commentsResult', null, {
|
||||
threadId: discussionPostId,
|
||||
parentId,
|
||||
endorsed: false,
|
||||
page,
|
||||
pageSize: 1,
|
||||
count: 2,
|
||||
@@ -201,6 +202,7 @@ describe('ThreadView', () => {
|
||||
id: commentId,
|
||||
rendered_body: rawBody,
|
||||
raw_body: rawBody,
|
||||
endorsed: false,
|
||||
})];
|
||||
});
|
||||
axiosMock.onPost(commentsApiUrl).reply(({ data }) => {
|
||||
@@ -209,6 +211,7 @@ describe('ThreadView', () => {
|
||||
rendered_body: rawBody,
|
||||
raw_body: rawBody,
|
||||
thread_id: threadId,
|
||||
endorsed: false,
|
||||
})];
|
||||
});
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, { isPostingEnabled: true });
|
||||
@@ -230,9 +233,9 @@ describe('ThreadView', () => {
|
||||
expect(JSON.parse(axiosMock.history.patch[axiosMock.history.patch.length - 1].data)).toMatchObject(data);
|
||||
}
|
||||
|
||||
it('should not allow posting a comment on a closed post', async () => {
|
||||
it('should not allow posting a reply on a closed post', async () => {
|
||||
axiosMock.reset();
|
||||
await mockAxiosReturnPagedComments(closedPostId, true);
|
||||
await mockAxiosReturnPagedComments(closedPostId, ThreadType.QUESTION);
|
||||
await waitFor(() => renderComponent(closedPostId, true));
|
||||
const comments = await waitFor(() => screen.findAllByTestId('comment-comment-4'));
|
||||
const hoverCard = within(comments[0]).getByTestId('hover-card-comment-4');
|
||||
@@ -288,7 +291,7 @@ describe('ThreadView', () => {
|
||||
expect(screen.queryByTestId('tinymce-editor')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should allow posting a response', async () => {
|
||||
it('should allow posting a comment', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const post = await screen.findByTestId('post-thread-1');
|
||||
@@ -540,8 +543,11 @@ describe('ThreadView', () => {
|
||||
// Wait for the content to load
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
const hoverCard = within(comment).getByTestId('hover-card-comment-1');
|
||||
|
||||
const endorseButton = await waitFor(() => within(hoverCard).getByRole('button', { name: /Endorse/i }));
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(within(hoverCard).getByRole('button', { name: /Endorse/i }));
|
||||
fireEvent.click(endorseButton);
|
||||
});
|
||||
expect(axiosMock.history.patch).toHaveLength(2);
|
||||
expect(JSON.parse(axiosMock.history.patch[1].data)).toMatchObject({ endorsed: true });
|
||||
@@ -591,7 +597,7 @@ describe('ThreadView', () => {
|
||||
|
||||
it('pressing load more button will load next page of comments', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
await mockAxiosReturnPagedComments(discussionPostId, false, 2);
|
||||
await mockAxiosReturnPagedComments(discussionPostId, ThreadType.DISCUSSION, 2);
|
||||
|
||||
const loadMoreButton = await findLoadMoreCommentsButton();
|
||||
await act(async () => {
|
||||
@@ -604,7 +610,7 @@ describe('ThreadView', () => {
|
||||
|
||||
it('newly loaded comments are appended to the old ones', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
await mockAxiosReturnPagedComments(discussionPostId, false, 2);
|
||||
await mockAxiosReturnPagedComments(discussionPostId, ThreadType.DISCUSSION, 2);
|
||||
|
||||
const loadMoreButton = await findLoadMoreCommentsButton();
|
||||
await act(async () => {
|
||||
@@ -622,7 +628,7 @@ describe('ThreadView', () => {
|
||||
const findLoadMoreCommentsButtons = () => screen.findByTestId('load-more-comments');
|
||||
|
||||
it('initially loads only the first page', async () => {
|
||||
await mockAxiosReturnPagedComments(questionPostId);
|
||||
await mockAxiosReturnPagedComments(questionPostId, ThreadType.QUESTION);
|
||||
act(() => renderComponent(questionPostId));
|
||||
|
||||
expect(await screen.findByTestId('comment-comment-4'))
|
||||
@@ -633,7 +639,7 @@ describe('ThreadView', () => {
|
||||
});
|
||||
|
||||
it('pressing load more button will load next page of comments', async () => {
|
||||
await mockAxiosReturnPagedComments(questionPostId);
|
||||
await mockAxiosReturnPagedComments(questionPostId, ThreadType.QUESTION);
|
||||
await waitFor(() => renderComponent(questionPostId));
|
||||
|
||||
const loadMoreButton = await findLoadMoreCommentsButtons();
|
||||
@@ -644,7 +650,7 @@ describe('ThreadView', () => {
|
||||
expect(await screen.queryByTestId('comment-comment-5'))
|
||||
.not
|
||||
.toBeInTheDocument();
|
||||
await mockAxiosReturnPagedComments(questionPostId, false, 2, 1);
|
||||
await mockAxiosReturnPagedComments(questionPostId, ThreadType.QUESTION, 2, 1);
|
||||
await act(async () => {
|
||||
fireEvent.click(loadMoreButton);
|
||||
});
|
||||
@@ -664,7 +670,7 @@ describe('ThreadView', () => {
|
||||
expect(screen.queryByTestId('reply-comment-3')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pressing load more button will load next page of responses', async () => {
|
||||
it('pressing load more button will load next page of replies', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const loadMoreButton = await findLoadMoreCommentsResponsesButton();
|
||||
@@ -674,7 +680,7 @@ describe('ThreadView', () => {
|
||||
await screen.findByTestId('reply-comment-3');
|
||||
});
|
||||
|
||||
it('newly loaded responses are appended to the old ones', async () => {
|
||||
it('newly loaded replies are appended to the old ones', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const loadMoreButton = await findLoadMoreCommentsResponsesButton();
|
||||
@@ -687,7 +693,7 @@ describe('ThreadView', () => {
|
||||
expect(screen.queryByTestId('reply-comment-2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('load more button is hidden when no more responses pages to load', async () => {
|
||||
it('load more button is hidden when no more replies pages to load', async () => {
|
||||
await waitFor(() => renderComponent(discussionPostId));
|
||||
|
||||
const loadMoreButton = await findLoadMoreCommentsResponsesButton();
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button, Spinner } from '@openedx/paragon';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { EndorsementStatus } from '../../../data/constants';
|
||||
import { ThreadType } from '../../../data/constants';
|
||||
import { useUserPostingEnabled } from '../../data/hooks';
|
||||
import { isLastElementOfList } from '../../utils';
|
||||
import { usePostComments } from '../data/hooks';
|
||||
@@ -13,7 +13,7 @@ import messages from '../messages';
|
||||
import PostCommentsContext from '../postCommentsContext';
|
||||
import { Comment, ResponseEditor } from './comment';
|
||||
|
||||
const CommentsView = ({ endorsed }) => {
|
||||
const CommentsView = ({ threadType }) => {
|
||||
const intl = useIntl();
|
||||
const [addingResponse, setAddingResponse] = useState(false);
|
||||
const { isClosed } = useContext(PostCommentsContext);
|
||||
@@ -25,7 +25,7 @@ const CommentsView = ({ endorsed }) => {
|
||||
hasMorePages,
|
||||
isLoading,
|
||||
handleLoadMoreResponses,
|
||||
} = usePostComments(endorsed);
|
||||
} = usePostComments(threadType);
|
||||
|
||||
const handleAddResponse = useCallback(() => {
|
||||
setAddingResponse(true);
|
||||
@@ -45,7 +45,7 @@ const CommentsView = ({ endorsed }) => {
|
||||
</div>
|
||||
), []);
|
||||
|
||||
const handleComments = useCallback((postCommentsIds, showLoadMoreResponses = false) => (
|
||||
const handleComments = useCallback((postCommentsIds) => (
|
||||
<div className="mx-4" role="list">
|
||||
{postCommentsIds.map((commentId) => (
|
||||
<Comment
|
||||
@@ -54,72 +54,66 @@ const CommentsView = ({ endorsed }) => {
|
||||
marginBottom={isLastElementOfList(postCommentsIds, commentId)}
|
||||
/>
|
||||
))}
|
||||
{hasMorePages && !isLoading && !showLoadMoreResponses && (
|
||||
<Button
|
||||
onClick={handleLoadMoreResponses}
|
||||
variant="link"
|
||||
block="true"
|
||||
className="px-4 mt-3 border-0 line-height-24 py-0 mb-2 font-style font-weight-500 font-size-14"
|
||||
data-testid="load-more-comments"
|
||||
>
|
||||
{intl.formatMessage(messages.loadMoreResponses)}
|
||||
</Button>
|
||||
)}
|
||||
{isLoading && !showLoadMoreResponses && (
|
||||
<div className="mb-2 mt-3 d-flex justify-content-center">
|
||||
<Spinner animation="border" variant="primary" className="spinner-dimensions" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
), [hasMorePages, isLoading, handleLoadMoreResponses]);
|
||||
|
||||
return (
|
||||
((hasMorePages && isLoading) || !isLoading) && (
|
||||
<>
|
||||
{endorsedCommentsIds.length > 0 && (
|
||||
<>
|
||||
{endorsedCommentsIds.length > 0 && (
|
||||
<>
|
||||
{handleDefinition(messages.endorsedResponseCount, endorsedCommentsIds.length)}
|
||||
{endorsed === EndorsementStatus.DISCUSSION
|
||||
? handleComments(endorsedCommentsIds, true)
|
||||
: handleComments(endorsedCommentsIds, false)}
|
||||
</>
|
||||
)}
|
||||
{endorsed !== EndorsementStatus.ENDORSED && (
|
||||
<>
|
||||
{handleDefinition(messages.responseCount, unEndorsedCommentsIds.length)}
|
||||
{unEndorsedCommentsIds.length === 0 && <br />}
|
||||
{handleComments(unEndorsedCommentsIds, false)}
|
||||
{(isUserPrivilegedInPostingRestriction && !!unEndorsedCommentsIds.length && !isClosed) && (
|
||||
<div className="mx-4">
|
||||
{!addingResponse && (
|
||||
<Button
|
||||
variant="plain"
|
||||
block="true"
|
||||
className="card mb-4 px-0 border-0 py-10px mt-2 font-style font-weight-500
|
||||
line-height-24 font-size-14 text-primary-500"
|
||||
onClick={handleAddResponse}
|
||||
data-testid="add-response"
|
||||
>
|
||||
{intl.formatMessage(messages.addResponse)}
|
||||
</Button>
|
||||
)}
|
||||
<ResponseEditor
|
||||
addWrappingDiv
|
||||
addingResponse={addingResponse}
|
||||
handleCloseEditor={handleCloseResponseEditor}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{handleDefinition(messages.endorsedResponseCount, endorsedCommentsIds.length)}
|
||||
{handleComments(endorsedCommentsIds)}
|
||||
</>
|
||||
)}
|
||||
{handleDefinition(messages.responseCount, unEndorsedCommentsIds.length)}
|
||||
{unEndorsedCommentsIds.length > 0 && handleComments(unEndorsedCommentsIds)}
|
||||
{hasMorePages && !isLoading && (!!unEndorsedCommentsIds.length || !!endorsedCommentsIds.length) && (
|
||||
<Button
|
||||
onClick={handleLoadMoreResponses}
|
||||
variant="link"
|
||||
block="true"
|
||||
className="px-4 mt-3 border-0 line-height-24 py-0 mb-2 font-style font-weight-500 font-size-14"
|
||||
data-testid="load-more-comments"
|
||||
>
|
||||
{intl.formatMessage(messages.loadMoreResponses)}
|
||||
</Button>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="mb-2 mt-3 d-flex justify-content-center">
|
||||
<Spinner animation="border" variant="primary" className="spinner-dimensions" />
|
||||
</div>
|
||||
)}
|
||||
{(isUserPrivilegedInPostingRestriction && (!!unEndorsedCommentsIds.length || !!endorsedCommentsIds.length)
|
||||
&& !isClosed) && (
|
||||
<div className="mx-4">
|
||||
{!addingResponse && (
|
||||
<Button
|
||||
variant="plain"
|
||||
block="true"
|
||||
className="card mb-4 px-0 border-0 py-10px mt-2 font-style font-weight-500
|
||||
line-height-24 font-size-14 text-primary-500"
|
||||
onClick={handleAddResponse}
|
||||
data-testid="add-response"
|
||||
>
|
||||
{intl.formatMessage(messages.addResponse)}
|
||||
</Button>
|
||||
)}
|
||||
<ResponseEditor
|
||||
addWrappingDiv
|
||||
addingResponse={addingResponse}
|
||||
handleCloseEditor={handleCloseResponseEditor}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
CommentsView.propTypes = {
|
||||
endorsed: PropTypes.oneOf([
|
||||
EndorsementStatus.ENDORSED, EndorsementStatus.UNENDORSED, EndorsementStatus.DISCUSSION,
|
||||
threadType: PropTypes.oneOf([
|
||||
ThreadType.DISCUSSION, ThreadType.QUESTION,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ const Comment = ({
|
||||
}, []);
|
||||
|
||||
const handleCommentEndorse = useCallback(async () => {
|
||||
await dispatch(editComment(id, { endorsed: !endorsed }, ContentActions.ENDORSE));
|
||||
await dispatch(editComment(id, { endorsed: !endorsed }));
|
||||
await dispatch(fetchThread(threadId, courseId));
|
||||
}, [id, endorsed, threadId]);
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ const Reply = ({ responseId }) => {
|
||||
}, []);
|
||||
|
||||
const handleReplyEndorse = useCallback(() => {
|
||||
dispatch(editComment(id, { endorsed: !endorsed }, ContentActions.ENDORSE));
|
||||
dispatch(editComment(id, { endorsed: !endorsed }));
|
||||
}, [endorsed, id]);
|
||||
|
||||
const handleAbusedFlag = useCallback(() => {
|
||||
|
||||
@@ -6,7 +6,7 @@ Factory.define('comment')
|
||||
.sequence('rendered_body', ['endorsed'], (idx, endorsed) => `Some contents for <b>${endorsed ? 'endorsed ' : 'unendorsed '}comment number ${idx}</b>.`)
|
||||
.attr('thread_id', null, 'test-thread')
|
||||
.option('endorsedBy', null, null)
|
||||
.attr('endorsed', ['endorsedBy'], (endorsedBy) => !!endorsedBy)
|
||||
.attr('endorsed', ['endorsed'], (endorsed) => endorsed)
|
||||
.attr('endorsed_by', ['endorsedBy'], (endorsedBy) => endorsedBy)
|
||||
.attr('endorsed_by_label', ['endorsedBy'], (endorsedBy) => (endorsedBy ? 'Staff' : null))
|
||||
.attr('endorsed_at', ['endorsedBy'], (endorsedBy) => (endorsedBy ? (new Date()).toISOString() : null))
|
||||
@@ -38,7 +38,7 @@ Factory.define('commentsResult')
|
||||
.option('pageSize', null, 5)
|
||||
.option('threadId', null, 'test-thread')
|
||||
.option('parentId', null, null)
|
||||
.option('endorsed', null, null)
|
||||
.option('endorsed', false, false)
|
||||
.option('childCount', null, 0)
|
||||
.attr('pagination', ['threadId', 'count', 'page', 'pageSize'], (threadId, count, page, pageSize) => {
|
||||
const numPages = Math.ceil(count / pageSize);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ensureConfig, getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { EndorsementValue } from '../../../data/constants';
|
||||
import { ThreadType } from '../../../data/constants';
|
||||
|
||||
ensureConfig([
|
||||
'LMS_BASE_URL',
|
||||
@@ -20,7 +20,7 @@ export const getCommentsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussi
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export const getThreadComments = async (threadId, {
|
||||
endorsed,
|
||||
threadType,
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
@@ -29,12 +29,12 @@ export const getThreadComments = async (threadId, {
|
||||
} = {}) => {
|
||||
const params = snakeCaseObject({
|
||||
threadId,
|
||||
endorsed: EndorsementValue[endorsed],
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
requestedFields: 'profile_image',
|
||||
enableInContextSidebar,
|
||||
mergeQuestionTypeResponses: threadType === ThreadType.QUESTION ? true : null,
|
||||
});
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient().get(getCommentsApiUrl(), { params: { ...params, signal } });
|
||||
|
||||
@@ -40,13 +40,13 @@ export function usePost(postId) {
|
||||
return thread || {};
|
||||
}
|
||||
|
||||
export function usePostComments(endorsed = null) {
|
||||
export function usePostComments(threadType) {
|
||||
const { enableInContextSidebar, postId } = useContext(DiscussionContext);
|
||||
const [isLoading, dispatch] = useDispatchWithState();
|
||||
const comments = useSelector(selectThreadComments(postId, endorsed));
|
||||
const comments = useSelector(selectThreadComments(postId));
|
||||
const reverseOrder = useSelector(selectCommentSortOrder);
|
||||
const hasMorePages = useSelector(selectThreadHasMorePages(postId, endorsed));
|
||||
const currentPage = useSelector(selectThreadCurrentPage(postId, endorsed));
|
||||
const hasMorePages = useSelector(selectThreadHasMorePages(postId));
|
||||
const currentPage = useSelector(selectThreadCurrentPage(postId));
|
||||
|
||||
const endorsedCommentsIds = useMemo(() => (
|
||||
[...filterPosts(comments, 'endorsed')].map(comment => comment.id)
|
||||
@@ -58,19 +58,19 @@ export function usePostComments(endorsed = null) {
|
||||
|
||||
const handleLoadMoreResponses = useCallback(async () => {
|
||||
const params = {
|
||||
endorsed,
|
||||
threadType,
|
||||
page: currentPage + 1,
|
||||
reverseOrder,
|
||||
};
|
||||
await dispatch(fetchThreadComments(postId, params));
|
||||
trackLoadMoreEvent(postId, params);
|
||||
}, [currentPage, endorsed, postId, reverseOrder]);
|
||||
}, [currentPage, threadType, postId, reverseOrder]);
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
dispatch(fetchThreadComments(postId, {
|
||||
endorsed,
|
||||
threadType,
|
||||
page: 1,
|
||||
reverseOrder,
|
||||
enableInContextSidebar,
|
||||
@@ -80,7 +80,7 @@ export function usePostComments(endorsed = null) {
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [postId, endorsed, reverseOrder, enableInContextSidebar]);
|
||||
}, [postId, threadType, reverseOrder, enableInContextSidebar]);
|
||||
|
||||
return {
|
||||
endorsedCommentsIds,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Factory } from 'rosie';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
|
||||
import { EndorsementStatus } from '../../../data/constants';
|
||||
import { ThreadType } from '../../../data/constants';
|
||||
import { initializeStore } from '../../../store';
|
||||
import executeThunk from '../../../test-utils';
|
||||
import { getCommentsApiUrl } from './api';
|
||||
@@ -39,37 +39,23 @@ describe('Comments/Responses data layer tests', () => {
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
threadType: 'discussion',
|
||||
endorsed: EndorsementStatus.DISCUSSION,
|
||||
},
|
||||
{
|
||||
threadType: 'question',
|
||||
endorsed: EndorsementStatus.UNENDORSED,
|
||||
},
|
||||
{
|
||||
threadType: 'question',
|
||||
endorsed: EndorsementStatus.ENDORSED,
|
||||
},
|
||||
])('successfully processes comments for \'$threadType\' thread with endorsed=$endorsed', async ({
|
||||
endorsed,
|
||||
}) => {
|
||||
ThreadType.DISCUSSION,
|
||||
ThreadType.QUESTION,
|
||||
])('successfully processes comments for %s type thread', async (threadType) => {
|
||||
const threadId = 'test-thread';
|
||||
axiosMock.onGet(commentsApiUrl)
|
||||
.reply(200, Factory.build('commentsResult'));
|
||||
|
||||
await executeThunk(fetchThreadComments(threadId, { endorsed }), store.dispatch, store.getState);
|
||||
await executeThunk(fetchThreadComments(threadId, { threadType }), store.dispatch, store.getState);
|
||||
|
||||
expect(store.getState().comments.commentsInThreads)
|
||||
.toEqual({ 'test-thread': { [endorsed]: ['comment-1', 'comment-2', 'comment-3'] } });
|
||||
.toEqual({ 'test-thread': ['comment-1', 'comment-2', 'comment-3'] });
|
||||
expect(store.getState().comments.pagination)
|
||||
.toEqual({
|
||||
'test-thread': {
|
||||
[endorsed]: {
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
hasMorePages: false,
|
||||
},
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
hasMorePages: false,
|
||||
},
|
||||
});
|
||||
expect(Object.keys(store.getState().comments.commentsById))
|
||||
@@ -82,7 +68,7 @@ describe('Comments/Responses data layer tests', () => {
|
||||
.toEqual('test-thread');
|
||||
});
|
||||
|
||||
test('successfully processes comment responses', async () => {
|
||||
test('successfully processes comment replies', async () => {
|
||||
const threadId = 'test-thread';
|
||||
const commentId = 'comment-1';
|
||||
axiosMock.onGet(commentsApiUrl)
|
||||
@@ -101,7 +87,7 @@ describe('Comments/Responses data layer tests', () => {
|
||||
.toEqual({ 'comment-1': ['comment-4', 'comment-5', 'comment-6'] });
|
||||
});
|
||||
|
||||
test('successfully handles response creation for discussion type threads', async () => {
|
||||
test('successfully handles comment creation for threads', async () => {
|
||||
const threadId = 'test-thread';
|
||||
const content = 'Test comment';
|
||||
axiosMock.onGet(commentsApiUrl)
|
||||
@@ -119,21 +105,19 @@ describe('Comments/Responses data layer tests', () => {
|
||||
await executeThunk(addComment(content, threadId, null), store.dispatch, store.getState);
|
||||
|
||||
expect(store.getState().comments.commentsInThreads[threadId])
|
||||
.toEqual({
|
||||
[EndorsementStatus.DISCUSSION]: [
|
||||
'comment-1',
|
||||
'comment-2',
|
||||
'comment-3',
|
||||
'comment-4',
|
||||
],
|
||||
});
|
||||
.toEqual([
|
||||
'comment-1',
|
||||
'comment-2',
|
||||
'comment-3',
|
||||
'comment-4',
|
||||
]);
|
||||
expect(Object.keys(store.getState().comments.commentsById))
|
||||
.toEqual(['comment-1', 'comment-2', 'comment-3', 'comment-4']);
|
||||
expect(store.getState().comments.commentsById['comment-4'].threadId)
|
||||
.toEqual(threadId);
|
||||
});
|
||||
|
||||
test('successfully handles reply creation for discussion type threads', async () => {
|
||||
test('successfully handles reply creation for threads', async () => {
|
||||
const threadId = 'test-thread';
|
||||
const parentId = 'comment-1';
|
||||
const content = 'Test comment';
|
||||
@@ -156,13 +140,11 @@ describe('Comments/Responses data layer tests', () => {
|
||||
await executeThunk(addComment(content, threadId, null), store.dispatch, store.getState);
|
||||
|
||||
expect(store.getState().comments.commentsInThreads[threadId])
|
||||
.toEqual({
|
||||
[EndorsementStatus.DISCUSSION]: [
|
||||
'comment-1',
|
||||
'comment-2',
|
||||
'comment-3',
|
||||
],
|
||||
});
|
||||
.toEqual([
|
||||
'comment-1',
|
||||
'comment-2',
|
||||
'comment-3',
|
||||
]);
|
||||
expect(Object.keys(store.getState().comments.commentsById))
|
||||
.toEqual(['comment-1', 'comment-2', 'comment-3', 'comment-4']);
|
||||
expect(store.getState().comments.commentsInComments[parentId])
|
||||
@@ -173,54 +155,6 @@ describe('Comments/Responses data layer tests', () => {
|
||||
.toEqual(parentId);
|
||||
});
|
||||
|
||||
test('successfully handles comment creation for question type threads', async () => {
|
||||
const threadId = 'test-thread';
|
||||
const content = 'Test comment';
|
||||
axiosMock.onGet(commentsApiUrl)
|
||||
.reply(200, Factory.build('commentsResult', null, { endorsed: false }));
|
||||
await executeThunk(
|
||||
fetchThreadComments(threadId, { endorsed: EndorsementStatus.UNENDORSED }),
|
||||
store.dispatch,
|
||||
store.getState,
|
||||
);
|
||||
axiosMock.onGet(commentsApiUrl)
|
||||
.reply(200, Factory.build('commentsResult', null, { endorsed: true }));
|
||||
await executeThunk(
|
||||
fetchThreadComments(threadId, { endorsed: EndorsementStatus.ENDORSED }),
|
||||
store.dispatch,
|
||||
store.getState,
|
||||
);
|
||||
|
||||
axiosMock.onPost(commentsApiUrl)
|
||||
.reply(200, Factory.build('comment', {
|
||||
thread_id: threadId,
|
||||
raw_body: content,
|
||||
rendered_body: content,
|
||||
}));
|
||||
|
||||
await executeThunk(addComment(content, threadId, null), store.dispatch, store.getState);
|
||||
|
||||
expect(store.getState().comments.commentsInThreads[threadId])
|
||||
.toEqual({
|
||||
[EndorsementStatus.UNENDORSED]: [
|
||||
'comment-1',
|
||||
'comment-2',
|
||||
'comment-3',
|
||||
// Newly-added comment
|
||||
'comment-7',
|
||||
],
|
||||
[EndorsementStatus.ENDORSED]: [
|
||||
'comment-4',
|
||||
'comment-5',
|
||||
'comment-6',
|
||||
],
|
||||
});
|
||||
expect(Object.keys(store.getState().comments.commentsById))
|
||||
.toEqual(['comment-1', 'comment-2', 'comment-3', 'comment-4', 'comment-5', 'comment-6', 'comment-7']);
|
||||
expect(store.getState().comments.commentsById['comment-7'].threadId)
|
||||
.toEqual(threadId);
|
||||
});
|
||||
|
||||
test('successfully handles comment edits', async () => {
|
||||
const threadId = 'test-thread';
|
||||
const commentId = 'comment-1';
|
||||
@@ -271,7 +205,7 @@ describe('Comments/Responses data layer tests', () => {
|
||||
.toContain(commentId);
|
||||
});
|
||||
|
||||
test('correctly handles comment responses pagination after posting a new response', async () => {
|
||||
test('correctly handles comment replies pagination after posting a new reply', async () => {
|
||||
const threadId = 'test-thread';
|
||||
const commentId = 'comment-1';
|
||||
|
||||
@@ -327,15 +261,9 @@ describe('Comments/Responses data layer tests', () => {
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
threadType: 'discussion',
|
||||
endorsed: EndorsementStatus.DISCUSSION,
|
||||
},
|
||||
{
|
||||
threadType: 'unendorsed',
|
||||
endorsed: EndorsementStatus.UNENDORSED,
|
||||
},
|
||||
])('correctly handles `$threadType` thread comments pagination after posting a new comment', async ({ endorsed }) => {
|
||||
ThreadType.DISCUSSION,
|
||||
ThreadType.QUESTION,
|
||||
])('correctly handles %s thread comments pagination after posting a new comment', async (threadType) => {
|
||||
const threadId = 'test-thread';
|
||||
|
||||
// Build all comments first, so we can paginate over them and they
|
||||
@@ -348,7 +276,7 @@ describe('Comments/Responses data layer tests', () => {
|
||||
results: allComments.slice(0, 3),
|
||||
pagination: { count: 4, numPages: 2 },
|
||||
});
|
||||
await executeThunk(fetchThreadComments(threadId, { endorsed }), store.dispatch, store.getState);
|
||||
await executeThunk(fetchThreadComments(threadId, { threadType }), store.dispatch, store.getState);
|
||||
|
||||
// Post new comment
|
||||
const comment = Factory.build('comment', { thread_id: threadId });
|
||||
@@ -365,10 +293,10 @@ describe('Comments/Responses data layer tests', () => {
|
||||
results: allComments.slice(3, 6),
|
||||
pagination: { count: 6, numPages: 2 },
|
||||
});
|
||||
await executeThunk(fetchThreadComments(threadId, { page: 2, endorsed }), store.dispatch, store.getState);
|
||||
await executeThunk(fetchThreadComments(threadId, { page: 2, threadType }), store.dispatch, store.getState);
|
||||
|
||||
// sorting is implemented on backend
|
||||
expect(store.getState().comments.commentsInThreads[threadId][endorsed])
|
||||
expect(store.getState().comments.commentsInThreads[threadId])
|
||||
.toEqual([
|
||||
'comment-1',
|
||||
'comment-2',
|
||||
|
||||
@@ -8,9 +8,9 @@ export const selectCommentOrResponseById = commentOrResponseId => createSelector
|
||||
comments => comments[commentOrResponseId],
|
||||
);
|
||||
|
||||
export const selectThreadComments = (threadId, endorsed = null) => createSelector(
|
||||
export const selectThreadComments = (threadId) => createSelector(
|
||||
[
|
||||
state => state.comments.commentsInThreads[threadId]?.[endorsed] || [],
|
||||
state => state.comments.commentsInThreads[threadId] || [],
|
||||
selectCommentsById,
|
||||
],
|
||||
mapIdToComment,
|
||||
@@ -28,12 +28,12 @@ export const selectCommentResponses = commentId => createSelector(
|
||||
mapIdToComment,
|
||||
);
|
||||
|
||||
export const selectThreadHasMorePages = (threadId, endorsed = null) => (
|
||||
state => state.comments.pagination[threadId]?.[endorsed]?.hasMorePages || false
|
||||
export const selectThreadHasMorePages = (threadId) => (
|
||||
state => state.comments.pagination[threadId]?.hasMorePages || false
|
||||
);
|
||||
|
||||
export const selectThreadCurrentPage = (threadId, endorsed = null) => (
|
||||
state => state.comments.pagination[threadId]?.[endorsed]?.currentPage || null
|
||||
export const selectThreadCurrentPage = (threadId) => (
|
||||
state => state.comments.pagination[threadId]?.currentPage || null
|
||||
);
|
||||
|
||||
export const selectCommentHasMorePages = commentId => (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { EndorsementStatus, RequestStatus } from '../../../data/constants';
|
||||
import { RequestStatus } from '../../../data/constants';
|
||||
|
||||
const commentsSlice = createSlice({
|
||||
name: 'comments',
|
||||
@@ -31,15 +31,14 @@ const commentsSlice = createSlice({
|
||||
}
|
||||
),
|
||||
fetchCommentsSuccess: (state, { payload }) => {
|
||||
const { threadId, page, endorsed } = payload;
|
||||
const { threadId, page } = payload;
|
||||
|
||||
const newState = { ...state };
|
||||
|
||||
newState.status = RequestStatus.SUCCESSFUL;
|
||||
|
||||
newState.commentsInThreads = {
|
||||
...newState.commentsInThreads,
|
||||
[threadId]: newState.commentsInThreads[threadId] || {},
|
||||
[threadId]: newState.commentsInThreads[threadId] || [],
|
||||
};
|
||||
|
||||
newState.pagination = {
|
||||
@@ -50,23 +49,16 @@ const commentsSlice = createSlice({
|
||||
if (page === 1) {
|
||||
newState.commentsInThreads = {
|
||||
...newState.commentsInThreads,
|
||||
[threadId]: {
|
||||
...newState.commentsInThreads[threadId],
|
||||
[endorsed]: payload.commentsInThreads[threadId] || [],
|
||||
},
|
||||
[threadId]: [...payload.commentsInThreads[threadId]] || [],
|
||||
};
|
||||
} else {
|
||||
newState.commentsInThreads = {
|
||||
...newState.commentsInThreads,
|
||||
[threadId]: {
|
||||
...newState.commentsInThreads[threadId],
|
||||
[endorsed]: [
|
||||
...new Set([
|
||||
...(newState.commentsInThreads[threadId][endorsed] || []),
|
||||
...(payload.commentsInThreads[threadId] || []),
|
||||
]),
|
||||
],
|
||||
},
|
||||
[threadId]: [
|
||||
...new Set([
|
||||
...(newState.commentsInThreads[threadId] || []),
|
||||
...(payload.commentsInThreads[threadId] || []),
|
||||
]),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,11 +66,9 @@ const commentsSlice = createSlice({
|
||||
...newState.pagination,
|
||||
[threadId]: {
|
||||
...newState.pagination[threadId],
|
||||
[endorsed]: {
|
||||
currentPage: payload.page,
|
||||
totalPages: payload.pagination.numPages,
|
||||
hasMorePages: Boolean(payload.pagination.next),
|
||||
},
|
||||
currentPage: payload.page,
|
||||
totalPages: payload.pagination.numPages,
|
||||
hasMorePages: Boolean(payload.pagination.next),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -181,21 +171,10 @@ const commentsSlice = createSlice({
|
||||
],
|
||||
};
|
||||
} else {
|
||||
const threadComments = newState.commentsInThreads[payload.threadId] || {};
|
||||
const endorsementStatus = threadComments[EndorsementStatus.DISCUSSION]
|
||||
? EndorsementStatus.DISCUSSION
|
||||
: EndorsementStatus.UNENDORSED;
|
||||
|
||||
const updatedThreadComments = {
|
||||
...threadComments,
|
||||
[endorsementStatus]: [
|
||||
...(threadComments[endorsementStatus] || []),
|
||||
payload.id,
|
||||
],
|
||||
};
|
||||
const threadComments = newState.commentsInThreads[payload.threadId] || [];
|
||||
newState.commentsInThreads = {
|
||||
...newState.commentsInThreads,
|
||||
[payload.threadId]: updatedThreadComments,
|
||||
[payload.threadId]: [...threadComments, payload.id],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -231,30 +210,7 @@ const commentsSlice = createSlice({
|
||||
[payload.id]: payload,
|
||||
},
|
||||
commentDraft: null,
|
||||
}
|
||||
),
|
||||
updateCommentsList: (state, { payload }) => {
|
||||
const { id: commentId, threadId, endorsed } = payload;
|
||||
const commentAddListtype = endorsed ? EndorsementStatus.ENDORSED : EndorsementStatus.UNENDORSED;
|
||||
const commentRemoveListType = !endorsed ? EndorsementStatus.ENDORSED : EndorsementStatus.UNENDORSED;
|
||||
|
||||
const updatedThread = { ...state.commentsInThreads[threadId] };
|
||||
|
||||
updatedThread[commentRemoveListType] = updatedThread[commentRemoveListType]
|
||||
?.filter(item => item !== commentId)
|
||||
?? [];
|
||||
updatedThread[commentAddListtype] = [
|
||||
...(updatedThread[commentAddListtype] || []), commentId,
|
||||
];
|
||||
|
||||
return {
|
||||
...state,
|
||||
commentsInThreads: {
|
||||
...state.commentsInThreads,
|
||||
[threadId]: updatedThread,
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
deleteCommentRequest: (state) => (
|
||||
{
|
||||
...state,
|
||||
@@ -285,12 +241,9 @@ const commentsSlice = createSlice({
|
||||
commentsById: { ...state.commentsById },
|
||||
};
|
||||
|
||||
[EndorsementStatus.DISCUSSION, EndorsementStatus.UNENDORSED, EndorsementStatus.ENDORSED].forEach((endorsed) => {
|
||||
newState.commentsInThreads[threadId] = {
|
||||
...newState.commentsInThreads[threadId],
|
||||
[endorsed]: newState.commentsInThreads[threadId]?.[endorsed]?.filter(item => item !== commentId),
|
||||
};
|
||||
});
|
||||
newState.commentsInThreads[threadId] = [
|
||||
...newState.commentsInThreads[threadId]?.filter(item => item !== commentId) || [],
|
||||
];
|
||||
|
||||
if (parentId) {
|
||||
newState.commentsInComments[parentId] = newState.commentsInComments[parentId].filter(
|
||||
@@ -328,7 +281,6 @@ export const {
|
||||
updateCommentFailed,
|
||||
updateCommentRequest,
|
||||
updateCommentSuccess,
|
||||
updateCommentsList,
|
||||
deleteCommentDenied,
|
||||
deleteCommentFailed,
|
||||
deleteCommentRequest,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import { ContentActions, EndorsementStatus } from '../../../data/constants';
|
||||
import { getHttpErrorStatus } from '../../utils';
|
||||
import {
|
||||
deleteComment, getCommentResponses, getThreadComments, postComment, updateComment,
|
||||
@@ -26,7 +25,6 @@ import {
|
||||
updateCommentDenied,
|
||||
updateCommentFailed,
|
||||
updateCommentRequest,
|
||||
updateCommentsList,
|
||||
updateCommentSuccess,
|
||||
} from './slices';
|
||||
|
||||
@@ -78,7 +76,7 @@ export function fetchThreadComments(
|
||||
{
|
||||
page = 1,
|
||||
reverseOrder,
|
||||
endorsed = EndorsementStatus.DISCUSSION,
|
||||
threadType,
|
||||
enableInContextSidebar,
|
||||
signal,
|
||||
} = {},
|
||||
@@ -87,11 +85,10 @@ export function fetchThreadComments(
|
||||
try {
|
||||
dispatch(fetchCommentsRequest());
|
||||
const data = await getThreadComments(threadId, {
|
||||
page, reverseOrder, endorsed, enableInContextSidebar, signal,
|
||||
page, reverseOrder, threadType, enableInContextSidebar, signal,
|
||||
});
|
||||
dispatch(fetchCommentsSuccess({
|
||||
...normaliseComments(camelCaseObject(data)),
|
||||
endorsed,
|
||||
page,
|
||||
threadId,
|
||||
}));
|
||||
@@ -127,15 +124,12 @@ export function fetchCommentResponses(commentId, { page = 1, reverseOrder = true
|
||||
};
|
||||
}
|
||||
|
||||
export function editComment(commentId, comment, action = null) {
|
||||
export function editComment(commentId, comment) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
dispatch(updateCommentRequest({ commentId }));
|
||||
const data = await updateComment(commentId, comment);
|
||||
dispatch(updateCommentSuccess(camelCaseObject(data)));
|
||||
if (action === ContentActions.ENDORSE) {
|
||||
dispatch(updateCommentsList(camelCaseObject(data)));
|
||||
}
|
||||
} catch (error) {
|
||||
if (getHttpErrorStatus(error) === 403) {
|
||||
dispatch(updateCommentDenied());
|
||||
|
||||
@@ -61,10 +61,6 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.font-style-normal {
|
||||
font-style: normal !important;
|
||||
}
|
||||
|
||||
.post-footer-icon-dimensions {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
@@ -496,7 +492,7 @@ code {
|
||||
}
|
||||
|
||||
.font-style {
|
||||
font-style: normal;
|
||||
font-style: normal !important;
|
||||
}
|
||||
|
||||
.in-context-navbar {
|
||||
|
||||
Reference in New Issue
Block a user