diff --git a/components/files/file-browser.tsx b/components/files/file-browser.tsx index 8a47b7d32..b9cdcb867 100644 --- a/components/files/file-browser.tsx +++ b/components/files/file-browser.tsx @@ -11,7 +11,7 @@ import { AlertCircle, Star, Clock, FolderUp, FileArchive, FileSpreadsheet, Presentation, FileCode, Box, PenTool, Terminal as TerminalIcon, Database, Type as TypeIcon, - Menu, Users, Share2, SquarePen, + Menu, Users, Share2, SquarePen, Link, } from "@/components/icons"; import { useIsDesktop } from "@/hooks/use-media-query"; import { Button } from "@/components/ui/button"; @@ -239,17 +239,28 @@ function getFileIconByName(name: string, size: "sm" | "lg") { return ; } -function getFileIcon(resource: FileResource) { +// A symlink's icon, whatever its name suggests: it has no content to open. +function SymlinkIcon({ className, label }: { className: string; label: string }) { + return ( + + {label} + + ); +} + +function getFileIcon(resource: FileResource, symlinkLabel: string) { if (resource.isDirectory) { return ; } + if (resource.isSymlink) return ; return getFileIconByName(resource.name, "sm"); } -function getGridIcon(resource: FileResource) { +function getGridIcon(resource: FileResource, symlinkLabel: string) { if (resource.isDirectory) { return ; } + if (resource.isSymlink) return ; return getFileIconByName(resource.name, "lg"); } @@ -418,8 +429,15 @@ export function FileBrowser({ // it (not a shared-with-me node), and holds the mayShare right (owned nodes // report full rights; treat missing myRights as allowed). const canShare = useCallback((r: FileResource | null | undefined): boolean => - !!(sharingEnabled && onShare && client && r && !r.isShared && (r.myRights?.mayShare ?? true)), + !!(sharingEnabled && onShare && client && r && !r.isShared && !r.isSymlink && (r.myRights?.mayShare ?? true)), [sharingEnabled, onShare, client]); + // Files have content to preview, open, download or duplicate. Folders and + // symlinks don't: a symlink row only supports rename, move and delete. + const hasContent = useCallback((name: string): boolean => { + const r = resources.find(res => res.name === name); + return !!r && !r.isDirectory && !r.isSymlink; + }, [resources]); + const symlinkLabel = t("symlink"); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; name: string } | null>(null); const [emptyContextMenu, setEmptyContextMenu] = useState<{ x: number; y: number } | null>(null); const [showNewTextFile, setShowNewTextFile] = useState(false); @@ -700,6 +718,7 @@ export function FileBrowser({ }, [marquee, onSetSelection]); const handleResourceDoubleClick = (resource: FileResource) => { + if (resource.isSymlink) return; if (resource.isDirectory) { const newPath = currentPath === '/' ? `/${resource.name}` @@ -929,7 +948,7 @@ export function FileBrowser({ ? `/${resource.name}` : `${currentPath}/${resource.name}`; onNavigate(newPath, resource.id); - } else if (resource) { + } else if (resource && !resource.isSymlink) { onDownload(resource.name); } return; @@ -1003,10 +1022,10 @@ export function FileBrowser({ variant="ghost" size="sm" className="h-8" - onClick={() => onBatchDownload([...selectedResources].filter(n => !resources.find(r => r.name === n)?.isDirectory))} + onClick={() => onBatchDownload([...selectedResources].filter(hasContent))} > - {t("download")} ({[...selectedResources].filter(n => !resources.find(r => r.name === n)?.isDirectory).length}) + {t("download")} ({[...selectedResources].filter(hasContent).length}) )} - {!resources.find(r => r.name === contextMenu.name)?.isDirectory && isOfficeEditable?.(contextMenu.name) && onEditFile && ( + {hasContent(contextMenu.name) && isOfficeEditable?.(contextMenu.name) && onEditFile && ( )} - {!resources.find(r => r.name === contextMenu.name)?.isDirectory && ( + {hasContent(contextMenu.name) && ( - + {!resources.find(r => r.name === contextMenu.name)?.isSymlink && ( + + )} {clipboard && (