Files
app/src/admin/components/ProjectFileManager.tsx
BOHA 528e55991b security: fix all Critical and High findings from FLAWS_REPORT audit
- Auth: pessimistic locking on login tokens and refresh token rotation,
  backup code attempt counter, rate limiting verification
- Schema: unique constraints on business numbers, FK relations,
  unsigned/signed alignment, attendance duplicate prevention
- Invoices/PDFs: DOMPurify sanitization, bounded queries in stats
  and alerts, VAT rounding, Puppeteer error handling
- Orders/Offers: transactional parent+child creation, Zod NaN
  refinement, status enums, uniqueness checks
- Projects/Files: path traversal protection, streamed uploads,
  permission guards, query param validation
- Attendance/HR: duplicate checks, ownership validation, GPS
  restrictions, trip distance validation
- Frontend: modal lock reference counting, XSS escaping in print
  HTML, ref mutation fixes, accessibility attributes

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 00:58:35 +02:00

893 lines
28 KiB
TypeScript

import { useState, useEffect, useRef, useCallback } from "react";
import { useAlert } from "../context/AlertContext";
import ConfirmModal from "./ConfirmModal";
import apiFetch from "../utils/api";
const API_BASE = "/api/admin";
interface ProjectFileManagerProps {
projectId: number;
projectNumber: string | null;
hasPermission: (perm: string) => boolean;
hasNasFolder: boolean;
}
interface FileItem {
name: string;
type: "file" | "folder";
size?: number;
size_formatted?: string;
modified?: string;
extension?: string;
item_count?: number;
is_symlink?: boolean;
link_target?: string;
}
function getFileIcon(type: string, extension?: string) {
if (type === "folder") {
return (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="#e6a817"
strokeWidth="1.5"
>
<path
d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
fill="rgba(230, 168, 23, 0.15)"
/>
</svg>
);
}
const ext = (extension || "").toLowerCase();
const iconMap: Record<string, { color: string; path: string }> = {
pdf: {
color: "#e74c3c",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
doc: {
color: "#3498db",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
docx: {
color: "#3498db",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
xls: {
color: "#27ae60",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
xlsx: {
color: "#27ae60",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
ppt: {
color: "#e67e22",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
pptx: {
color: "#e67e22",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
jpg: {
color: "#3498db",
path: "M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z",
},
jpeg: {
color: "#3498db",
path: "M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z",
},
png: {
color: "#3498db",
path: "M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z",
},
gif: {
color: "#3498db",
path: "M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z",
},
zip: {
color: "#e67e22",
path: "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z",
},
rar: {
color: "#e67e22",
path: "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z",
},
"7z": {
color: "#e67e22",
path: "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z",
},
dwg: {
color: "#8e44ad",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
dxf: {
color: "#8e44ad",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
step: {
color: "#8e44ad",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
stp: {
color: "#8e44ad",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
};
const cfg = iconMap[ext] || {
color: "var(--text-muted)",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
};
return (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke={cfg.color}
strokeWidth="1.5"
>
<path d={cfg.path} />
<polyline points="14 2 14 8 20 8" />
</svg>
);
}
function SymlinkBadge({ target }: { target?: string }) {
return (
<span className="fm-symlink-badge" title={target || "Odkaz"}>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span>
);
}
function FileNameCell({
item,
onFolderClick,
}: {
item: FileItem;
onFolderClick: (name: string) => void;
}) {
if (item.type === "folder") {
return (
<span className="fm-name-cell">
<button
type="button"
className="fm-folder-link"
onClick={() => onFolderClick(item.name)}
>
{item.name}
</button>
{item.is_symlink && <SymlinkBadge target={item.link_target} />}
{item.item_count !== undefined && (
<span className="fm-item-count">{item.item_count}</span>
)}
</span>
);
}
return (
<span className="fm-name-cell">
<span className="fm-file-name">{item.name}</span>
{item.is_symlink && <SymlinkBadge target={item.link_target} />}
</span>
);
}
export default function ProjectFileManager({
projectId,
projectNumber,
hasPermission,
hasNasFolder,
}: ProjectFileManagerProps) {
const alert = useAlert();
const fileInputRef = useRef<HTMLInputElement>(null);
const isCancelling = useRef(false);
const [items, setItems] = useState<FileItem[]>([]);
const [loading, setLoading] = useState(true);
const [currentPath, setCurrentPath] = useState("");
const [breadcrumb, setBreadcrumb] = useState<string[]>([""]);
const [fullPath, setFullPath] = useState("");
const [dragOver, setDragOver] = useState(false);
const [uploading, setUploading] = useState(false);
const [newFolderMode, setNewFolderMode] = useState(false);
const [newFolderName, setNewFolderName] = useState("");
const [creatingFolder, setCreatingFolder] = useState(false);
const [renamingItem, setRenamingItem] = useState<string | null>(null);
const [renameValue, setRenameValue] = useState("");
const [deleteTarget, setDeleteTarget] = useState<FileItem | null>(null);
const [deleting, setDeleting] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const canManage = hasPermission("projects.files");
const fetchFiles = useCallback(
async (path = "", options: { ignore?: boolean } = {}) => {
setLoading(true);
setErrorMessage(null);
try {
const params = new URLSearchParams({ project_id: String(projectId) });
if (path) {
params.set("path", path);
}
const res = await apiFetch(`${API_BASE}/project-files?${params}`);
if (options.ignore) return;
if (res.status === 401) return;
const data = await res.json();
if (data.success) {
setItems(data.data.items || []);
setBreadcrumb(data.data.breadcrumb || [""]);
setCurrentPath(data.data.path || "");
setFullPath(data.data.full_path || "");
} else if (res.status === 404) {
setItems([]);
setBreadcrumb([""]);
} else {
setErrorMessage(data.error || "Nepodařilo se načíst soubory");
}
} catch {
if (!options.ignore) {
setErrorMessage("Chyba připojení");
}
} finally {
if (!options.ignore) {
setLoading(false);
}
}
},
[projectId],
);
useEffect(() => {
const opts = { ignore: false };
fetchFiles("", opts);
return () => {
opts.ignore = true;
};
}, [fetchFiles]);
const navigateTo = (path: string) => {
setNewFolderMode(false);
setRenamingItem(null);
fetchFiles(path);
};
const handleBreadcrumbClick = (index: number) => {
if (index === 0) {
navigateTo("");
return;
}
const path = breadcrumb.slice(1, index + 1).join("/");
navigateTo(path);
};
const handleFolderClick = (folderName: string) => {
const path = currentPath ? `${currentPath}/${folderName}` : folderName;
navigateTo(path);
};
const handleUpload = async (files: FileList | null) => {
if (!files || files.length === 0) return;
setUploading(true);
let successCount = 0;
let errorMsg: string | null = null;
for (const file of Array.from(files)) {
const formData = new FormData();
formData.append("file", file);
const params = new URLSearchParams({
action: "upload",
project_id: String(projectId),
});
if (currentPath) {
params.set("path", currentPath);
}
try {
const res = await apiFetch(
`${API_BASE}/project-files/upload?${params}`,
{
method: "POST",
body: formData,
},
);
const data = await res.json();
if (data.success) {
successCount++;
} else {
errorMsg = data.error || "Chyba při nahrávání";
}
} catch {
errorMsg = "Chyba připojení";
}
}
setUploading(false);
if (successCount > 0) {
const msg =
successCount === 1
? "Soubor byl nahrán"
: `Nahráno ${successCount} souborů`;
alert.success(msg);
fetchFiles(currentPath);
}
if (errorMsg) {
alert.error(errorMsg);
}
};
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
handleUpload(e.target.files);
e.target.value = "";
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
if (!canManage) return;
handleUpload(e.dataTransfer.files);
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
if (canManage) {
setDragOver(true);
}
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
};
const handleCreateFolder = async () => {
const name = newFolderName.trim();
if (!name) return;
setCreatingFolder(true);
try {
const params = new URLSearchParams({
action: "create_folder",
project_id: String(projectId),
});
const res = await apiFetch(`${API_BASE}/project-files?${params}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: currentPath, folder_name: name }),
});
const data = await res.json();
if (data.success) {
alert.success("Složka byla vytvořena");
setNewFolderMode(false);
setNewFolderName("");
fetchFiles(currentPath);
} else {
alert.error(data.error || "Nepodařilo se vytvořit složku");
}
} catch {
alert.error("Chyba připojení");
} finally {
setCreatingFolder(false);
}
};
const handleDownload = async (item: FileItem) => {
const filePath = currentPath ? `${currentPath}/${item.name}` : item.name;
const params = new URLSearchParams({
action: "download",
project_id: String(projectId),
path: filePath,
});
try {
const res = await apiFetch(`${API_BASE}/project-files?${params}`);
if (!res.ok) {
const err = await res.json().catch(() => null);
alert.error(err?.error || "Chyba při stahování");
return;
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = item.name;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
} catch {
alert.error("Chyba připojení");
}
};
const handleDelete = async () => {
if (!deleteTarget) return;
setDeleting(true);
const filePath = currentPath
? `${currentPath}/${deleteTarget.name}`
: deleteTarget.name;
try {
const params = new URLSearchParams({
project_id: String(projectId),
path: filePath,
});
const res = await apiFetch(`${API_BASE}/project-files?${params}`, {
method: "DELETE",
});
const data = await res.json();
if (data.success) {
alert.success(
deleteTarget.type === "folder"
? "Složka byla smazána"
: "Soubor byl smazán",
);
fetchFiles(currentPath);
} else {
alert.error(data.error || "Nepodařilo se smazat");
}
} catch {
alert.error("Chyba připojení");
} finally {
setDeleting(false);
setDeleteTarget(null);
}
};
const handleRename = async (item: FileItem) => {
const newName = renameValue.trim();
if (!newName || newName === item.name) {
setRenamingItem(null);
return;
}
const fromPath = currentPath ? `${currentPath}/${item.name}` : item.name;
const toPath = currentPath ? `${currentPath}/${newName}` : newName;
try {
const params = new URLSearchParams({
action: "move",
project_id: String(projectId),
});
const res = await apiFetch(`${API_BASE}/project-files?${params}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ from_path: fromPath, to_path: toPath }),
});
const data = await res.json();
if (data.success) {
alert.success("Přejmenováno");
fetchFiles(currentPath);
} else {
alert.error(data.error || "Nepodařilo se přejmenovat");
}
} catch {
alert.error("Chyba připojení");
} finally {
setRenamingItem(null);
}
};
const startRename = (item: FileItem) => {
setRenamingItem(item.name);
setRenameValue(item.name);
};
if (loading && items.length === 0 && !errorMessage) {
return (
<div className="admin-card">
<div className="admin-card-body">
<h3 className="admin-card-title">Soubory</h3>
<div className="admin-skeleton" style={{ padding: 0, gap: "0.5rem" }}>
{[0, 1, 2, 3].map((i) => (
<div key={i} className="admin-skeleton-row">
<div
className="admin-skeleton-line"
style={{
width: "18px",
height: "18px",
borderRadius: "4px",
flexShrink: 0,
}}
/>
<div
className="admin-skeleton-line"
style={{ width: `${60 + i * 10}%` }}
/>
</div>
))}
</div>
</div>
</div>
);
}
if (errorMessage) {
return (
<div className="admin-card">
<div className="admin-card-body">
<h3 className="admin-card-title">Soubory</h3>
<div className="fm-empty">
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="var(--text-tertiary)"
strokeWidth="1.5"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
<span>{errorMessage}</span>
</div>
</div>
</div>
);
}
return (
<div className="admin-card">
<div className="admin-card-body">
<h3 className="admin-card-title">Soubory</h3>
{/* Toolbar */}
<div className="fm-toolbar">
<div className="fm-breadcrumb">
{breadcrumb.map((segment, i) => (
<span key={i} className="fm-breadcrumb-segment">
{i > 0 && <span className="fm-breadcrumb-sep">/</span>}
<button
type="button"
className={`fm-breadcrumb-btn ${i === breadcrumb.length - 1 ? "active" : ""}`}
onClick={() => handleBreadcrumbClick(i)}
>
{i === 0 ? projectNumber : segment}
</button>
</span>
))}
</div>
{fullPath && (
<span className="fm-full-path" title={fullPath}>
{fullPath}
</span>
)}
{canManage && (
<div className="fm-toolbar-actions">
<button
type="button"
className="admin-btn admin-btn-secondary admin-btn-sm"
onClick={() => {
setNewFolderMode(!newFolderMode);
setNewFolderName("");
}}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
<line x1="12" y1="11" x2="12" y2="17" />
<line x1="9" y1="14" x2="15" y2="14" />
</svg>
Složka
</button>
<button
type="button"
className="admin-btn admin-btn-primary admin-btn-sm"
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
>
{uploading ? (
<>
<div className="admin-spinner admin-spinner-sm" />
Nahrávání...
</>
) : (
<>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
Nahrát
</>
)}
</button>
<input
ref={fileInputRef}
type="file"
multiple
style={{ display: "none" }}
onChange={handleFileInputChange}
/>
</div>
)}
</div>
{/* New folder input */}
{newFolderMode && (
<div className="fm-new-folder">
<input
type="text"
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
className="admin-form-input"
placeholder="Název složky..."
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter") handleCreateFolder();
if (e.key === "Escape") {
setNewFolderMode(false);
setNewFolderName("");
}
}}
style={{ fontSize: "12px", padding: "6px 10px" }}
/>
<button
type="button"
className="admin-btn admin-btn-primary admin-btn-sm"
onClick={handleCreateFolder}
disabled={creatingFolder || !newFolderName.trim()}
>
{creatingFolder ? (
<div className="admin-spinner admin-spinner-sm" />
) : (
"Vytvořit"
)}
</button>
<button
type="button"
className="admin-btn admin-btn-secondary admin-btn-sm"
onClick={() => {
setNewFolderMode(false);
setNewFolderName("");
}}
>
Zrušit
</button>
</div>
)}
{/* Drop zone + table */}
<div
className={`fm-content ${dragOver ? "fm-drag-over" : ""}`}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
>
{dragOver && (
<div className="fm-dropzone-overlay">
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
<span>Přetáhněte soubory sem</span>
</div>
)}
{items.length === 0 && !loading ? (
<div className="fm-empty">
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="var(--text-tertiary)"
strokeWidth="1.5"
>
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
<span>
{hasNasFolder
? "Složka je prázdná"
: "Složka projektu zatím neexistuje"}
</span>
{canManage && !hasNasFolder && (
<span style={{ fontSize: "11px" }}>
Nahrání souboru ji automaticky vytvoří
</span>
)}
</div>
) : (
<div className="admin-table-responsive">
<table className="admin-table">
<thead>
<tr>
<th style={{ width: "30px" }}></th>
<th>Název</th>
<th style={{ width: "90px" }}>Velikost</th>
<th style={{ width: "120px" }}>Změněno</th>
{canManage && (
<th style={{ width: "100px", textAlign: "right" }}>
Akce
</th>
)}
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.name}>
<td style={{ textAlign: "center" }}>
{getFileIcon(item.type, item.extension)}
</td>
<td>
{renamingItem === item.name ? (
<input
type="text"
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
className="admin-form-input"
style={{
fontSize: "11px",
padding: "3px 8px",
maxWidth: "300px",
}}
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleRename(item);
}
if (e.key === "Escape") {
e.preventDefault();
isCancelling.current = true;
setRenamingItem(null);
setRenameValue(item.name);
setTimeout(() => {
isCancelling.current = false;
}, 0);
}
}}
onBlur={() => {
if (isCancelling.current) {
return;
}
handleRename(item);
}}
/>
) : (
<FileNameCell
item={item}
onFolderClick={handleFolderClick}
/>
)}
</td>
<td className="fm-meta">
{item.type === "file" ? item.size_formatted : "\u2014"}
</td>
<td className="fm-meta">{item.modified || "\u2014"}</td>
{canManage && (
<td style={{ textAlign: "right" }}>
<div className="fm-actions">
{item.type === "file" && (
<button
type="button"
className="admin-btn-icon"
title="Stáhnout"
onClick={() => handleDownload(item)}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
</button>
)}
<button
type="button"
className="admin-btn-icon"
title="Přejmenovat"
onClick={() => startRename(item)}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
</button>
<button
type="button"
className="admin-btn-icon danger"
title="Smazat"
onClick={() => setDeleteTarget(item)}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="3 6 5 6 21 6" />
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
<path d="M10 11v6M14 11v6" />
</svg>
</button>
</div>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
<ConfirmModal
isOpen={deleteTarget !== null}
onClose={() => setDeleteTarget(null)}
onConfirm={handleDelete}
title={
deleteTarget?.type === "folder" ? "Smazat složku" : "Smazat soubor"
}
message={`Opravdu chcete smazat "${deleteTarget?.name}"?${deleteTarget?.type === "folder" ? " Složka bude smazána včetně veškerého obsahu." : ""}`}
confirmText="Smazat"
cancelText="Zrušit"
type="danger"
loading={deleting}
/>
</div>
);
}