mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
Wired up admin api for hidden comments (#21724)
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 - Added unit tests for mocking apiClient event listeners. - added eventlistener playwright tests to ensure it fires on UI clicks.
This commit is contained in:
parent
63c210199f
commit
781bfdd60f
12 changed files with 624 additions and 5 deletions
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@tryghost/comments-ui",
|
"name": "@tryghost/comments-ui",
|
||||||
"version": "0.22.4",
|
"version": "0.23.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "git@github.com:TryGhost/comments-ui.git",
|
"repository": "git@github.com:TryGhost/comments-ui.git",
|
||||||
"author": "Ghost Foundation",
|
"author": "Ghost Foundation",
|
||||||
|
|
|
@ -139,7 +139,13 @@ async function showComment({state, api, data: comment}: {state: EditableAppConte
|
||||||
}
|
}
|
||||||
// We need to refetch the comment, to make sure we have an up to date HTML content
|
// 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)
|
// + all relations are loaded as the current member (not the admin)
|
||||||
const data = await api.comments.read(comment.id);
|
let data;
|
||||||
|
if (state.admin && state.adminApi && state.labs.commentImprovements) {
|
||||||
|
data = await state.adminApi.read({commentId: comment.id});
|
||||||
|
} else {
|
||||||
|
data = await api.comments.read(comment.id);
|
||||||
|
}
|
||||||
|
|
||||||
const updatedComment = data.comments[0];
|
const updatedComment = data.comments[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
333
apps/comments-ui/src/utils/adminAPI.test.ts
Normal file
333
apps/comments-ui/src/utils/adminAPI.test.ts
Normal file
|
@ -0,0 +1,333 @@
|
||||||
|
import * as vi from 'vitest';
|
||||||
|
import {setupAdminAPI} from './adminApi';
|
||||||
|
|
||||||
|
describe('setupAdminAPI', () => {
|
||||||
|
let addEventListenerSpy: vi.SpyInstance;
|
||||||
|
let postMessageMock: vi.Mock;
|
||||||
|
let frame: HTMLIFrameElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
frame = document.createElement('iframe');
|
||||||
|
frame.dataset.frame = 'admin-auth';
|
||||||
|
Object.defineProperty(frame, 'contentWindow', {
|
||||||
|
value: {
|
||||||
|
postMessage: vi.vitest.fn()
|
||||||
|
},
|
||||||
|
writable: false
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(frame);
|
||||||
|
|
||||||
|
// Mock window.addEventListener - at runtime this gets injected into the theme.
|
||||||
|
// from here https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/frontend/src/admin-auth/message-handler.js
|
||||||
|
// In which case, we have to mock it in order to test it.
|
||||||
|
addEventListenerSpy = vi.vitest.spyOn(window, 'addEventListener');
|
||||||
|
postMessageMock = frame.contentWindow!.postMessage as vi.Mock;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore mocks and remove iframe
|
||||||
|
vi.vitest.restoreAllMocks();
|
||||||
|
frame.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can call getUser', async () => {
|
||||||
|
const adminUrl = 'https://example.com';
|
||||||
|
const api = setupAdminAPI({adminUrl});
|
||||||
|
|
||||||
|
const apiPromise = api.getUser();
|
||||||
|
|
||||||
|
const eventHandler = addEventListenerSpy.mock.calls.find(
|
||||||
|
([eventType]) => eventType === 'message'
|
||||||
|
)?.[1];
|
||||||
|
|
||||||
|
const mockEvent = new MessageEvent('message', {
|
||||||
|
origin: new URL(adminUrl).origin,
|
||||||
|
data: JSON.stringify({
|
||||||
|
uid: 2,
|
||||||
|
result: {
|
||||||
|
users: [{id: 1, name: 'Test User'}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
eventHandler!(mockEvent);
|
||||||
|
|
||||||
|
const user = await apiPromise;
|
||||||
|
|
||||||
|
expect(user).toEqual({id: 1, name: 'Test User'});
|
||||||
|
|
||||||
|
expect(postMessageMock).toHaveBeenCalledWith(
|
||||||
|
JSON.stringify({uid: 2, action: 'getUser'}),
|
||||||
|
new URL(adminUrl).origin
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can call hideComment', async () => {
|
||||||
|
const adminUrl = 'https://example.com';
|
||||||
|
const api = setupAdminAPI({adminUrl});
|
||||||
|
|
||||||
|
const apiPromise = api.hideComment('123');
|
||||||
|
|
||||||
|
const eventHandler = addEventListenerSpy.mock.calls.find(
|
||||||
|
([eventType]) => eventType === 'message'
|
||||||
|
)?.[1];
|
||||||
|
|
||||||
|
const mockEvent = new MessageEvent('message', {
|
||||||
|
origin: new URL(adminUrl).origin,
|
||||||
|
data: JSON.stringify({
|
||||||
|
uid: 2,
|
||||||
|
result: {success: true} // not the actual endpoint, we're just testing the event listener
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
eventHandler!(mockEvent);
|
||||||
|
|
||||||
|
const result = await apiPromise;
|
||||||
|
|
||||||
|
expect(result).toEqual({success: true});
|
||||||
|
|
||||||
|
expect(postMessageMock).toHaveBeenCalledWith(
|
||||||
|
JSON.stringify({uid: 2, action: 'hideComment', id: '123'}),
|
||||||
|
new URL(adminUrl).origin
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can call showComment', async () => {
|
||||||
|
const adminUrl = 'https://example.com';
|
||||||
|
const api = setupAdminAPI({adminUrl});
|
||||||
|
|
||||||
|
const apiPromise = api.showComment('123');
|
||||||
|
|
||||||
|
const eventHandler = addEventListenerSpy.mock.calls.find(
|
||||||
|
([eventType]) => eventType === 'message'
|
||||||
|
)?.[1];
|
||||||
|
|
||||||
|
const mockEvent = new MessageEvent('message', {
|
||||||
|
origin: new URL(adminUrl).origin,
|
||||||
|
data: JSON.stringify({
|
||||||
|
uid: 2,
|
||||||
|
result: {success: true} // not the actual data, we're just testing the event listener and functions execution
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
eventHandler!(mockEvent);
|
||||||
|
|
||||||
|
const result = await apiPromise;
|
||||||
|
|
||||||
|
expect(result).toEqual({success: true});
|
||||||
|
|
||||||
|
expect(postMessageMock).toHaveBeenCalledWith(
|
||||||
|
JSON.stringify({uid: 2, action: 'showComment', id: '123'}),
|
||||||
|
new URL(adminUrl).origin
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can call browse', async () => {
|
||||||
|
const adminUrl = 'https://example.com';
|
||||||
|
const api = setupAdminAPI({adminUrl});
|
||||||
|
|
||||||
|
const apiPromise = api.browse({page: 1, postId: '123', order: 'asc'});
|
||||||
|
|
||||||
|
const eventHandler = addEventListenerSpy.mock.calls.find(
|
||||||
|
([eventType]) => eventType === 'message'
|
||||||
|
)?.[1];
|
||||||
|
|
||||||
|
const mockEvent = new MessageEvent('message', {
|
||||||
|
origin: new URL(adminUrl).origin,
|
||||||
|
data: JSON.stringify({
|
||||||
|
uid: 2,
|
||||||
|
result: {
|
||||||
|
comments: [{id: 1, body: 'Test Comment'}],
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
limit: 15,
|
||||||
|
pages: 1,
|
||||||
|
total: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
eventHandler!(mockEvent);
|
||||||
|
|
||||||
|
const result = await apiPromise;
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
comments: [{id: 1, body: 'Test Comment'}],
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
limit: 15,
|
||||||
|
pages: 1,
|
||||||
|
total: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(postMessageMock).toHaveBeenCalledWith(
|
||||||
|
JSON.stringify({uid: 2, action: 'browseComments', postId: '123', params: 'limit=20&page=1&order=asc'}),
|
||||||
|
new URL(adminUrl).origin
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can call replies', async () => {
|
||||||
|
const adminUrl = 'https://example.com';
|
||||||
|
const api = setupAdminAPI({adminUrl});
|
||||||
|
|
||||||
|
const apiPromise = api.replies({commentId: '123', afterReplyId: '456', limit: 10});
|
||||||
|
|
||||||
|
const eventHandler = addEventListenerSpy.mock.calls.find(
|
||||||
|
([eventType]) => eventType === 'message'
|
||||||
|
)?.[1];
|
||||||
|
|
||||||
|
const mockEvent = new MessageEvent('message', {
|
||||||
|
origin: new URL(adminUrl).origin,
|
||||||
|
data: JSON.stringify({
|
||||||
|
uid: 2,
|
||||||
|
result: {
|
||||||
|
comments: [{id: 1, body: 'Test Reply'}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
eventHandler!(mockEvent);
|
||||||
|
|
||||||
|
const result = await apiPromise;
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
comments: [{id: 1, body: 'Test Reply'}]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(postMessageMock).toHaveBeenCalledWith(
|
||||||
|
JSON.stringify({
|
||||||
|
uid: 2,
|
||||||
|
action: 'getReplies',
|
||||||
|
commentId: '123',
|
||||||
|
params: 'limit=10&filter=id%3A%3E%27456%27'
|
||||||
|
}),
|
||||||
|
new URL(adminUrl).origin
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can call read', async () => {
|
||||||
|
const adminUrl = 'https://example.com';
|
||||||
|
const api = setupAdminAPI({adminUrl});
|
||||||
|
|
||||||
|
const apiPromise = api.read({commentId: '123'});
|
||||||
|
|
||||||
|
const eventHandler = addEventListenerSpy.mock.calls.find(
|
||||||
|
([eventType]) => eventType === 'message'
|
||||||
|
)?.[1];
|
||||||
|
|
||||||
|
const mockEvent = new MessageEvent('message', {
|
||||||
|
origin: new URL(adminUrl).origin,
|
||||||
|
data: JSON.stringify({
|
||||||
|
uid: 2,
|
||||||
|
result: {
|
||||||
|
comments: [{id: 1, body: 'Test Comment'}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
eventHandler!(mockEvent);
|
||||||
|
|
||||||
|
const result = await apiPromise;
|
||||||
|
|
||||||
|
expect(result).toEqual({comments: [{id: 1, body: 'Test Comment'}]});
|
||||||
|
|
||||||
|
expect(postMessageMock).toHaveBeenCalledWith(
|
||||||
|
JSON.stringify({uid: 2, action: 'readComment', commentId: '123'}),
|
||||||
|
new URL(adminUrl).origin
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call postMessage with the correct data on API call', async () => {
|
||||||
|
const adminUrl = 'https://example.com';
|
||||||
|
const api = setupAdminAPI({adminUrl});
|
||||||
|
|
||||||
|
// Simulate an API call
|
||||||
|
const apiPromise = api.getUser();
|
||||||
|
|
||||||
|
// Simulate a message event to resolve the promise
|
||||||
|
const eventHandler = addEventListenerSpy.mock.calls.find(
|
||||||
|
([eventType]) => eventType === 'message'
|
||||||
|
)?.[1];
|
||||||
|
|
||||||
|
const mockEvent = new MessageEvent('message', {
|
||||||
|
origin: new URL(adminUrl).origin,
|
||||||
|
data: JSON.stringify({
|
||||||
|
uid: 2, // Mock UID from the handler
|
||||||
|
result: {
|
||||||
|
users: [{id: 1, name: 'Test User'}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger the event handler manually
|
||||||
|
eventHandler!(mockEvent);
|
||||||
|
|
||||||
|
// Await the result
|
||||||
|
const user = await apiPromise;
|
||||||
|
|
||||||
|
expect(user).toEqual({id: 1, name: 'Test User'});
|
||||||
|
expect(postMessageMock).toHaveBeenCalledWith(
|
||||||
|
JSON.stringify({uid: 2, action: 'getUser'}),
|
||||||
|
new URL(adminUrl).origin
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject the promise if an error occurs', async () => {
|
||||||
|
const adminUrl = 'https://example.com';
|
||||||
|
const api = setupAdminAPI({adminUrl});
|
||||||
|
|
||||||
|
// Simulate an API call
|
||||||
|
const apiPromise = api.getUser();
|
||||||
|
|
||||||
|
// Simulate a message event with an error
|
||||||
|
const eventHandler = addEventListenerSpy.mock.calls.find(
|
||||||
|
([eventType]) => eventType === 'message'
|
||||||
|
)?.[1];
|
||||||
|
|
||||||
|
const mockEvent = new MessageEvent('message', {
|
||||||
|
origin: new URL(adminUrl).origin,
|
||||||
|
data: JSON.stringify({
|
||||||
|
uid: 2, // Mock UID from the handler
|
||||||
|
error: {message: 'Test Error'}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger the event handler manually
|
||||||
|
eventHandler!(mockEvent);
|
||||||
|
|
||||||
|
await expect(apiPromise).rejects.toEqual({message: 'Test Error'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore messages from an invalid origin', async () => {
|
||||||
|
const adminUrl = 'https://example.com';
|
||||||
|
const api = setupAdminAPI({adminUrl});
|
||||||
|
|
||||||
|
const apiPromise = api.getUser();
|
||||||
|
|
||||||
|
// Simulate a message event from an invalid origin
|
||||||
|
const eventHandler = addEventListenerSpy.mock.calls.find(
|
||||||
|
([eventType]) => eventType === 'message'
|
||||||
|
)?.[1];
|
||||||
|
|
||||||
|
const mockEvent = new MessageEvent('message', {
|
||||||
|
origin: 'https://invalid.com',
|
||||||
|
data: JSON.stringify({
|
||||||
|
uid: 2,
|
||||||
|
result: {users: [{id: 1, name: 'Invalid User'}]}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger the event handler manually
|
||||||
|
eventHandler!(mockEvent);
|
||||||
|
|
||||||
|
// Ensure the promise doesn't resolve
|
||||||
|
await expect(Promise.race([apiPromise, Promise.resolve('unresolved')])).resolves.toBe('unresolved');
|
||||||
|
});
|
||||||
|
});
|
|
@ -105,6 +105,11 @@ export function setupAdminAPI({adminUrl}: {adminUrl: string}) {
|
||||||
|
|
||||||
const response = await callApi('getReplies', {commentId, params: params.toString()});
|
const response = await callApi('getReplies', {commentId, params: params.toString()});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
async read({commentId}: {commentId: string}) {
|
||||||
|
const response = await callApi('readComment', {commentId});
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -240,5 +240,105 @@ test.describe('Auth Frame', async () => {
|
||||||
await moreButtons.nth(1).getByText('Show comment').click();
|
await moreButtons.nth(1).getByText('Show comment').click();
|
||||||
await expect(secondComment).toContainText('This is comment 2');
|
await expect(secondComment).toContainText('This is comment 2');
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
test('authFrameMain fires getUser (exposed function)', async ({page}) => {
|
||||||
|
const mockedApi = new MockedApi({});
|
||||||
|
mockedApi.addComment({
|
||||||
|
html: '<p>This is comment 1</p>'
|
||||||
|
});
|
||||||
|
mockedApi.addComment({
|
||||||
|
html: '<p>This is comment 2</p>'
|
||||||
|
});
|
||||||
|
mockedApi.addComment({
|
||||||
|
html: '<p>This is comment 3</p>'
|
||||||
|
});
|
||||||
|
mockedApi.addComment({
|
||||||
|
html: '<p>This is comment 4</p>'
|
||||||
|
});
|
||||||
|
mockedApi.addComment({
|
||||||
|
html: '<p>This is comment 5</p>'
|
||||||
|
});
|
||||||
|
|
||||||
|
const actions: string[] = [];
|
||||||
|
|
||||||
|
await page.exposeFunction('__testHelper', (action: string) => {
|
||||||
|
actions.push(action);
|
||||||
|
});
|
||||||
|
|
||||||
|
await mockAdminAuthFrame({
|
||||||
|
admin,
|
||||||
|
page
|
||||||
|
});
|
||||||
|
|
||||||
|
await initialize({
|
||||||
|
mockedApi,
|
||||||
|
page,
|
||||||
|
publication: 'Publisher Weekly',
|
||||||
|
admin,
|
||||||
|
labs: {
|
||||||
|
commentImprovements: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger the message event
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const event = new MessageEvent('message', {
|
||||||
|
data: JSON.stringify({uid: 'test', action: 'getUser'}),
|
||||||
|
origin: 'https://localhost:1234'
|
||||||
|
});
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate that "getUser" was captured
|
||||||
|
expect(actions).toContain('getUser');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fires admin read when making a hidden comment visible', async ({page}) => {
|
||||||
|
const mockedApi = new MockedApi({});
|
||||||
|
|
||||||
|
mockedApi.addComment({
|
||||||
|
html: '<p>This is comment 1</p>'
|
||||||
|
});
|
||||||
|
mockedApi.addComment({
|
||||||
|
html: '<p>This is comment 2</p>',
|
||||||
|
status: 'hidden'
|
||||||
|
});
|
||||||
|
|
||||||
|
const actions: string[] = [];
|
||||||
|
|
||||||
|
await page.exposeFunction('__testHelper', (action: string) => {
|
||||||
|
actions.push(action);
|
||||||
|
});
|
||||||
|
|
||||||
|
await mockAdminAuthFrame({
|
||||||
|
admin,
|
||||||
|
page
|
||||||
|
});
|
||||||
|
|
||||||
|
const {frame} = await initialize({
|
||||||
|
mockedApi,
|
||||||
|
page,
|
||||||
|
publication: 'Publisher Weekly',
|
||||||
|
admin,
|
||||||
|
labs: {
|
||||||
|
commentImprovements: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const iframeElement = await page.locator('iframe[data-frame="admin-auth"]');
|
||||||
|
await expect(iframeElement).toHaveCount(1);
|
||||||
|
|
||||||
|
// Check if more actions button is visible on each comment
|
||||||
|
const comments = await frame.getByTestId('comment-component');
|
||||||
|
await expect(comments).toHaveCount(2);
|
||||||
|
|
||||||
|
const moreButtons = await frame.getByTestId('more-button');
|
||||||
|
await expect(moreButtons).toHaveCount(2);
|
||||||
|
|
||||||
|
// Click the 2nd button
|
||||||
|
await moreButtons.nth(1).click();
|
||||||
|
await moreButtons.nth(1).getByText('Show comment').click();
|
||||||
|
|
||||||
|
await expect(actions).toContain('readComment');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {E2E_PORT} from '../../playwright.config';
|
import {E2E_PORT} from '../../playwright.config';
|
||||||
import {LabsType, MockedApi} from './MockedApi';
|
|
||||||
import {Locator, Page} from '@playwright/test';
|
import {Locator, Page} from '@playwright/test';
|
||||||
|
import {MockedApi} from './MockedApi';
|
||||||
import {expect} from '@playwright/test';
|
import {expect} from '@playwright/test';
|
||||||
|
|
||||||
export const MOCKED_SITE_URL = 'https://localhost:1234';
|
export const MOCKED_SITE_URL = 'https://localhost:1234';
|
||||||
|
@ -21,6 +21,12 @@ function escapeHtml(unsafe: string) {
|
||||||
.replace(/>/g, '>');
|
.replace(/>/g, '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__testHelper?: (action: string) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function authFrameMain() {
|
function authFrameMain() {
|
||||||
window.addEventListener('message', function (event) {
|
window.addEventListener('message', function (event) {
|
||||||
let d = null;
|
let d = null;
|
||||||
|
@ -44,6 +50,9 @@ function authFrameMain() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.action === 'getUser') {
|
if (data.action === 'getUser') {
|
||||||
|
if (window.__testHelper) {
|
||||||
|
window.__testHelper('getUser');
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
respond(null, {
|
respond(null, {
|
||||||
users: [
|
users: [
|
||||||
|
@ -58,6 +67,23 @@ function authFrameMain() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.action === 'readComment') {
|
||||||
|
if (window.__testHelper) {
|
||||||
|
window.__testHelper('readComment');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
respond(null, {
|
||||||
|
comment: {
|
||||||
|
id: 'comment-id',
|
||||||
|
html: '<p>This is a comment</p>'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
respond(err, null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Other actions: return empty object
|
// Other actions: return empty object
|
||||||
try {
|
try {
|
||||||
respond(null, {});
|
respond(null, {});
|
||||||
|
|
|
@ -49,6 +49,17 @@ 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') {
|
if (data.action === 'getUser') {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// This is a new endpoint for the admin API to return replies to a comment with pagination
|
// This is a new endpoint for the admin API to return replies to a comment with pagination
|
||||||
|
|
||||||
const commentsService = require('../../services/comments');
|
const commentsService = require('../../services/comments');
|
||||||
|
const ALLOWED_INCLUDES = ['member', 'replies', 'replies.member', 'replies.count.likes', 'replies.liked', 'count.replies', 'count.likes', 'liked', 'post', 'parent'];
|
||||||
|
|
||||||
/** @type {import('@tryghost/api-framework').Controller} */
|
/** @type {import('@tryghost/api-framework').Controller} */
|
||||||
const controller = {
|
const controller = {
|
||||||
|
@ -30,6 +31,28 @@ const controller = {
|
||||||
query(frame) {
|
query(frame) {
|
||||||
return commentsService.controller.adminReplies(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ const tpl = require('@tryghost/tpl');
|
||||||
const errors = require('@tryghost/errors');
|
const errors = require('@tryghost/errors');
|
||||||
const {MemberCommentEvent} = require('@tryghost/member-events');
|
const {MemberCommentEvent} = require('@tryghost/member-events');
|
||||||
const DomainEvents = require('@tryghost/domain-events');
|
const DomainEvents = require('@tryghost/domain-events');
|
||||||
|
const labs = require('../../../shared/labs');
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
commentNotFound: 'Comment could not be found',
|
commentNotFound: 'Comment could not be found',
|
||||||
|
@ -208,6 +209,28 @@ class CommentsService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (labs.isSet('commentImprovements')) {
|
||||||
|
const replies = model.related('replies'); // Get the loaded replies relation
|
||||||
|
await replies.fetch({withRelated: ['member', 'count.likes']}); // Fetch all replies
|
||||||
|
|
||||||
|
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 to handle pagination, so we can remove hidden/deleted replies here
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ module.exports = function apiRoutes() {
|
||||||
|
|
||||||
router.get('/mentions', mw.authAdminApi, http(api.mentions.browse));
|
router.get('/mentions', mw.authAdminApi, http(api.mentions.browse));
|
||||||
|
|
||||||
|
router.get('/comments/:id', mw.authAdminApi, http(api.commentReplies.read));
|
||||||
router.get('/comments/:id/replies', mw.authAdminApi, http(api.commentReplies.browse));
|
router.get('/comments/:id/replies', mw.authAdminApi, http(api.commentReplies.browse));
|
||||||
router.get('/comments/post/:post_id', mw.authAdminApi, http(api.comments.browse));
|
router.get('/comments/post/:post_id', mw.authAdminApi, http(api.comments.browse));
|
||||||
router.put('/comments/:id', mw.authAdminApi, http(api.comments.edit));
|
router.put('/comments/:id', mw.authAdminApi, http(api.comments.edit));
|
||||||
|
|
|
@ -210,7 +210,7 @@
|
||||||
},
|
},
|
||||||
"comments": {
|
"comments": {
|
||||||
"url": "https://cdn.jsdelivr.net/ghost/comments-ui@~{version}/umd/comments-ui.min.js",
|
"url": "https://cdn.jsdelivr.net/ghost/comments-ui@~{version}/umd/comments-ui.min.js",
|
||||||
"version": "0.22"
|
"version": "0.23"
|
||||||
},
|
},
|
||||||
"signupForm": {
|
"signupForm": {
|
||||||
"url": "https://cdn.jsdelivr.net/ghost/signup-form@~{version}/umd/signup-form.min.js",
|
"url": "https://cdn.jsdelivr.net/ghost/signup-form@~{version}/umd/signup-form.min.js",
|
||||||
|
|
|
@ -458,6 +458,97 @@ describe('Admin Comments API', function () {
|
||||||
assert.equal(res2.body.comments[0].html, 'Reply 4');
|
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);
|
||||||
|
|
||||||
|
res.body.comments[0].replies[0].member.should.be.an.Object().with.properties('id', 'uuid', 'name', 'avatar_image');
|
||||||
|
|
||||||
|
res.body.comments[0].replies[0].should.be.an.Object().with.properties('id', 'html', 'status', 'created_at', 'member', 'count');
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
res.body.comments[0].replies[0].member.should.be.an.Object().with.properties('id', 'uuid', 'name', 'avatar_image');
|
||||||
|
res.body.comments[0].replies[0].should.be.an.Object().with.properties('id', 'html', 'status', 'created_at', 'member', 'count');
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
res.body.comments[0].replies[0].member.should.be.an.Object().with.properties('id', 'uuid', 'name', 'avatar_image');
|
||||||
|
res.body.comments[0].replies[0].should.be.an.Object().with.properties('id', 'html', 'status', 'created_at', 'member', 'count');
|
||||||
|
});
|
||||||
|
|
||||||
it('ensure replies are always ordered from oldest to newest', async function () {
|
it('ensure replies are always ordered from oldest to newest', async function () {
|
||||||
const post = fixtureManager.get('posts', 1);
|
const post = fixtureManager.get('posts', 1);
|
||||||
const {parent} = await dbFns.addCommentWithReplies({
|
const {parent} = await dbFns.addCommentWithReplies({
|
||||||
|
|
Loading…
Add table
Reference in a new issue