import { useT } from "@agent-native/core/client/i18n"; import { IconChevronRight, IconFolder, IconFolderOpen, IconFolderPlus, IconTrash, IconEdit, IconDots, } from "@tabler/icons-react"; import { useMemo, useState } from "react"; import { NavLink } from "react-router"; import { toast } from "sonner"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useCreateFolder, useDeleteFolder, useRenameFolder, } from "@/hooks/use-library"; import { cn } from "@/lib/utils"; export interface FolderNode { id: string; parentId: string | null; spaceId: string | null; name: string; children?: FolderNode[]; } function buildTree(folders: FolderNode[]): FolderNode[] { const map = new Map(); for (const f of folders) { map.set(f.id, { ...f, children: [] }); } const roots: FolderNode[] = []; for (const f of folders) { const node = map.get(f.id)!; if (f.parentId && map.has(f.parentId)) { map.get(f.parentId)!.children!.push(node); } else { roots.push(node); } } return roots; } interface FolderTreeProps { folders: FolderNode[]; organizationId?: string; spaceId?: string | null; /** Build the URL for a folder — allows library or space-scoped trees. */ buildPath: (folderId: string) => string; activeFolderId?: string | null; } export function FolderTree({ folders, organizationId, spaceId = null, buildPath, activeFolderId, }: FolderTreeProps) { const t = useT(); const tree = useMemo(() => buildTree(folders), [folders]); if (folders.length === 0) { return (

{t("folderTree.noFolders")}

); } return ( ); } interface FolderItemProps { node: FolderNode; depth: number; buildPath: (folderId: string) => string; activeFolderId?: string | null; organizationId?: string; spaceId?: string | null; } function FolderItem({ node, depth, buildPath, activeFolderId, organizationId, spaceId, }: FolderItemProps) { const t = useT(); const [open, setOpen] = useState(true); const [renameOpen, setRenameOpen] = useState(false); const [renameValue, setRenameValue] = useState(node.name); const [newOpen, setNewOpen] = useState(false); const [newValue, setNewValue] = useState(""); const [confirmDelete, setConfirmDelete] = useState(false); const hasChildren = (node.children?.length ?? 0) > 0; const isActive = activeFolderId === node.id; const renameFolder = useRenameFolder(); const deleteFolder = useDeleteFolder(); const createFolder = useCreateFolder(); return (
  • {open && hasChildren ? ( ) : ( )} {node.name} { setTimeout(() => { setRenameValue(node.name); setRenameOpen(true); }, 0); }} > {" "} {t("folderTree.rename")} { setTimeout(() => { setNewValue(""); setNewOpen(true); }, 0); }} > {" "} {t("folderTree.newSubfolder")} setTimeout(() => setConfirmDelete(true), 0)} className="text-destructive" > {" "} {t("folderTree.delete")}
    { setTimeout(() => { setRenameValue(node.name); setRenameOpen(true); }, 0); }} > {t("folderTree.rename")} { setTimeout(() => { setNewValue(""); setNewOpen(true); }, 0); }} > {" "} {t("folderTree.newSubfolder")} setTimeout(() => setConfirmDelete(true), 0)} className="text-destructive" > {" "} {t("folderTree.delete")}
    {hasChildren ? (
      {node.children!.map((child) => ( ))}
    ) : null} {/* Rename dialog */} {t("folderTree.renameFolder")} setRenameValue(e.target.value)} className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary/30" /> {t("common.cancel")} { const name = renameValue.trim(); if (!name) return; renameFolder.mutate( { id: node.id, name }, { onSuccess: () => toast.success(t("folderTree.renamed")), onError: (err: any) => toast.error( err?.message ?? t("folderTree.renameFailed"), ), }, ); }} > {t("common.save")} {/* New subfolder dialog */} {t("folderTree.newSubfolder")} {t("folderTree.createInside", { name: node.name })} setNewValue(e.target.value)} placeholder={t("folderTree.folderName")} className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary/30" /> {t("common.cancel")} { const name = newValue.trim(); if (!name) return; createFolder.mutate( { name, ...(organizationId ? { organizationId } : {}), spaceId: spaceId ?? undefined, parentId: node.id, }, { onSuccess: () => toast.success(t("folderTree.created")), onError: (err: any) => toast.error( err?.message ?? t("folderTree.createFailed"), ), }, ); }} > {t("common.create")} {/* Delete confirm */} {t("folderTree.deleteFolder")} {t("folderTree.deleteDescription")} {t("common.cancel")} { deleteFolder.mutate( { id: node.id }, { onSuccess: () => toast.success(t("folderTree.deleted")), onError: (err: any) => toast.error( err?.message ?? t("folderTree.deleteFailed"), ), }, ); }} > {t("folderTree.delete")}
  • ); }