import React, { CSSProperties, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import useFloatingMenu from '../ui/useFloatingMenu'; import { ArrowLeftIcon, FolderIcon, FolderMoveIcon, } from '../ui/icons'; import type { FolderTreeNode } from '../lib/apiTypes'; import type { DocumentId } from '../types/identifiers'; export interface SelectionFolderMenuProps { label: React.ReactNode; folderTree?: FolderTreeNode[]; onSelectFolder?: (folderId: DocumentId | null) => Promise | void; disabled?: boolean; className?: string; triggerContent?: React.ReactNode; triggerClassName?: string; placeholder?: string; emptyMessage?: string; onOpenMenu?: () => void; positionStrategy?: 'absolute' | 'fixed'; rootTitle?: string; } const SelectionFolderMenu: React.FC = ({ label, folderTree = [], onSelectFolder, disabled = false, className, triggerContent = null, triggerClassName = 'quick-add__chip quick-add__trigger panel-floating-actions__trigger', placeholder = 'Search folders…', emptyMessage = 'No folders', onOpenMenu, positionStrategy = 'absolute', rootTitle = 'Folders', }) => { const anchorRef = useRef(null); const inputRef = useRef(null); const [query, setQuery] = useState(''); const [currentFolderId, setCurrentFolderId] = useState(null); const [pending, setPending] = useState(false); const { isOpen, toggle, close, menuRef, menuStyle, updatePosition, } = useFloatingMenu({ anchorRef, align: 'center', positionStrategy, minWidth: 260, }) as { isOpen: boolean; toggle: () => void; close: () => void; menuRef: React.MutableRefObject; menuStyle: CSSProperties | null; updatePosition: () => void; }; useEffect(() => { if (disabled && isOpen) { close(); } }, [disabled, isOpen, close]); useEffect(() => { if (!isOpen) { return undefined; } setQuery(''); setCurrentFolderId(null); setPending(false); const frame = requestAnimationFrame(() => { updatePosition(); if (inputRef.current) { inputRef.current.focus(); } }); return () => cancelAnimationFrame(frame); }, [isOpen, updatePosition]); // Build a flat map for easy lookup const { nodeMap, parentMap } = useMemo(() => { const nMap = new Map(); const pMap = new Map(); const traverse = (nodes: FolderTreeNode[], parentId: DocumentId | null) => { nodes.forEach((node) => { nMap.set(node.id, node); if (parentId) { pMap.set(node.id, parentId); } if (node.children) { traverse(node.children, node.id); } }); }; traverse(folderTree, null); return { nodeMap: nMap, parentMap: pMap }; }, [folderTree]); const currentChildren = useMemo(() => { const currentFolder = currentFolderId ? nodeMap.get(currentFolderId) : null; return currentFolder ? currentFolder.children || [] : folderTree; }, [currentFolderId, nodeMap, folderTree]); const currentFolder = currentFolderId ? nodeMap.get(currentFolderId) : null; // Filter items based on search query // If searching, we might want to show flattened results matching the query? // Or just filter current level? // Usually, search implies searching the whole tree. const isSearching = query.trim().length > 0; const displayedItems = useMemo(() => { if (isSearching) { const search = query.trim().toLowerCase(); const results: FolderTreeNode[] = []; nodeMap.forEach((node) => { if (node.name.toLowerCase().includes(search)) { results.push(node); } }); return results; } return currentChildren; }, [isSearching, query, currentChildren, nodeMap]); const handleTriggerClick = useCallback(() => { if (disabled) { return; } if (!isOpen) { onOpenMenu?.(); } toggle(); }, [disabled, isOpen, onOpenMenu, toggle]); const handleSelect = useCallback( async (folderId: DocumentId | null) => { if (!onSelectFolder) return; setPending(true); try { await onSelectFolder(folderId); close(); } catch (error) { console.error('Failed to move to folder', error); } finally { setPending(false); } }, [onSelectFolder, close] ); const handleNavigate = (folderId: DocumentId) => { setCurrentFolderId(folderId); setQuery(''); // Clear search on navigation if (inputRef.current) { inputRef.current.focus(); } }; const handleUp = () => { if (!currentFolderId) return; const parentId = parentMap.get(currentFolderId) || null; setCurrentFolderId(parentId); }; return (
{isOpen ? (
{/* Search Bar */}
setQuery(event.target.value)} placeholder={placeholder} aria-label={placeholder} disabled={pending} />
{/* Navigation Header (only if not searching) */} {!isSearching && (
{currentFolderId ? ( ) : null} {currentFolder ? currentFolder.name : rootTitle}
)}
{displayedItems.length ? ( displayedItems.map((item) => { const hasChildren = item.children && item.children.length > 0; return (
{/* Clickable area to navigate down */} {/* Move Button for this specific folder */}
); }) ) : (
{emptyMessage}
)}
) : null}
); }; export default SelectionFolderMenu;