From 393001c07c6a3ebbfebdb8d0c5391775740618f0 Mon Sep 17 00:00:00 2001 From: lashman Date: Sun, 22 Mar 2026 07:41:59 +0200 Subject: [PATCH] admin can delete status changes from post timeline --- packages/api/src/routes/admin/posts.ts | 20 ++++++++++++++++++++ packages/web/src/components/Timeline.tsx | 18 ++++++++++++++++++ packages/web/src/pages/PostDetail.tsx | 9 +++++++++ 3 files changed, 47 insertions(+) diff --git a/packages/api/src/routes/admin/posts.ts b/packages/api/src/routes/admin/posts.ts index b28ff04..4f4b478 100644 --- a/packages/api/src/routes/admin/posts.ts +++ b/packages/api/src/routes/admin/posts.ts @@ -509,6 +509,26 @@ export default async function adminPostRoutes(app: FastifyInstance) { } ); + // delete a status change entry + app.delete<{ Params: { id: string } }>( + "/admin/status-changes/:id", + { preHandler: [app.requireAdmin], config: { rateLimit: { max: 20, timeWindow: "1 minute" } } }, + async (req, reply) => { + const sc = await prisma.statusChange.findUnique({ where: { id: req.params.id }, select: { id: true, postId: true, toStatus: true } }); + if (!sc) { + reply.status(404).send({ error: "Status change not found" }); + return; + } + // delete related notifications + await prisma.notification.deleteMany({ + where: { postId: sc.postId, type: "status_changed" }, + }); + await prisma.statusChange.delete({ where: { id: sc.id } }); + req.log.info({ adminId: req.adminId, statusChangeId: sc.id }, "status change deleted"); + reply.status(204).send(); + } + ); + app.post<{ Params: { id: string }; Body: z.infer }>( "/admin/posts/:id/rollback", { preHandler: [app.requireAdmin], config: { rateLimit: { max: 10, timeWindow: "1 minute" } } }, diff --git a/packages/web/src/components/Timeline.tsx b/packages/web/src/components/Timeline.tsx index c82e9f9..6203c04 100644 --- a/packages/web/src/components/Timeline.tsx +++ b/packages/web/src/components/Timeline.tsx @@ -51,6 +51,7 @@ export default function Timeline({ onReply, onShowEditHistory, onLockComment, + onDeleteStatusChange, }: { entries: TimelineEntry[] onReact?: (entryId: string, emoji: string) => void @@ -61,6 +62,7 @@ export default function Timeline({ onReply?: (entry: TimelineEntry) => void onShowEditHistory?: (entryId: string) => void onLockComment?: (entryId: string) => void + onDeleteStatusChange?: (entryId: string) => void }) { return (
@@ -76,6 +78,7 @@ export default function Timeline({ onReply={onReply} onShowEditHistory={onShowEditHistory} onLockComment={onLockComment} + onDeleteStatusChange={onDeleteStatusChange} index={i} /> ))} @@ -93,6 +96,7 @@ function TimelineItem({ onReply, onShowEditHistory, onLockComment, + onDeleteStatusChange, index, }: { entry: TimelineEntry @@ -104,6 +108,7 @@ function TimelineItem({ onReply?: (entry: TimelineEntry) => void onShowEditHistory?: (entryId: string) => void onLockComment?: (entryId: string) => void + onDeleteStatusChange?: (entryId: string) => void index: number }) { const confirm = useConfirm() @@ -138,6 +143,19 @@ function TimelineItem({ + {isCurrentAdmin && onDeleteStatusChange && ( + + )}
{entry.reason && (
{ + try { + await api.delete(`/admin/status-changes/${id}`) + toast.success('Status change removed') + fetchPost() + } catch { + toast.error('Failed to remove status change') + } + } : undefined} />
)}