mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Fixed incorrect replies pagination count after deleting reply
closes https://linear.app/ghost/issue/PLG-303 - when deleting a reply our "replies left" calculation was getting out of sync because the `count.replies` state on the parent comment wasn't being updated, the result was for each comment deleted we were displaying 1 more reply that was still to load - updated the `deleteComment` action to also modify the parent comment's `count.replies` value when a reply was deleted ensuring our "replies left" calculation remains correct
This commit is contained in:
parent
bd9f6bb216
commit
46f6f49c03
3 changed files with 53 additions and 36 deletions
|
@ -280,13 +280,13 @@ async function deleteComment({state, api, data: comment}: {state: EditableAppCon
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
comments: state.comments.map((c) => {
|
comments: state.comments.map((topLevelComment) => {
|
||||||
// If the comment has replies we want to keep it so the replies are
|
// If the comment has replies we want to keep it so the replies are
|
||||||
// still visible, but mark the comment as deleted. Otherwise remove it.
|
// still visible, but mark the comment as deleted. Otherwise remove it.
|
||||||
if (c.id === comment.id) {
|
if (topLevelComment.id === comment.id) {
|
||||||
if (c.replies.length > 0) {
|
if (topLevelComment.replies.length > 0) {
|
||||||
return {
|
return {
|
||||||
...c,
|
...topLevelComment,
|
||||||
status: 'deleted'
|
status: 'deleted'
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
@ -294,11 +294,22 @@ async function deleteComment({state, api, data: comment}: {state: EditableAppCon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedReplies = c.replies.filter(r => r.id !== comment.id);
|
const originalLength = topLevelComment.replies.length;
|
||||||
return {
|
const updatedReplies = topLevelComment.replies.filter(reply => reply.id !== comment.id);
|
||||||
...c,
|
const hasDeletedReply = originalLength !== updatedReplies.length;
|
||||||
|
|
||||||
|
const updatedTopLevelComment = {
|
||||||
|
...topLevelComment,
|
||||||
replies: updatedReplies
|
replies: updatedReplies
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// When a reply is deleted we need to update the parent's count so
|
||||||
|
// pagination displays the correct number of replies still to load
|
||||||
|
if (hasDeletedReply && topLevelComment.count?.replies) {
|
||||||
|
topLevelComment.count.replies = topLevelComment.count.replies - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedTopLevelComment;
|
||||||
}).filter(Boolean),
|
}).filter(Boolean),
|
||||||
commentCount: state.commentCount - 1
|
commentCount: state.commentCount - 1
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,7 +12,7 @@ const RepliesPagination: React.FC<Props> = ({loadMore, count}) => {
|
||||||
const shortText = t('{{amount}} more', {amount: formatNumber(count)});
|
const shortText = t('{{amount}} more', {amount: formatNumber(count)});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center justify-start">
|
<div className="flex w-full items-center justify-start" data-testid="replies-pagination">
|
||||||
<button className="text-md group mb-10 ml-[48px] flex w-auto items-center px-0 pb-2 pt-0 text-left font-sans font-semibold text-neutral-700 sm:mb-12 dark:text-white " data-testid="reply-pagination-button" type="button" onClick={loadMore}>
|
<button className="text-md group mb-10 ml-[48px] flex w-auto items-center px-0 pb-2 pt-0 text-left font-sans font-semibold text-neutral-700 sm:mb-12 dark:text-white " data-testid="reply-pagination-button" type="button" onClick={loadMore}>
|
||||||
<span className="flex h-[40px] w-auto items-center justify-center whitespace-nowrap rounded-[6px] bg-black/5 px-4 py-2 text-center font-sans text-sm font-semibold text-neutral-700 outline-0 transition-all duration-150 hover:bg-black/10 dark:bg-white/15 dark:text-neutral-300 dark:hover:bg-white/20 dark:hover:text-neutral-100">↓ <span className="ml-1 hidden sm:inline">{longText}</span><span className="ml-1 inline sm:hidden">{shortText}</span> </span>
|
<span className="flex h-[40px] w-auto items-center justify-center whitespace-nowrap rounded-[6px] bg-black/5 px-4 py-2 text-center font-sans text-sm font-semibold text-neutral-700 outline-0 transition-all duration-150 hover:bg-black/10 dark:bg-white/15 dark:text-neutral-300 dark:hover:bg-white/20 dark:hover:text-neutral-100">↓ <span className="ml-1 hidden sm:inline">{longText}</span><span className="ml-1 inline sm:hidden">{shortText}</span> </span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -341,6 +341,13 @@ test.describe('Actions', async () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function deleteComment(page, frame, commentComponent) {
|
||||||
|
await commentComponent.getByTestId('more-button').first().click();
|
||||||
|
await frame.getByTestId('delete').click();
|
||||||
|
const popupIframe = page.frameLocator('iframe[title="deletePopup"]');
|
||||||
|
await popupIframe.getByTestId('delete-popup-confirm').click();
|
||||||
|
}
|
||||||
|
|
||||||
test('Can delete a comment', async ({page}) => {
|
test('Can delete a comment', async ({page}) => {
|
||||||
const loggedInMember = buildMember();
|
const loggedInMember = buildMember();
|
||||||
mockedApi.setMember(loggedInMember);
|
mockedApi.setMember(loggedInMember);
|
||||||
|
@ -352,15 +359,8 @@ test.describe('Actions', async () => {
|
||||||
|
|
||||||
const {frame} = await initializeTest(page);
|
const {frame} = await initializeTest(page);
|
||||||
|
|
||||||
const comment = frame.getByTestId('comment-component').nth(0);
|
const commentToDelete = frame.getByTestId('comment-component').nth(0);
|
||||||
const moreButton = comment.getByTestId('more-button').first();
|
await deleteComment(page, frame, commentToDelete);
|
||||||
await moreButton.click();
|
|
||||||
await frame.getByTestId('delete').click();
|
|
||||||
|
|
||||||
const popupIframe = page.frameLocator('iframe[title="deletePopup"]');
|
|
||||||
|
|
||||||
await expect(popupIframe.getByTestId('delete-popup')).toBeVisible();
|
|
||||||
await popupIframe.getByTestId('delete-popup-confirm').click();
|
|
||||||
|
|
||||||
await expect(frame.getByTestId('comment-component')).toHaveCount(0);
|
await expect(frame.getByTestId('comment-component')).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
@ -382,20 +382,33 @@ test.describe('Actions', async () => {
|
||||||
const {frame} = await initializeTest(page);
|
const {frame} = await initializeTest(page);
|
||||||
|
|
||||||
const comment = frame.getByTestId('comment-component').nth(0);
|
const comment = frame.getByTestId('comment-component').nth(0);
|
||||||
const reply = comment.getByTestId('comment-component').nth(0);
|
const replyToDelete = comment.getByTestId('comment-component').nth(0);
|
||||||
const moreButton = reply.getByTestId('more-button').first();
|
await deleteComment(page, frame, replyToDelete);
|
||||||
await moreButton.click();
|
|
||||||
await frame.getByTestId('delete').click();
|
|
||||||
|
|
||||||
const popupIframe = page.frameLocator('iframe[title="deletePopup"]');
|
|
||||||
|
|
||||||
await expect(popupIframe.getByTestId('delete-popup')).toBeVisible();
|
|
||||||
await popupIframe.getByTestId('delete-popup-confirm').click();
|
|
||||||
|
|
||||||
await expect(frame.getByTestId('comment-component')).toHaveCount(1);
|
await expect(frame.getByTestId('comment-component')).toHaveCount(1);
|
||||||
await expect(frame.getByTestId('replies-line')).not.toBeVisible();
|
await expect(frame.getByTestId('replies-line')).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Deleting a reply updates pagination', async ({page}) => {
|
||||||
|
const loggedInMember = buildMember();
|
||||||
|
mockedApi.setMember(loggedInMember);
|
||||||
|
|
||||||
|
mockedApi.addComment({
|
||||||
|
html: '<p>Parent comment</p>',
|
||||||
|
// 6 replies
|
||||||
|
replies: Array.from({length: 6}, (_, i) => buildReply({member: loggedInMember, html: `<p>Reply ${i + 1}</p>`}))
|
||||||
|
});
|
||||||
|
|
||||||
|
const {frame} = await initializeTest(page);
|
||||||
|
await expect(frame.getByTestId('replies-pagination')).toContainText('3');
|
||||||
|
|
||||||
|
const replyToDelete = frame.getByTestId('comment-component').nth(2);
|
||||||
|
await deleteComment(page, frame, replyToDelete);
|
||||||
|
|
||||||
|
// Replies count does not change - we still have 3 unloaded replies
|
||||||
|
await expect(frame.getByTestId('replies-pagination')).toContainText('3');
|
||||||
|
});
|
||||||
|
|
||||||
test('Can delete a comment with replies', async ({page}) => {
|
test('Can delete a comment with replies', async ({page}) => {
|
||||||
const loggedInMember = buildMember();
|
const loggedInMember = buildMember();
|
||||||
mockedApi.setMember(loggedInMember);
|
mockedApi.setMember(loggedInMember);
|
||||||
|
@ -412,22 +425,15 @@ test.describe('Actions', async () => {
|
||||||
|
|
||||||
const {frame} = await initializeTest(page);
|
const {frame} = await initializeTest(page);
|
||||||
|
|
||||||
const comment = frame.getByTestId('comment-component').nth(0);
|
const commentToDelete = frame.getByTestId('comment-component').nth(0);
|
||||||
const moreButton = comment.getByTestId('more-button').first();
|
await deleteComment(page, frame, commentToDelete);
|
||||||
await moreButton.click();
|
|
||||||
await frame.getByTestId('delete').click();
|
|
||||||
|
|
||||||
const popupIframe = page.frameLocator('iframe[title="deletePopup"]');
|
|
||||||
|
|
||||||
await expect(popupIframe.getByTestId('delete-popup')).toBeVisible();
|
|
||||||
await popupIframe.getByTestId('delete-popup-confirm').click();
|
|
||||||
|
|
||||||
await expect(frame.getByTestId('comment-component')).toHaveCount(2);
|
await expect(frame.getByTestId('comment-component')).toHaveCount(2);
|
||||||
await expect(frame.getByText('This comment has been removed')).toBeVisible();
|
await expect(frame.getByText('This comment has been removed')).toBeVisible();
|
||||||
await expect(frame.getByTestId('replies-line')).toBeVisible();
|
await expect(frame.getByTestId('replies-line')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Sorting - flag needs to be enabled', () => {
|
test.describe('Sorting', () => {
|
||||||
test('Renders Sorting Form dropdown', async ({page}) => {
|
test('Renders Sorting Form dropdown', async ({page}) => {
|
||||||
mockedApi.addComment({
|
mockedApi.addComment({
|
||||||
html: '<p>This is comment 1</p>'
|
html: '<p>This is comment 1</p>'
|
||||||
|
|
Loading…
Add table
Reference in a new issue