import React, {useMemo, useRef} from 'react'; import {arrayToTree} from 'performant-array-to-tree'; import {useFolders} from '../../files/queries/use-folders'; import {DriveFolder} from '../../files/drive-entry'; import {driveState, useDriveStore} from '../../drive-store'; import {FolderIcon} from '@common/icons/material/Folder'; import {getPathForFolder, RootFolderPage} from '../../drive-page/drive-page'; import {mergeProps} from '@react-aria/utils'; import { ConnectedDraggable, useDraggable, } from '@common/ui/interactions/dnd/use-draggable'; import {useSidebarTreeDropTarget} from './use-sidebar-tree-drop-target'; import {makeFolderTreeDragId} from './folder-tree-drag-id'; import {FileEntry} from '@common/uploads/file-entry'; import clsx from 'clsx'; import {BackupIcon} from '@common/icons/material/Backup'; import {TreeItem, TreeItemProps} from '@common/ui/tree/tree-item'; import {Tree} from '@common/ui/tree/tree'; import {useNavigate} from '@common/utils/hooks/use-navigate'; interface TreeFolder extends DriveFolder { children: TreeFolder[]; } export function FolderTree() { const navigate = useNavigate(); const {data} = useFolders(); const expandedKeys = useDriveStore(s => s.sidebarExpandedKeys); const activePage = useDriveStore(s => s.activePage); let selectedKeys: number[] = []; if (activePage?.isFolderPage) { selectedKeys = activePage.folder ? [activePage.folder.id] : []; } const tree = useMemo(() => { const folders = arrayToTree(data?.folders || [], { parentId: 'parent_id', dataField: null, }) as TreeFolder[]; const rootFolder = { ...RootFolderPage.folder, children: folders, }; return [rootFolder]; }, [data?.folders]); return ( { driveState().setSidebarExpandedKeys(keys); }} selectedKeys={selectedKeys} onSelectedKeysChange={([id]) => { const entryHash = findHash(id as number, tree); if (entryHash) { navigate(getPathForFolder(entryHash)); } else { navigate(RootFolderPage.path); } }} > {() => } ); } // props will be passed by tree via cloneElement function FolderTreeItem(props: Partial>) { const {node} = props as Required>; const labelRef = useRef(null); const isRootFolder = node.id === 0; const isDragging = useDriveStore(s => s.entriesBeingDragged.includes(node.id) ); const {draggableProps} = useDraggable({ type: 'fileEntry', id: makeFolderTreeDragId(node), ref: labelRef, disabled: isRootFolder, hidePreview: true, onDragStart: (e, draggable) => { const d = draggable as ConnectedDraggable; driveState().setEntriesBeingDragged(d.getData().map(e => e.id)); driveState().selectEntries([]); }, onDragEnd: () => { driveState().setEntriesBeingDragged([]); }, getData: () => [node], }); const {droppableProps, isDragOver} = useSidebarTreeDropTarget({ folder: node, ref: labelRef, }); return ( { e.preventDefault(); e.stopPropagation(); driveState().deselectEntries('all'); driveState().setContextMenuData({ x: e.clientX, y: e.clientY, entry: node, }); }} labelRef={labelRef} className={isRootFolder ? 'focus-visible:ring-2' : undefined} labelClassName={clsx( isDragOver && 'bg-primary/selected ring ring-2 ring-inset ring-primary', isDragging && 'opacity-30', isRootFolder && 'h-40' )} icon={ isRootFolder ? ( ) : ( ) } label={node.name} /> ); } const findHash = (id: number, nodes: FileEntry[]): string | undefined => { for (const item of nodes) { if (item.id === id) { return item.hash; } else if (item.children) { const hash = findHash(id, item.children); if (hash) { return hash; } } } };