0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Fixed hidden/deleted replies in getCommentByID (#21707)

ref PLG-270

- Updated the getCommentByID service to filter out hidden and deleted
replies.
- Ensured all replies are loaded before applying the filter.
- Simplified logic to handle non-paginated routes by directly removing
unwanted replies.
- Wired up new Admin Endpoint that shows hidden replies but not deleted
replies.
- Updated comments-ui client.
This commit is contained in:
Ronald Langeveld 2024-11-25 16:15:08 +08:00 committed by GitHub
parent 7cfb755bbd
commit 31a80cf9b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 200 additions and 4 deletions

View file

@ -100,7 +100,7 @@ async function addReply({state, api, data: {reply, parent}}: {state: EditableApp
}
async function hideComment({state, data: comment}: {state: EditableAppContext, adminApi: any, data: {id: string}}) {
if (state.adminApi) {
if (state.admin && state.adminApi && state.labs.commentImprovements) {
await state.adminApi.hideComment(comment.id);
}
return {
@ -134,8 +134,8 @@ async function hideComment({state, data: comment}: {state: EditableAppContext, a
}
async function showComment({state, api, data: comment}: {state: EditableAppContext, api: GhostApi, adminApi: any, data: {id: string}}) {
if (state.adminApi) {
await state.adminApi.showComment(comment.id);
if (state.admin && state.adminApi && state.labs.commentImprovements) {
await state.adminApi.read({commentId: comment.id});
}
// We need to refetch the comment, to make sure we have an up to date HTML content
// + all relations are loaded as the current member (not the admin)

View file

@ -105,6 +105,11 @@ export function setupAdminAPI({adminUrl}: {adminUrl: string}) {
const response = await callApi('getReplies', {commentId, params: params.toString()});
return response;
},
async read({commentId}: {commentId: string}) {
const response = await callApi('readComment', {commentId});
return response;
}
};

View file

@ -49,6 +49,19 @@ window.addEventListener('message', async function (event) {
}
}
if (data.action === 'readComment') {
try {
const {commentId} = data;
const res = await fetch(
adminUrl + `/comments/${commentId}/}`
);
const json = await res.json();
respond(null, json);
} catch (err) {
respond(err, null);
}
}
if (data.action === 'getUser') {
try {
const res = await fetch(

View file

@ -1,4 +1,5 @@
// This is a new endpoint for the admin API to return replies to a comment with pagination
const ALLOWED_INCLUDES = ['member', 'replies', 'replies.member', 'replies.count.likes', 'replies.liked', 'count.replies', 'count.likes', 'liked', 'post', 'parent'];
const commentsService = require('../../services/comments');
@ -30,6 +31,27 @@ const controller = {
query(frame) {
return commentsService.controller.adminReplies(frame);
}
}, read: {
headers: {
cacheInvalidate: false
},
options: [
'include'
],
data: [
'id',
'email'
],
validation: {
options: {
include: ALLOWED_INCLUDES
}
},
permissions: true,
query(frame) {
frame.options.isAdmin = true;
return commentsService.controller.read(frame);
}
}
};

View file

@ -2,6 +2,7 @@ const tpl = require('@tryghost/tpl');
const errors = require('@tryghost/errors');
const {MemberCommentEvent} = require('@tryghost/member-events');
const DomainEvents = require('@tryghost/domain-events');
const labs = require('../../../shared/labs');
const messages = {
commentNotFound: 'Comment could not be found',
@ -208,6 +209,29 @@ class CommentsService {
});
}
if (labs.isSet('commentImprovements')) {
const replies = model.related('replies'); // Get the loaded replies relation
await replies.fetch(); // Ensure all replies are loaded
if (replies && replies.length > 0) {
// Filter out deleted replies for all, and hidden replies for non-admins
replies.remove(
replies.filter((reply) => {
const status = reply.get('status');
if (status === 'deleted') {
return true;
} // Always remove deleted replies
if (!options.isAdmin && status === 'hidden') {
return true;
} // Remove hidden replies for non-admins
return false; // Keep others
})
);
}
}
// this route does not need need to handle pagination, so we can remove hidden/deleted replies here
return model;
}

View file

@ -54,6 +54,7 @@ module.exports = function apiRoutes() {
router.get('/comments/:id/replies', mw.authAdminApi, http(api.commentReplies.browse));
router.get('/comments/post/:post_id', mw.authAdminApi, http(api.comments.browse));
router.put('/comments/:id', mw.authAdminApi, http(api.comments.edit));
router.get('/comments/:id', mw.authAdminApi, http(api.commentReplies.read));
// ## Pages
router.get('/pages', mw.authAdminApi, http(api.pages.browse));

View file

@ -458,6 +458,89 @@ describe('Admin Comments API', function () {
assert.equal(res2.body.comments[0].html, 'Reply 4');
});
it('Does not return deleted replies', async function () {
const post = fixtureManager.get('posts', 1);
await mockManager.mockLabsEnabled('commentImprovements');
const {parent} = await dbFns.addCommentWithReplies({
post_id: post.id,
member_id: fixtureManager.get('members', 0).id,
replies: [{
member_id: fixtureManager.get('members', 1).id,
status: 'hidden'
}, {
member_id: fixtureManager.get('members', 2).id,
status: 'deleted'
},
{
member_id: fixtureManager.get('members', 3).id,
status: 'hidden'
},
{
member_id: fixtureManager.get('members', 4).id,
status: 'published'
}
]
});
const res = await adminApi.get(`/comments/${parent.get('id')}/`);
res.body.comments[0].replies.length.should.eql(3);
});
it('Does return published replies', async function () {
const post = fixtureManager.get('posts', 1);
await mockManager.mockLabsEnabled('commentImprovements');
const {parent} = await dbFns.addCommentWithReplies({
post_id: post.id,
member_id: fixtureManager.get('members', 0).id,
replies: [{
member_id: fixtureManager.get('members', 1).id,
status: 'published'
}, {
member_id: fixtureManager.get('members', 2).id,
status: 'published'
},
{
member_id: fixtureManager.get('members', 3).id,
status: 'published'
}
]
});
const res = await adminApi.get(`/comments/${parent.get('id')}/`);
res.body.comments[0].replies.length.should.eql(3);
});
it('Does return published and hidden replies but not deleted', async function () {
const post = fixtureManager.get('posts', 1);
await mockManager.mockLabsEnabled('commentImprovements');
const {parent} = await dbFns.addCommentWithReplies({
post_id: post.id,
member_id: fixtureManager.get('members', 0).id,
replies: [{
member_id: fixtureManager.get('members', 1).id,
status: 'published'
}, {
member_id: fixtureManager.get('members', 2).id,
status: 'published'
},
{
member_id: fixtureManager.get('members', 3).id,
status: 'published'
},
{
member_id: fixtureManager.get('members', 4).id,
status: 'hidden'
},
{
member_id: fixtureManager.get('members', 5).id,
status: 'deleted'
}
]
});
const res = await adminApi.get(`/comments/${parent.get('id')}/`);
res.body.comments[0].replies.length.should.eql(4);
});
it('ensure replies are always ordered from oldest to newest', async function () {
const post = fixtureManager.get('posts', 1);
const {parent} = await dbFns.addCommentWithReplies({

View file

@ -447,7 +447,7 @@ describe('Comments API', function () {
});
});
it('shows hidden and deleted comment where there is a reply', async function () {
it('shows hidden and deleted parent comment where there is a reply', async function () {
await mockManager.mockLabsEnabled('commentImprovements');
await setupBrowseCommentsData();
const hiddenComment = await dbFns.addComment({
@ -504,6 +504,54 @@ describe('Comments API', function () {
});
});
it('Does not return deleted or hidden replies', async function () {
await mockManager.mockLabsEnabled('commentImprovements');
const {parent} = await dbFns.addCommentWithReplies({
member_id: fixtureManager.get('members', 0).id,
replies: [{
member_id: fixtureManager.get('members', 1).id,
status: 'hidden'
}, {
member_id: fixtureManager.get('members', 2).id,
status: 'deleted'
},
{
member_id: fixtureManager.get('members', 3).id,
status: 'hidden'
},
{
member_id: fixtureManager.get('members', 4).id,
status: 'published'
}
]
});
const res = await membersAgent.get(`/api/comments/${parent.get('id')}/`);
res.body.comments[0].replies.length.should.eql(1);
});
it('Does return published replies', async function () {
await mockManager.mockLabsEnabled('commentImprovements');
const {parent} = await dbFns.addCommentWithReplies({
member_id: fixtureManager.get('members', 0).id,
replies: [{
member_id: fixtureManager.get('members', 1).id,
status: 'published'
}, {
member_id: fixtureManager.get('members', 2).id,
status: 'published'
},
{
member_id: fixtureManager.get('members', 3).id,
status: 'published'
}
]
});
const res = await membersAgent.get(`/api/comments/${parent.get('id')}/`);
res.body.comments[0].replies.length.should.eql(3);
});
it('Returns nothing if both parent and reply are hidden', async function () {
await mockManager.mockLabsEnabled('commentImprovements');
const hiddenComment = await dbFns.addComment({