import type { CSSProperties, DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, ReactNode } from "react"; import { Copy, Download, FolderOpen, Home, MoveRight, Plus, Trash2, UploadCloud } from "lucide-react"; import { Button, Dialog, ExplorerTree, type ExplorerTreeNodeContext } from "@govoplan/core-webui"; import type { ConflictAction, FileSpace, RenameResponse } from "../../../api/files"; import type { ConflictDialogState, FileActionTarget, FileConflictItem, FolderNode, ContextMenuState } from "../types"; import { isPathUnderOrSame, normalizeFolder, treeNodeKey } from "../utils/fileManagerUtils"; export function TransferFolderSelector({ space, nodes, selectedFolder, onSelect, disabled }: { space: FileSpace | null; nodes: FolderNode[]; selectedFolder: string; onSelect: (folderPath: string) => void; disabled?: boolean; }) { return (
{nodes.length === 0 && No folders yet. Choose the root folder.}
); } export function TransferFolderSelectorNodes({ nodes, selectedFolder, onSelect, disabled, depth = 1 }: { nodes: FolderNode[]; selectedFolder: string; onSelect: (folderPath: string) => void; disabled?: boolean; depth?: number; }) { if (nodes.length === 0) return null; return (
{nodes.map((node) => (
))}
); } export function FolderTree({ nodes, activeSpaceId, spaceId, currentFolder, dropTargetKey = "", expandedKeys, onOpen, onToggle, onContextMenu, onDragOverTarget, onDropOnTarget, onClearDropState, onRequestDragExpand, onDragStartFolder, onDragEndFolder, dragDropEnabled = true, contextMenuEnabled = true, disabled, depth = 1 }: { nodes: FolderNode[]; activeSpaceId: string; spaceId: string; currentFolder: string; dropTargetKey?: string; expandedKeys: Set; onOpen: (spaceId: string, path: string) => void; onToggle: (spaceId: string, path: string) => void; onContextMenu?: (event: ReactMouseEvent, spaceId: string, path: string) => void; onDragOverTarget?: (event: ReactDragEvent, target: FileActionTarget) => void; onDropOnTarget?: (event: ReactDragEvent, target: FileActionTarget) => Promise; onClearDropState?: () => void; onRequestDragExpand?: (spaceId: string, path: string) => void; onDragStartFolder?: (spaceId: string, path: string, event: ReactDragEvent) => void; onDragEndFolder?: () => void; dragDropEnabled?: boolean; contextMenuEnabled?: boolean; disabled?: boolean; depth?: number; }) { return ( treeNodeKey(spaceId, node.path)} getNodeLabel={(node) => node.name} getNodeChildren={(node) => node.children} activeId={activeSpaceId === spaceId ? treeNodeKey(spaceId, currentFolder) : ""} expandedIds={expandedKeys} onOpen={(node) => onOpen(spaceId, node.path)} onToggle={(node) => onToggle(spaceId, node.path)} disabled={disabled} depth={depth} getNodeWrapClassName={(node) => { const target = { spaceId, folderPath: node.path }; return dragDropEnabled && dropTargetKey === `${target.spaceId}:${normalizeFolder(target.folderPath)}` ? "is-drop-target" : undefined; }} getNodeWrapStyle={(_node, context: ExplorerTreeNodeContext) => ({ paddingLeft: `${Math.min(context.depth * 14, 56)}px` })} getNodeDraggable={() => dragDropEnabled && !disabled} onContextMenu={contextMenuEnabled && onContextMenu ? (event, node) => onContextMenu(event, spaceId, node.path) : undefined} onDragStart={dragDropEnabled && onDragStartFolder ? (event, node) => onDragStartFolder(spaceId, node.path, event) : undefined} onDragEnd={dragDropEnabled && onDragEndFolder ? () => onDragEndFolder() : undefined} onDragOver={dragDropEnabled && onDragOverTarget ? (event, node, context) => { const target = { spaceId, folderPath: node.path }; onDragOverTarget(event, target); if (context.hasChildren && !context.expanded) onRequestDragExpand?.(spaceId, node.path); } : undefined} onDragLeave={dragDropEnabled && onClearDropState ? () => onClearDropState() : undefined} onDrop={dragDropEnabled && onDropOnTarget ? (event, node) => void onDropOnTarget(event, { spaceId, folderPath: node.path }) : undefined} /> ); } export function RenamePreviewList({ response, selectedFileIds, selectedFolderPaths, recursive, visibleCount, onShowMore, onShowFewer }: { response: RenameResponse; selectedFileIds: Set; selectedFolderPaths: Set; recursive: boolean; visibleCount: number; onShowMore: () => void; onShowFewer: () => void; }) { const selectedFolderRoots = Array.from(selectedFolderPaths).map(normalizeFolder); const hiddenNonRecursiveItems = !recursive ? response.items.filter((item) => { if (item.kind === "file" && selectedFileIds.has(item.id)) return false; if (item.kind === "folder" && selectedFolderRoots.includes(normalizeFolder(item.old_path))) return false; return selectedFolderRoots.some((folder) => isPathUnderOrSame(item.old_path, folder)); }).length : 0; const visibleItems = response.items.filter((item) => { if (recursive) return true; if (item.kind === "file") { if (selectedFileIds.has(item.id)) return true; return !selectedFolderRoots.some((folder) => isPathUnderOrSame(item.old_path, folder)); } return selectedFolderRoots.includes(normalizeFolder(item.old_path)); }); const safeVisibleCount = Math.max(20, visibleCount); const shownItems = visibleItems.slice(0, safeVisibleCount); const remaining = visibleItems.length - shownItems.length; return (
{hiddenNonRecursiveItems > 0 && {hiddenNonRecursiveItems} contained path(s) will move with the selected folder(s), but their own names will stay unchanged.} {shownItems.map((item) => {item.old_path}{item.new_path})} {remaining > 0 && ( )} {shownItems.length > 20 && ( )}
); } export function FileDialog({ title, onClose, children }: { title: string; onClose: () => void; children: ReactNode }) { return ( {children} ); } export function FileContextMenu({ menu, hasSelection, canCreateFolder, canUpload, canDownload, canOrganize, canDelete, downloadLabel, onCreateFolder, onUpload, onDownload, onMove, onCopy, onDelete }: { menu: ContextMenuState; hasSelection: boolean; canCreateFolder: boolean; canUpload: boolean; canDownload: boolean; canOrganize: boolean; canDelete: boolean; downloadLabel: string; onCreateFolder: () => void; onUpload: () => void; onDownload: () => void; onMove: () => void; onCopy: () => void; onDelete: () => void; }) { const showNewFolder = true; const showDelete = menu.target !== "empty"; const viewportWidth = typeof window === "undefined" ? 1024 : window.innerWidth; const viewportHeight = typeof window === "undefined" ? 768 : window.innerHeight; const estimatedWidth = 220; const estimatedHeight = 260; const left = Math.max(8, Math.min(menu.x, viewportWidth - estimatedWidth - 8)); const openUp = menu.y + estimatedHeight > viewportHeight; const style: CSSProperties = openUp ? { left, bottom: Math.max(8, viewportHeight - menu.y) } : { left, top: Math.max(8, menu.y) }; return (
event.stopPropagation()}> {showNewFolder && } {showDelete && }
); } export function FileConflictDialog({ state, busy, onClose, onCancel, onOverwrite, onRename, onReview, onApplyReview, onUpdateItem }: { state: ConflictDialogState; busy: boolean; onClose: () => void; onCancel: () => void; onOverwrite: () => void; onRename: () => void; onReview: () => void; onApplyReview: () => void; onUpdateItem: (id: string, patch: Partial) => void; }) { return (

{state.message}

{state.items.length} conflict(s) {state.items.length > 1 ? "Choose the same action for all conflicts or review the target names individually." : "Choose how to resolve this conflict."}
{state.review && (
{state.items.map((item) => (
{item.label} {item.kind === "folder" ? "Folder" : "File"} conflict at {item.targetPath}
onUpdateItem(item.id, { newPath: event.target.value })} disabled={busy || item.action !== "rename"} aria-label={`New path for ${item.label}`} />
))}
)} {!state.review && (
{state.items.slice(0, 8).map((item) => {item.targetPath})} {state.items.length > 8 && … and {state.items.length - 8} more}
)}
{!state.review && state.items.length > 1 && } {state.review && }
); }