feat(rubgram): add bulk delete in admin
Build and Push Image / build-backend (push) Successful in 52m32s
Build and Push Image / build-frontend (push) Successful in 54m45s
Build and Push Image / push (push) Successful in 52m47s

This commit is contained in:
2026-06-20 21:15:16 +07:00 Unverified
parent 4180883cfc
commit 446aa7d5c8
4 changed files with 96 additions and 15 deletions
+78 -14
View File
@@ -1,7 +1,7 @@
"use client";
import { useProgress } from "@bprogress/next";
import { Calendar, Lock, Unlock } from "lucide-react";
import { Calendar, Lock, Trash2, Unlock } from "lucide-react";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
@@ -39,7 +39,7 @@ import { shared } from "@/lib/comms";
import { sse } from "@/lib/db/sse-endpoints";
import { cn } from "@/lib/utils";
import type { getEndgameConfig } from "../api";
import { setFree, setLimit, toggleCheck, toggleLock } from "../api";
import { bulkDelete, setFree, setLimit, toggleCheck, toggleLock } from "../api";
export function SidebarLink({
submission,
@@ -259,6 +259,7 @@ export function SubmissionList({
}) {
const [query, setQuery] = useState("");
const [debug] = shared.state("debug");
const [selIds, setSelIds] = shared.state("rubgram.sel_id");
const bottomRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
@@ -280,23 +281,86 @@ export function SubmissionList({
(debug || (s.paid && !s.deleted)) &&
(s.publicQueue + s.name).toLowerCase().includes(query),
)
.map((s) => (
<SidebarMenuButton
key={s.id}
asChild
className={cn(((debug && !s.paid) || s.deleted) && "opacity-50")}
>
<SidebarLink
submission={{ ...s, queue: s.publicQueue }}
prefetch={!s.deleted}
/>
</SidebarMenuButton>
))}
.map((s) =>
debug ? (
<div key={s.id} className="flex items-center gap-0">
<Checkbox
className="mx-1 shrink-0"
checked={selIds?.includes(s.id) ?? false}
onCheckedChange={(ch) =>
setSelIds((prev) =>
ch
? [...(prev || []), s.id]
: (prev || []).filter((i) => i !== s.id),
)
}
/>
<SidebarMenuButton
asChild
className={cn(
((debug && !s.paid) || s.deleted) && "opacity-50",
)}
>
<SidebarLink
submission={{ ...s, queue: s.publicQueue }}
prefetch={!s.deleted}
/>
</SidebarMenuButton>
</div>
) : (
<SidebarMenuButton
key={s.id}
asChild
className={cn(
((debug && !s.paid) || s.deleted) && "opacity-50",
)}
>
<SidebarLink
submission={{ ...s, queue: s.publicQueue }}
prefetch={!s.deleted}
/>
</SidebarMenuButton>
),
)}
<div ref={bottomRef} />
</>
);
}
export function BulkDeleteButton() {
const [selIds, setSelIds] = shared.state("rubgram.sel_id");
const [loading, setLoading] = useState(false);
if (!selIds?.length) return null;
return (
<SidebarMenu>
<SidebarMenuItem>
<Button
variant="destructive"
size="sm"
className="w-full"
disabled={loading}
onClick={async () => {
setLoading(true);
try {
await bulkDelete(selIds);
setSelIds([]);
toast.success(`ลบ ${selIds.length} รายการแล้ว`);
} catch (e) {
toast.error(`${e}`);
}
setLoading(false);
}}
>
{loading ? "กำลังลบ..." : <Trash2 />}
ลบ {selIds.length} รายการ
</Button>
</SidebarMenuItem>
</SidebarMenu>
);
}
export function Watcher() {
const router = useRouter();
const count = useRef<number | undefined>(undefined);
+2
View File
@@ -33,6 +33,7 @@ import { db } from "@/lib/db";
import { endgameSubmissions } from "@/lib/db/schema";
import { getEndgameConfig, random, wipe } from "../api";
import {
BulkDeleteButton,
CalendarButton,
LimitManager,
SubmissionList,
@@ -123,6 +124,7 @@ export default async function AdminLayout({
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<BulkDeleteButton />
<SidebarGroup>
<SidebarGroupLabel>การตั้งค่า</SidebarGroupLabel>
<LimitManager
+15 -1
View File
@@ -1,6 +1,6 @@
"use server";
import { and, desc, eq, gt, lt, not, sql } from "drizzle-orm";
import { and, desc, eq, gt, inArray, lt, not, sql } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
@@ -433,6 +433,20 @@ export async function cancel(sid: string) {
revalidatePath("/rubgram");
}
export async function bulkDelete(ids: string[]) {
if (!(await adminCheck())) throw "Unauthorized";
await db
.update(endgameSubmissions)
.set({ deleted: true })
.where(inArray(endgameSubmissions.id, ids));
revalidatePath("/rubgram/admin");
sse.rubgram.pub("update", { type: "wipe" });
await actionLog(`Bulk deleted ${ids.length} rubgram submissions`);
}
export async function discordCall(id: string) {
if (!(await adminCheck())) throw "Unauthorized";
+1
View File
@@ -21,6 +21,7 @@ type SharedStates = Partial<{
connected: boolean;
updated: boolean;
"rubgram.services": string[];
"rubgram.sel_id": string[];
"tl.deleteMode": boolean;
inspect: Record<string, string>;
// nf(Not Found) and showcase(Not In Showcase) is error dialog that opens guide