admin can delete status changes from post timeline
This commit is contained in:
@@ -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<typeof rollbackBody> }>(
|
app.post<{ Params: { id: string }; Body: z.infer<typeof rollbackBody> }>(
|
||||||
"/admin/posts/:id/rollback",
|
"/admin/posts/:id/rollback",
|
||||||
{ preHandler: [app.requireAdmin], config: { rateLimit: { max: 10, timeWindow: "1 minute" } } },
|
{ preHandler: [app.requireAdmin], config: { rateLimit: { max: 10, timeWindow: "1 minute" } } },
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export default function Timeline({
|
|||||||
onReply,
|
onReply,
|
||||||
onShowEditHistory,
|
onShowEditHistory,
|
||||||
onLockComment,
|
onLockComment,
|
||||||
|
onDeleteStatusChange,
|
||||||
}: {
|
}: {
|
||||||
entries: TimelineEntry[]
|
entries: TimelineEntry[]
|
||||||
onReact?: (entryId: string, emoji: string) => void
|
onReact?: (entryId: string, emoji: string) => void
|
||||||
@@ -61,6 +62,7 @@ export default function Timeline({
|
|||||||
onReply?: (entry: TimelineEntry) => void
|
onReply?: (entry: TimelineEntry) => void
|
||||||
onShowEditHistory?: (entryId: string) => void
|
onShowEditHistory?: (entryId: string) => void
|
||||||
onLockComment?: (entryId: string) => void
|
onLockComment?: (entryId: string) => void
|
||||||
|
onDeleteStatusChange?: (entryId: string) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
@@ -76,6 +78,7 @@ export default function Timeline({
|
|||||||
onReply={onReply}
|
onReply={onReply}
|
||||||
onShowEditHistory={onShowEditHistory}
|
onShowEditHistory={onShowEditHistory}
|
||||||
onLockComment={onLockComment}
|
onLockComment={onLockComment}
|
||||||
|
onDeleteStatusChange={onDeleteStatusChange}
|
||||||
index={i}
|
index={i}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -93,6 +96,7 @@ function TimelineItem({
|
|||||||
onReply,
|
onReply,
|
||||||
onShowEditHistory,
|
onShowEditHistory,
|
||||||
onLockComment,
|
onLockComment,
|
||||||
|
onDeleteStatusChange,
|
||||||
index,
|
index,
|
||||||
}: {
|
}: {
|
||||||
entry: TimelineEntry
|
entry: TimelineEntry
|
||||||
@@ -104,6 +108,7 @@ function TimelineItem({
|
|||||||
onReply?: (entry: TimelineEntry) => void
|
onReply?: (entry: TimelineEntry) => void
|
||||||
onShowEditHistory?: (entryId: string) => void
|
onShowEditHistory?: (entryId: string) => void
|
||||||
onLockComment?: (entryId: string) => void
|
onLockComment?: (entryId: string) => void
|
||||||
|
onDeleteStatusChange?: (entryId: string) => void
|
||||||
index: number
|
index: number
|
||||||
}) {
|
}) {
|
||||||
const confirm = useConfirm()
|
const confirm = useConfirm()
|
||||||
@@ -138,6 +143,19 @@ function TimelineItem({
|
|||||||
<time dateTime={entry.createdAt} style={{ color: 'var(--text-tertiary)', fontSize: 'var(--text-xs)' }}>
|
<time dateTime={entry.createdAt} style={{ color: 'var(--text-tertiary)', fontSize: 'var(--text-xs)' }}>
|
||||||
{new Date(entry.createdAt).toLocaleDateString()}
|
{new Date(entry.createdAt).toLocaleDateString()}
|
||||||
</time>
|
</time>
|
||||||
|
{isCurrentAdmin && onDeleteStatusChange && (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
if (!await confirm('Remove this status change?')) return
|
||||||
|
onDeleteStatusChange(entry.id)
|
||||||
|
}}
|
||||||
|
className="action-btn"
|
||||||
|
aria-label="Remove status change"
|
||||||
|
style={{ color: 'var(--text-tertiary)', padding: '2px 4px', borderRadius: 'var(--radius-sm)', minHeight: 24 }}
|
||||||
|
>
|
||||||
|
<IconTrash size={12} stroke={2} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{entry.reason && (
|
{entry.reason && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -931,6 +931,15 @@ export default function PostDetail() {
|
|||||||
onReply={post.isThreadLocked ? undefined : handleReply}
|
onReply={post.isThreadLocked ? undefined : handleReply}
|
||||||
onShowEditHistory={showCommentHistory}
|
onShowEditHistory={showCommentHistory}
|
||||||
onLockComment={admin.isAdmin ? toggleCommentEditLock : undefined}
|
onLockComment={admin.isAdmin ? toggleCommentEditLock : undefined}
|
||||||
|
onDeleteStatusChange={admin.isAdmin ? async (id) => {
|
||||||
|
try {
|
||||||
|
await api.delete(`/admin/status-changes/${id}`)
|
||||||
|
toast.success('Status change removed')
|
||||||
|
fetchPost()
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to remove status change')
|
||||||
|
}
|
||||||
|
} : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user