/* eslint-disable react-refresh/only-export-components */ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { ChevronRight, Check, GripVertical } from "lucide-react"; import type { Host, HostFolder, TabType } from "@/types/ui-types"; import type { HostDensity, HostTrayTrigger, } from "@/types/host-sidebar-preferences"; import { FolderIconEl } from "@/components/folder-style"; import { useServerStatus } from "@/lib/ServerStatusContext"; import { HostItem, statusCheckEnabled } from "../HostItem/HostItem"; import { isFolder, folderHasMatch, collectAllHosts } from "../visible-rows"; import { FolderActions } from "./FolderActions"; export function folderHostCount(folder: HostFolder): { total: number; online: number; } { let total = 0, online = 0; for (const child of folder.children) { if (isFolder(child)) { const c = folderHostCount(child); total += c.total; online += c.online; } else { total++; if (child.online) online++; } } return { total, online }; } export function FolderItem({ folder, depth = 0, onOpenTab, onEditHost, onShareHost, onDeleteHost, onDuplicateHost, onProxmoxDiscover, query = "", stripeMap, openFolders, onToggleFolder, selectionMode, selectedHostIds, onToggleSelect, onToggleSelectFolder, openMenuHostId, onMenuOpenChange, openTrayHostId, onTrayOpenChange, onManageFolder, onDeleteFolder, onOpenAllSessions, onShareFolder, onMoveHostsToFolder, draggedHostIds, onDragHostStart, onDragEnd, /** When true, only render the folder header (children come from the virtual list). */ flat = false, stripeIndex: stripeIndexProp, density = "comfortable", trayTrigger = "hover", showTags = true, openOnDoubleClick = false, arrangeMode = false, isDragging = false, onReorderDrop, onFolderDragStart, onFolderDragEnd, isReorderHovered = false, reorderHoverEdge = null, onReorderHoverChange, }: { folder: HostFolder; depth?: number; onOpenTab: ( host: Host, type: TabType, options?: { endpointId?: string; label?: string }, ) => void; onEditHost?: (host: Host) => void; onShareHost?: (host: Host) => void; onDeleteHost: (host: Host) => void; onDuplicateHost: (host: Host) => void; onProxmoxDiscover?: (host: Host) => void; query?: string; stripeMap?: Map; openFolders: Set; onToggleFolder: (name: string) => void; selectionMode: boolean; selectedHostIds: Set; onToggleSelect: (id: string) => void; onToggleSelectFolder: (folder: HostFolder) => void; openMenuHostId: string | null; onMenuOpenChange: (hostId: string | null) => void; openTrayHostId: string | null; onTrayOpenChange: (hostId: string | null) => void; onManageFolder: (folder: HostFolder) => void; onDeleteFolder: (folder: HostFolder) => void; onOpenAllSessions: (folder: HostFolder) => void; onShareFolder?: (folder: HostFolder) => void; onMoveHostsToFolder: (hostIds: string[], targetPath: string) => void; draggedHostIds: string[] | null; onDragHostStart: (hostId: string) => void; onDragEnd: () => void; flat?: boolean; stripeIndex?: number; density?: HostDensity; trayTrigger?: HostTrayTrigger; showTags?: boolean; openOnDoubleClick?: boolean; /** When true (rearranging unlocked), the header can be dragged and its * top/bottom edges become reorder drop zones. The middle still accepts * hosts dropped into the folder. */ arrangeMode?: boolean; /** True while this folder is the one being dragged. */ isDragging?: boolean; onReorderDrop?: (targetKey: string, position: "before" | "after") => void; onFolderDragStart?: (folderPath: string) => void; onFolderDragEnd?: () => void; /** Whether THIS folder header is the current reorder drop target -- see * HostItem's identical prop for why this is lifted rather than local. */ isReorderHovered?: boolean; reorderHoverEdge?: "before" | "after" | null; onReorderHoverChange?: (edge: "before" | "after" | null) => void; }) { const { t } = useTranslation(); const { getStatus, initialLoadComplete } = useServerStatus(); const { total } = folderHostCount(folder); const online = initialLoadComplete ? collectAllHosts(folder.children).filter( (h) => statusCheckEnabled(h) && getStatus(Number(h.id)) === "online", ).length : folderHostCount(folder).online; const [dragOver, setDragOver] = useState(false); const reorderEdge = isReorderHovered ? reorderHoverEdge : null; if (query && !folderHasMatch(folder, query)) return null; const folderPath = folder.path ?? folder.name; const isOpen = query ? true : openFolders.has(folderPath); const stripeIndex = stripeIndexProp ?? stripeMap?.get(folder) ?? 0; // Synthetic group headers (group-by tag/status/etc.) are not real folders, so // they can't be edited, deleted, or used as drop targets. const isGroup = folderPath.startsWith("__group__:"); // Nested folders show their parent path as a muted breadcrumb so depth stays // legible even when a folder is reached via search auto-expand rather than // by manually opening every ancestor. const pathSegments = isGroup ? [] : folderPath.split(" / "); const breadcrumb = pathSegments.length > 1 ? pathSegments.slice(0, -1).join(" / ") : null; const folderHosts = collectAllHosts(folder.children); const folderSelected = folderHosts.length > 0 && folderHosts.every((h) => selectedHostIds.has(h.id)); const canDragFolder = arrangeMode && !isGroup; // Hosts land IN the folder; the header's own top/bottom edges reorder it // among its siblings. const acceptsHostDrop = !!draggedHostIds && !isGroup; return (
0 ? { paddingLeft: depth * 12 } : undefined} >
{!flat && isOpen && (
{folder.children.map((child, i) => isFolder(child) ? ( ) : ( onOpenTab(child, t, options)} onEditHost={onEditHost ? () => onEditHost(child) : undefined} onShareHost={onShareHost ? () => onShareHost(child) : undefined} onProxmoxDiscover={ onProxmoxDiscover ? () => onProxmoxDiscover(child) : undefined } onDelete={() => onDeleteHost(child)} onDuplicate={() => onDuplicateHost(child)} query={query} stripeIndex={stripeMap?.get(child) ?? 0} selectionMode={selectionMode} selected={selectedHostIds.has(child.id)} onToggleSelect={() => onToggleSelect(child.id)} isMenuOpen={openMenuHostId === child.id} onMenuOpenChange={(open) => onMenuOpenChange(open ? child.id : null) } isTrayOpen={openTrayHostId === child.id} onTrayOpenChange={(open) => onTrayOpenChange(open ? child.id : null) } onDragStart={() => onDragHostStart(child.id)} onDragEnd={onDragEnd} density={density} trayTrigger={trayTrigger} showTags={showTags} openOnDoubleClick={openOnDoubleClick} arrangeMode={arrangeMode} onReorderDrop={ onReorderDrop ? (position) => onReorderDrop(`host:${child.id}`, position) : undefined } /> ), )}
)}
); }