0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Fixed replies line showing after all replies have been deleted

ref https://linear.app/ghost/issue/PLG-267

- updated delete comment action so it removes comments rather than just updating their status to `'deleted'`
- deleted comments that still have replies have their status updated so the replies remain visible
- matches updated API behaviour where deleted comments are not shown at all
This commit is contained in:
Kevin Ansfield 2024-12-10 22:12:32 +00:00
parent bd20ad3adb
commit e86d44ff85
5 changed files with 139 additions and 26 deletions

View file

@ -279,34 +279,60 @@ async function deleteComment({state, api, data: comment}: {state: EditableAppCon
}
});
return {
comments: state.comments.map((c) => {
const replies = c.replies.map((r) => {
if (r.id === comment.id) {
if (state.labs.commentImprovements) {
return {
comments: state.comments.map((c) => {
// 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.
if (c.id === comment.id) {
if (c.replies.length > 0) {
return {
...c,
status: 'deleted'
};
} else {
return null; // Will be filtered out later
}
}
const updatedReplies = c.replies.filter(r => r.id !== comment.id);
return {
...c,
replies: updatedReplies
};
}).filter(Boolean),
commentCount: state.commentCount - 1
};
} else {
return {
comments: state.comments.map((c) => {
const replies = c.replies.map((r) => {
if (r.id === comment.id) {
return {
...r,
status: 'deleted'
};
}
return r;
});
if (c.id === comment.id) {
return {
...r,
status: 'deleted'
...c,
status: 'deleted',
replies
};
}
return r;
});
if (c.id === comment.id) {
return {
...c,
status: 'deleted',
replies
};
}
return {
...c,
replies
};
}),
commentCount: state.commentCount - 1
};
}),
commentCount: state.commentCount - 1
};
}
}
async function editComment({state, api, data: {comment, parent}}: {state: EditableAppContext, api: GhostApi, data: {comment: Partial<Comment> & {id: string}, parent?: Comment}}) {

View file

@ -439,7 +439,7 @@ const RepliesLine: React.FC<{hasReplies: boolean}> = ({hasReplies}) => {
return null;
}
return (<div className="mb-2 h-full w-px grow rounded bg-gradient-to-b from-neutral-900/10 via-neutral-900/10 to-transparent dark:from-white/10 dark:via-white/10" />);
return (<div className="mb-2 h-full w-px grow rounded bg-gradient-to-b from-neutral-900/10 via-neutral-900/10 to-transparent dark:from-white/10 dark:via-white/10" data-testid="replies-line" />);
};
type CommentLayoutProps = {

View file

@ -27,7 +27,7 @@ const AuthorContextMenu: React.FC<Props> = ({comment, close, toggleEdit}) => {
<button className="w-full rounded px-2.5 py-1.5 text-left text-[14px] transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-700" data-testid="edit" type="button" onClick={toggleEdit}>
{t('Edit')}
</button>
<button className="w-full rounded px-2.5 py-1.5 text-left text-[14px] text-red-600 transition-colors hover:bg-neutral-100 dark:text-red-500 dark:hover:bg-neutral-700" type="button" onClick={deleteComment}>
<button className="w-full rounded px-2.5 py-1.5 text-left text-[14px] text-red-600 transition-colors hover:bg-neutral-100 dark:text-red-500 dark:hover:bg-neutral-700" data-testid="delete" type="button" onClick={deleteComment}>
{t('Delete')}
</button>
</div>

View file

@ -64,7 +64,7 @@ const DeletePopup = ({comment}: {comment: Comment}) => {
};
return (
<div className="shadow-modal relative h-screen w-screen rounded-none bg-white p-[28px] text-center sm:h-auto sm:w-[500px] sm:rounded-xl sm:p-8 sm:text-left" onMouseDown={stopPropagation}>
<div className="shadow-modal relative h-screen w-screen rounded-none bg-white p-[28px] text-center sm:h-auto sm:w-[500px] sm:rounded-xl sm:p-8 sm:text-left" data-testid="delete-popup" onMouseDown={stopPropagation}>
<div className="flex h-full flex-col justify-center pt-10 sm:justify-normal sm:pt-0">
<h1 className="mb-1.5 font-sans text-[2.2rem] font-bold tracking-tight text-black">
<span>{t('Are you sure?')}</span>
@ -73,6 +73,7 @@ const DeletePopup = ({comment}: {comment: Comment}) => {
<div className="mt-auto flex flex-col items-center justify-start gap-4 sm:mt-8 sm:flex-row">
<button
className={`text-md flex h-[44px] w-full items-center justify-center rounded-md px-4 font-sans font-medium text-white transition duration-200 ease-linear sm:w-fit ${buttonColor} opacity-100 hover:opacity-90`}
data-testid="delete-popup-confirm"
disabled={isSubmitting}
type="button"
onClick={submit}
@ -92,4 +93,4 @@ const DeletePopup = ({comment}: {comment: Comment}) => {
);
};
export default DeletePopup;
export default DeletePopup;

View file

@ -361,6 +361,92 @@ test.describe('Actions', async () => {
);
});
test('Can delete a comment', async ({page}) => {
const loggedInMember = buildMember();
mockedApi.setMember(loggedInMember);
mockedApi.addComment({
html: '<p>This is comment 1</p>',
member: loggedInMember
});
const {frame} = await initializeTest(page, {labs: true});
const comment = frame.getByTestId('comment-component').nth(0);
const moreButton = comment.getByTestId('more-button').first();
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);
});
test('Can delete a reply', async ({page}) => {
const loggedInMember = buildMember();
mockedApi.setMember(loggedInMember);
mockedApi.addComment({
html: '<p>This is comment 1</p>',
replies: [
mockedApi.buildReply({
html: '<p>This is a reply</p>',
member: loggedInMember
})
]
});
const {frame} = await initializeTest(page, {labs: true});
const comment = frame.getByTestId('comment-component').nth(0);
const reply = comment.getByTestId('comment-component').nth(0);
const moreButton = reply.getByTestId('more-button').first();
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('replies-line')).not.toBeVisible();
});
test('Can delete a comment with replies', async ({page}) => {
const loggedInMember = buildMember();
mockedApi.setMember(loggedInMember);
mockedApi.addComment({
html: '<p>This is comment 1</p>',
member: loggedInMember,
replies: [
mockedApi.buildReply({
html: '<p>This is a reply</p>'
})
]
});
const {frame} = await initializeTest(page, {labs: true});
const comment = frame.getByTestId('comment-component').nth(0);
const moreButton = comment.getByTestId('more-button').first();
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.getByText('This comment has been removed')).toBeVisible();
await expect(frame.getByTestId('replies-line')).toBeVisible();
});
test.describe('Sorting - flag needs to be enabled', () => {
test('Renders Sorting Form dropdown', async ({page}) => {
mockedApi.addComment({
@ -620,7 +706,7 @@ test.describe('Actions', async () => {
// Get the parent comment and verify initial state
const parentComment = frame.getByTestId('comment-component').nth(0);
const replies = await parentComment.getByTestId('comment-component').all();
// Verify initial state shows parent and replies
await expect(parentComment).toContainText('Parent comment');
await expect(replies[0]).toBeVisible();
@ -635,7 +721,7 @@ test.describe('Actions', async () => {
// Verify the edit form is visible
await expect(frame.getByTestId('form-editor')).toBeVisible();
// Verify replies are still visible while editing
await expect(replies[0]).toBeVisible();
await expect(replies[0]).toContainText('First reply');