feat(admin): multi-select bulk delete in media library

Per-file checkboxes + "حذف موارد انتخاب‌شده (N)" bar that deletes all selected
files in parallel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-03 01:29:22 +03:30
parent c076345ceb
commit c7694a9bbf
+29 -1
View File
@@ -15,8 +15,17 @@ export function FileManager() {
const [copied, setCopied] = useState<string | null>(null); const [copied, setCopied] = useState<string | null>(null);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [type, setType] = useState(""); const [type, setType] = useState("");
const [selected, setSelected] = useState<Set<string>>(new Set());
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const toggleSel = (id: string) =>
setSelected((s) => {
const n = new Set(s);
if (n.has(id)) n.delete(id);
else n.add(id);
return n;
});
const reload = useCallback(async () => { const reload = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
@@ -59,6 +68,14 @@ export function FileManager() {
setTimeout(() => setCopied(null), 1500); setTimeout(() => setCopied(null), 1500);
}; };
const bulkDelete = async () => {
if (selected.size === 0) return;
if (!confirm(`حذف ${selected.size.toLocaleString("fa-IR")} فایل انتخاب‌شده؟`)) return;
await Promise.all(Array.from(selected).map((id) => fetch(`/api/admin/resource/files/${id}`, { method: "DELETE" })));
setSelected(new Set());
reload();
};
return ( return (
<div className="space-y-4" dir="rtl"> <div className="space-y-4" dir="rtl">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
@@ -81,6 +98,11 @@ export function FileManager() {
</button> </button>
))} ))}
</div> </div>
{selected.size > 0 && (
<button onClick={bulkDelete} className="rounded-lg border border-red-500/40 bg-red-500/10 px-3 py-1.5 text-xs text-red-300 hover:bg-red-500/20">
حذف موارد انتخابشده ({selected.size.toLocaleString("fa-IR")})
</button>
)}
<input <input
className="ms-auto w-56 rounded-lg border border-[#262b40] bg-[#0c0e1a] px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500" className="ms-auto w-56 rounded-lg border border-[#262b40] bg-[#0c0e1a] px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500"
placeholder="جستجوی نام فایل…" value={search} onChange={(e) => setSearch(e.target.value)} placeholder="جستجوی نام فایل…" value={search} onChange={(e) => setSearch(e.target.value)}
@@ -103,7 +125,13 @@ export function FileManager() {
{files.map((f) => { {files.map((f) => {
const url = fileUrl(f); const url = fileUrl(f);
return ( return (
<div key={f.id} className="group rounded-lg border border-[#262b40] bg-[#0c0e1a] p-2"> <div key={f.id} className={`group relative rounded-lg border bg-[#0c0e1a] p-2 ${selected.has(f.id) ? "border-indigo-500" : "border-[#262b40]"}`}>
<input
type="checkbox"
checked={selected.has(f.id)}
onChange={() => toggleSel(f.id)}
className="absolute end-3 top-3 z-10 h-4 w-4 cursor-pointer accent-indigo-500"
/>
<div className="flex aspect-square items-center justify-center overflow-hidden rounded-md bg-[#070811]"> <div className="flex aspect-square items-center justify-center overflow-hidden rounded-md bg-[#070811]">
{url && isImage(f) ? ( {url && isImage(f) ? (
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element