0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-24 23:48:13 -05:00
ghost/apps/comments-ui/src/actions.ts
Simon Backx f1b51729fc
Converted Comments-UI App to TypeScript and React hooks (#17760)
refs https://github.com/TryGhost/Product/issues/3504

- App component now uses React hooks intead of React class component
- App is now written in TypeScript
- All JavaScript is now removed from the Comments-UI project
- Removed `PopupNotification` because these were never displayed
- Removed `action` from AppContext (never used)
- Moved options parsing out of `index.ts` into a separate utility file,
similar to the signup-form
- Improved reliability of some editor tests by always waiting for the
editor to be focused (was not always the case) + added an utility method
for this
2023-08-18 13:30:59 +00:00

401 lines
12 KiB
TypeScript

import {AddComment, Comment, CommentsOptions, EditableAppContext} from './AppContext';
import {AdminApi} from './utils/adminApi';
import {GhostApi} from './utils/api';
import {Page} from './pages';
async function loadMoreComments({state, api, options}: {state: EditableAppContext, api: GhostApi, options: CommentsOptions}): Promise<Partial<EditableAppContext>> {
let page = 1;
if (state.pagination && state.pagination.page) {
page = state.pagination.page + 1;
}
const data = await api.comments.browse({page, postId: options.postId});
// Note: we store the comments from new to old, and show them in reverse order
return {
comments: [...state.comments, ...data.comments],
pagination: data.meta.pagination
};
}
async function loadMoreReplies({state, api, data: {comment, limit}}: {state: EditableAppContext, api: GhostApi, data: {comment: any, limit?: number | 'all'}}): Promise<Partial<EditableAppContext>> {
const data = await api.comments.replies({commentId: comment.id, afterReplyId: comment.replies[comment.replies.length - 1]?.id, limit});
// Note: we store the comments from new to old, and show them in reverse order
return {
comments: state.comments.map((c) => {
if (c.id === comment.id) {
return {
...comment,
replies: [...comment.replies, ...data.comments]
};
}
return c;
})
};
}
async function addComment({state, api, data: comment}: {state: EditableAppContext, api: GhostApi, data: AddComment}) {
const data = await api.comments.add({comment});
comment = data.comments[0];
return {
comments: [comment, ...state.comments],
commentCount: state.commentCount + 1
};
}
async function addReply({state, api, data: {reply, parent}}: {state: EditableAppContext, api: GhostApi, data: {reply: any, parent: any}}) {
let comment = reply;
comment.parent_id = parent.id;
const data = await api.comments.add({comment});
comment = data.comments[0];
// When we add a reply,
// it is possible that we didn't load all the replies for the given comment yet.
// To fix that, we'll save the reply to a different field that is created locally to differentiate between replies before and after pagination 😅
// Replace the comment in the state with the new one
return {
comments: state.comments.map((c) => {
if (c.id === parent.id) {
return {
...parent,
replies: [...parent.replies, comment],
count: {
...parent.count,
replies: parent.count.replies + 1
}
};
}
return c;
}),
commentCount: state.commentCount + 1
};
}
async function hideComment({state, adminApi, data: comment}: {state: EditableAppContext, adminApi: any, data: {id: string}}) {
await adminApi.hideComment(comment.id);
return {
comments: state.comments.map((c) => {
const replies = c.replies.map((r) => {
if (r.id === comment.id) {
return {
...r,
status: 'hidden'
};
}
return r;
});
if (c.id === comment.id) {
return {
...c,
status: 'hidden',
replies
};
}
return {
...c,
replies
};
}),
commentCount: state.commentCount - 1
};
}
async function showComment({state, api, adminApi, data: comment}: {state: EditableAppContext, api: GhostApi, adminApi: any, data: {id: string}}) {
await adminApi.showComment(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)
const data = await api.comments.read(comment.id);
const updatedComment = data.comments[0];
return {
comments: state.comments.map((c) => {
const replies = c.replies.map((r) => {
if (r.id === comment.id) {
return updatedComment;
}
return r;
});
if (c.id === comment.id) {
return updatedComment;
}
return {
...c,
replies
};
}),
commentCount: state.commentCount + 1
};
}
async function likeComment({state, api, data: comment}: {state: EditableAppContext, api: GhostApi, data: {id: string}}) {
await api.comments.like({comment});
return {
comments: state.comments.map((c) => {
const replies = c.replies.map((r) => {
if (r.id === comment.id) {
return {
...r,
liked: true,
count: {
...r.count,
likes: r.count.likes + 1
}
};
}
return r;
});
if (c.id === comment.id) {
return {
...c,
liked: true,
replies,
count: {
...c.count,
likes: c.count.likes + 1
}
};
}
return {
...c,
replies
};
})
};
}
async function reportComment({api, data: comment}: {api: GhostApi, data: {id: string}}) {
await api.comments.report({comment});
return {};
}
async function unlikeComment({state, api, data: comment}: {state: EditableAppContext, api: GhostApi, data: {id: string}}) {
await api.comments.unlike({comment});
return {
comments: state.comments.map((c) => {
const replies = c.replies.map((r) => {
if (r.id === comment.id) {
return {
...r,
liked: false,
count: {
...r.count,
likes: r.count.likes - 1
}
};
}
return r;
});
if (c.id === comment.id) {
return {
...c,
liked: false,
replies,
count: {
...c.count,
likes: c.count.likes - 1
}
};
}
return {
...c,
replies
};
})
};
}
async function deleteComment({state, api, data: comment}: {state: EditableAppContext, api: GhostApi, data: {id: string}}) {
await api.comments.edit({
comment: {
id: comment.id,
status: 'deleted'
}
});
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 {
...c,
status: 'deleted',
replies
};
}
return {
...c,
replies
};
}),
commentCount: state.commentCount - 1
};
}
async function editComment({state, api, data: {comment, parent}}: {state: EditableAppContext, api: GhostApi, data: {comment: Partial<Comment> & {id: string}, parent?: Comment}}) {
const data = await api.comments.edit({
comment
});
comment = data.comments[0];
// Replace the comment in the state with the new one
return {
comments: state.comments.map((c) => {
if (parent && parent.id === c.id) {
return {
...c,
replies: c.replies.map((r) => {
if (r.id === comment.id) {
return comment;
}
return r;
})
};
} else if (c.id === comment.id) {
return comment;
}
return c;
})
};
}
async function updateMember({data, state, api}: {data: {name: string, expertise: string}, state: EditableAppContext, api: GhostApi}) {
const {name, expertise} = data;
const patchData: {name?: string, expertise?: string} = {};
const originalName = state?.member?.name;
if (name && originalName !== name) {
patchData.name = name;
}
const originalExpertise = state?.member?.expertise;
if (expertise !== undefined && originalExpertise !== expertise) {
// Allow to set it to an empty string or to null
patchData.expertise = expertise;
}
if (Object.keys(patchData).length > 0) {
try {
const member = await api.member.update(patchData);
if (!member) {
throw new Error('Failed to update member');
}
return {
member,
success: true
};
} catch (err) {
return {
success: false,
error: err
};
}
}
return null;
}
function openPopup({data}: {data: Page}) {
return {
popup: data
};
}
function closePopup() {
return {
popup: null
};
}
function increaseSecundaryFormCount({state}: {state: EditableAppContext}) {
return {
secundaryFormCount: state.secundaryFormCount + 1
};
}
function decreaseSecundaryFormCount({state}: {state: EditableAppContext}) {
return {
secundaryFormCount: state.secundaryFormCount - 1
};
}
// Sync actions make use of setState((currentState) => newState), to avoid 'race' conditions
export const SyncActions = {
openPopup,
closePopup,
increaseSecundaryFormCount,
decreaseSecundaryFormCount
};
export type SyncActionType = keyof typeof SyncActions;
export const Actions = {
// Put your actions here
addComment,
editComment,
hideComment,
deleteComment,
showComment,
likeComment,
unlikeComment,
reportComment,
addReply,
loadMoreComments,
loadMoreReplies,
updateMember
};
export type ActionType = keyof typeof Actions;
export function isSyncAction(action: string): action is SyncActionType {
return !!(SyncActions as any)[action];
}
/** Handle actions in the App, returns updated state */
export async function ActionHandler({action, data, state, api, adminApi, options}: {action: ActionType, data: any, state: EditableAppContext, options: CommentsOptions, api: GhostApi, adminApi: AdminApi}): Promise<Partial<EditableAppContext>> {
const handler = Actions[action];
if (handler) {
return await handler({data, state, api, adminApi, options} as any) || {};
}
return {};
}
/** Handle actions in the App, returns updated state */
export function SyncActionHandler({action, data, state, api, adminApi, options}: {action: SyncActionType, data: any, state: EditableAppContext, options: CommentsOptions, api: GhostApi, adminApi: AdminApi}): Partial<EditableAppContext> {
const handler = SyncActions[action];
if (handler) {
// Do not await here
return handler({data, state, api, adminApi, options} as any) || {};
}
return {};
}