feat: add support for loading endorsed and un-endorsed comments separately for question type posts
This commit is contained in:
@@ -2,8 +2,8 @@ import { Factory } from 'rosie';
|
||||
|
||||
Factory.define('comment')
|
||||
.sequence('id', (idx) => `comment-${idx}`)
|
||||
.sequence('raw_body', (idx) => `Some contents for **comment number ${idx}**.`)
|
||||
.sequence('rendered_body', (idx) => `Some contents for <b>comment number ${idx}</b>.`)
|
||||
.sequence('raw_body', ['endorsed'], (idx, endorsed) => `Some contents for **${endorsed ? 'endorsed ' : 'unendorsed '}comment number ${idx}**.`)
|
||||
.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)
|
||||
@@ -36,6 +36,7 @@ Factory.define('commentsResult')
|
||||
.option('pageSize', null, 5)
|
||||
.option('threadId', null, 'test-thread')
|
||||
.option('parentId', null, null)
|
||||
.option('endorsed', null, null)
|
||||
.attr('pagination', ['threadId', 'count', 'page', 'pageSize'], (threadId, count, page, pageSize) => {
|
||||
const numPages = Math.ceil(count / pageSize);
|
||||
const next = (page < numPages) ? `http://test.site/api/discussion/v1/comments/?thread_id=${threadId}&page=${page + 1}` : null;
|
||||
@@ -47,7 +48,12 @@ Factory.define('commentsResult')
|
||||
num_pages: numPages,
|
||||
};
|
||||
})
|
||||
.attr('results', ['count', 'pageSize', 'page', 'threadId', 'parentId'], (count, pageSize, page, threadId, parentId) => {
|
||||
.attr('results', ['count', 'pageSize', 'page', 'threadId', 'parentId', 'endorsed'], (count, pageSize, page, threadId, parentId, endorsed) => {
|
||||
const len = (pageSize * page <= count) ? pageSize : count % pageSize;
|
||||
return Factory.buildList('comment', len, { thread_id: threadId, parent_id: parentId });
|
||||
return Factory.buildList('comment', len, {
|
||||
thread_id: threadId,
|
||||
parent_id: parentId,
|
||||
}, {
|
||||
endorsedBy: endorsed ? 'staff' : null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { ensureConfig, getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { EndorsementValue } from '../../../data/constants';
|
||||
|
||||
ensureConfig([
|
||||
'LMS_BASE_URL',
|
||||
], 'Comments API service');
|
||||
@@ -13,18 +15,21 @@ export const commentsApiUrl = `${apiBaseUrl}/api/discussion/v1/comments/`;
|
||||
/**
|
||||
* Returns all the comments for the specified thread.
|
||||
* @param {string} threadId
|
||||
* @param {EndorsementStatus} endorsed
|
||||
* @param {number=} page
|
||||
* @param {number=} pageSize
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
export async function getThreadComments(
|
||||
threadId, {
|
||||
endorsed,
|
||||
page,
|
||||
pageSize,
|
||||
} = {},
|
||||
) {
|
||||
const params = snakeCaseObject({
|
||||
threadId,
|
||||
endorsed: EndorsementValue[endorsed],
|
||||
page,
|
||||
pageSize,
|
||||
requestedFields: 'profile_image',
|
||||
|
||||
@@ -4,6 +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 { initializeStore } from '../../../store';
|
||||
import { executeThunk } from '../../../test-utils';
|
||||
import { commentsApiUrl } from './api';
|
||||
@@ -36,17 +37,40 @@ describe('Comments/Responses data layer tests', () => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
test('successfully processes comments', async () => {
|
||||
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,
|
||||
}) => {
|
||||
const threadId = 'test-thread';
|
||||
axiosMock.onGet(commentsApiUrl)
|
||||
.reply(200, Factory.build('commentsResult'));
|
||||
|
||||
await executeThunk(fetchThreadComments(threadId), store.dispatch, store.getState);
|
||||
await executeThunk(fetchThreadComments(threadId, { endorsed }), store.dispatch, store.getState);
|
||||
|
||||
expect(store.getState().comments.commentsInThreads)
|
||||
.toEqual({ 'test-thread': ['comment-1', 'comment-2', 'comment-3'] });
|
||||
.toEqual({ 'test-thread': { [endorsed]: ['comment-1', 'comment-2', 'comment-3'] } });
|
||||
expect(store.getState().comments.pagination)
|
||||
.toEqual({ 'test-thread': { currentPage: 1, totalPages: 1, hasMorePages: false } });
|
||||
.toEqual({
|
||||
'test-thread': {
|
||||
[endorsed]: {
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
hasMorePages: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(Object.keys(store.getState().comments.commentsById))
|
||||
.toEqual(['comment-1', 'comment-2', 'comment-3']);
|
||||
expect(store.getState().comments.commentsById['comment-1'])
|
||||
@@ -76,7 +100,7 @@ describe('Comments/Responses data layer tests', () => {
|
||||
.toEqual({ 'comment-1': ['comment-4', 'comment-5', 'comment-6'] });
|
||||
});
|
||||
|
||||
test('successfully handles comment creation', async () => {
|
||||
test('successfully handles comment creation for discussion type threads', async () => {
|
||||
const threadId = 'test-thread';
|
||||
const content = 'Test comment';
|
||||
axiosMock.onGet(commentsApiUrl)
|
||||
@@ -94,13 +118,68 @@ describe('Comments/Responses data layer tests', () => {
|
||||
await executeThunk(addComment(content, threadId, null), store.dispatch, store.getState);
|
||||
|
||||
expect(store.getState().comments.commentsInThreads[threadId])
|
||||
.toEqual(['comment-1', 'comment-2', 'comment-3', 'comment-4']);
|
||||
.toEqual({
|
||||
[EndorsementStatus.DISCUSSION]: [
|
||||
'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 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';
|
||||
|
||||
@@ -4,9 +4,9 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
const selectCommentsById = state => state.comments.commentsById;
|
||||
const mapIdToComment = (ids, comments) => ids.map(id => comments[id]);
|
||||
|
||||
export const selectThreadComments = threadId => createSelector(
|
||||
export const selectThreadComments = (threadId, endorsed = null) => createSelector(
|
||||
[
|
||||
state => state.comments.commentsInThreads[threadId] || [],
|
||||
state => state.comments.commentsInThreads[threadId]?.[endorsed] || [],
|
||||
selectCommentsById,
|
||||
],
|
||||
mapIdToComment,
|
||||
@@ -20,12 +20,12 @@ export const selectCommentResponses = commentId => createSelector(
|
||||
mapIdToComment,
|
||||
);
|
||||
|
||||
export const selectThreadHasMorePages = threadId => (
|
||||
store => store.comments.pagination[threadId]?.hasMorePages || false
|
||||
export const selectThreadHasMorePages = (threadId, endorsed = null) => (
|
||||
store => store.comments.pagination[threadId]?.[endorsed]?.hasMorePages || false
|
||||
);
|
||||
|
||||
export const selectThreadCurrentPage = threadId => (
|
||||
store => store.comments.pagination[threadId]?.currentPage || null
|
||||
export const selectThreadCurrentPage = (threadId, endorsed = null) => (
|
||||
store => store.comments.pagination[threadId]?.[endorsed]?.currentPage || null
|
||||
);
|
||||
|
||||
export const commentsStatus = state => state.comments.status;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-param-reassign,import/prefer-default-export */
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { RequestStatus } from '../../../data/constants';
|
||||
import { EndorsementStatus, RequestStatus } from '../../../data/constants';
|
||||
|
||||
const commentsSlice = createSlice({
|
||||
name: 'comments',
|
||||
@@ -20,26 +20,29 @@ const commentsSlice = createSlice({
|
||||
// TODO: save in localstorage so user can continue editing?
|
||||
commentDraft: null,
|
||||
postStatus: RequestStatus.SUCCESSFUL,
|
||||
pagination: {
|
||||
},
|
||||
pagination: {},
|
||||
},
|
||||
reducers: {
|
||||
fetchCommentsRequest: (state) => {
|
||||
state.status = RequestStatus.IN_PROGRESS;
|
||||
},
|
||||
fetchCommentsSuccess: (state, { payload }) => {
|
||||
const { threadId, endorsed } = payload;
|
||||
// force endorsed to be null, true or false
|
||||
state.status = RequestStatus.SUCCESSFUL;
|
||||
state.commentsInThreads[payload.threadId] = [
|
||||
...(state.commentsInThreads[payload.threadId] || []),
|
||||
...(payload.commentsInThreads[payload.threadId] || []),
|
||||
state.commentsInThreads[threadId] = state.commentsInThreads[threadId] ?? {};
|
||||
state.pagination[threadId] = state.pagination[threadId] ?? {};
|
||||
state.commentsInThreads[threadId][endorsed] = [
|
||||
...(state.commentsInThreads[threadId][endorsed] || []),
|
||||
...(payload.commentsInThreads[threadId] || []),
|
||||
];
|
||||
state.commentsInComments = { ...state.commentsInComments, ...payload.commentsInComments };
|
||||
state.commentsById = { ...state.commentsById, ...payload.commentsById };
|
||||
state.pagination[payload.threadId] = {
|
||||
state.pagination[threadId][endorsed] = {
|
||||
currentPage: payload.page,
|
||||
totalPages: payload.pagination.numPages,
|
||||
hasMorePages: Boolean(payload.pagination.next),
|
||||
};
|
||||
state.commentsInComments = { ...state.commentsInComments, ...payload.commentsInComments };
|
||||
state.commentsById = { ...state.commentsById, ...payload.commentsById };
|
||||
},
|
||||
fetchCommentsFailed: (state) => {
|
||||
state.status = RequestStatus.FAILED;
|
||||
@@ -76,7 +79,12 @@ const commentsSlice = createSlice({
|
||||
if (payload.parentId) {
|
||||
state.commentsInComments[payload.parentId].push(payload.id);
|
||||
} else {
|
||||
state.commentsInThreads[payload.threadId].push(payload.id);
|
||||
// The comment should be added to either the discussion or unendorsed
|
||||
// sections since a new comment won't be endorsed yet.
|
||||
(
|
||||
state.commentsInThreads[payload.threadId][EndorsementStatus.DISCUSSION]
|
||||
?? state.commentsInThreads[payload.threadId][EndorsementStatus.UNENDORSED]
|
||||
).push(payload.id);
|
||||
}
|
||||
state.commentsById[payload.id] = payload;
|
||||
state.commentDraft = null;
|
||||
@@ -108,8 +116,13 @@ const commentsSlice = createSlice({
|
||||
deleteCommentSuccess: (state, { payload }) => {
|
||||
const { commentId } = payload;
|
||||
const { threadId, parentId } = state.commentsById[commentId];
|
||||
|
||||
state.postStatus = RequestStatus.SUCCESSFUL;
|
||||
state.commentsInThreads[threadId] = state.commentsInThreads[threadId].filter(item => item !== commentId);
|
||||
[EndorsementStatus.DISCUSSION, EndorsementStatus.UNENDORSED, EndorsementStatus.ENDORSED].forEach((endorsed) => {
|
||||
state.commentsInThreads[threadId][endorsed] = (
|
||||
state.commentsInThreads[threadId]?.[endorsed]?.filter(item => item !== commentId)
|
||||
);
|
||||
});
|
||||
if (parentId) {
|
||||
state.commentsInComments[parentId] = state.commentsInComments[parentId].filter(item => item !== commentId);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import { EndorsementStatus } from '../../../data/constants';
|
||||
import { getHttpErrorStatus } from '../../utils';
|
||||
import {
|
||||
deleteComment, getCommentResponses, getThreadComments, postComment, updateComment,
|
||||
@@ -72,13 +73,14 @@ function normaliseComments(data) {
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchThreadComments(threadId, { page = 1 } = {}) {
|
||||
export function fetchThreadComments(threadId, { page = 1, endorsed = EndorsementStatus.DISCUSSION } = {}) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
dispatch(fetchCommentsRequest({ threadId }));
|
||||
const data = await getThreadComments(threadId, { page });
|
||||
dispatch(fetchCommentsRequest());
|
||||
const data = await getThreadComments(threadId, { page, endorsed });
|
||||
dispatch(fetchCommentsSuccess({
|
||||
...normaliseComments(camelCaseObject(data)),
|
||||
endorsed,
|
||||
page,
|
||||
threadId,
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user