From ea8fdf236ea097ff154c9314f042916b3395c028 Mon Sep 17 00:00:00 2001 From: saltbo Date: Thu, 23 Jul 2026 10:11:26 -0400 Subject: [PATCH] fix(files): allow collapsing active folder tree branches --- src/components/layout/folder-tree.test.tsx | 96 ++++++++++++++++++++++ src/components/layout/folder-tree.tsx | 37 ++++++--- 2 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 src/components/layout/folder-tree.test.tsx diff --git a/src/components/layout/folder-tree.test.tsx b/src/components/layout/folder-tree.test.tsx new file mode 100644 index 00000000..c66bd4b3 --- /dev/null +++ b/src/components/layout/folder-tree.test.tsx @@ -0,0 +1,96 @@ +import { DirType } from '@shared/constants' +import type { StorageObject } from '@shared/types' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { cleanup, fireEvent, render, waitFor } from '@testing-library/react' +import type { ReactNode } from 'react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { listObjectsByPath } from '@/lib/api' +import { FolderTree } from './folder-tree' + +const mocks = vi.hoisted(() => ({ + currentPath: 'parent/child', +})) + +vi.mock('@tanstack/react-router', () => ({ + Link: ({ children, to, ...props }: { children: ReactNode; to: string }) => ( + + {children} + + ), + useSearch: () => ({ path: mocks.currentPath }), +})) + +vi.mock('@/lib/api', () => ({ + listObjectsByPath: vi.fn(), +})) + +function folder(id: string, name: string, parent = ''): StorageObject { + return { + id, + orgId: 'org-1', + alias: '', + name, + type: 'folder', + size: 0, + dirtype: DirType.USER_FOLDER, + parent, + object: '', + storageId: 'storage-1', + status: 'active', + trashedAt: null, + createdAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-01-01T00:00:00.000Z', + } +} + +function page(items: StorageObject[]) { + return { + items, + total: items.length, + page: 1, + pageSize: 100, + } +} + +function renderFolderTree() { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + }, + }) + + return render( + + + , + ) +} + +beforeEach(() => { + mocks.currentPath = 'parent/child' + vi.mocked(listObjectsByPath).mockImplementation(async (path) => { + if (path === '') return page([folder('parent', 'parent')]) + if (path === 'parent') return page([folder('child', 'child', 'parent')]) + return page([]) + }) +}) + +afterEach(() => { + cleanup() + vi.clearAllMocks() +}) + +describe('FolderTree', () => { + it('allows the current path ancestor to be collapsed manually', async () => { + const view = renderFolderTree() + await view.findByText('child') + const trigger = await view.findByRole('button', { name: 'parent' }) + + expect(trigger.getAttribute('aria-expanded')).toBe('true') + + fireEvent.click(trigger) + + await waitFor(() => expect(trigger.getAttribute('aria-expanded')).toBe('false')) + expect(view.queryByText('child')).toBeNull() + }) +}) diff --git a/src/components/layout/folder-tree.tsx b/src/components/layout/folder-tree.tsx index 8d6c2de0..59f4901a 100644 --- a/src/components/layout/folder-tree.tsx +++ b/src/components/layout/folder-tree.tsx @@ -3,7 +3,7 @@ import type { StorageObject } from '@shared/types' import { useQuery } from '@tanstack/react-query' import { Link, useSearch } from '@tanstack/react-router' import { ChevronRight, Folder } from 'lucide-react' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' import { SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem } from '@/components/ui/sidebar' import { listObjectsByPath } from '@/lib/api' @@ -35,7 +35,10 @@ function FolderNode({ const isActive = currentPath === folderPath const [open, setOpen] = useState(shouldAutoExpand) - const expanded = open || shouldAutoExpand + useEffect(() => { + if (isAncestorOf(folderPath, currentPath)) setOpen(true) + }, [folderPath, currentPath]) + // Always prefetch to know if this folder has children (for arrow visibility) const query = useFolders(folderPath, true) const subFolders = query.data ?? [] @@ -43,17 +46,27 @@ function FolderNode({ return ( - + - - e.preventDefault()} disabled={!hasChildren}> - - - - {folder.name} - +
+ {hasChildren ? ( + + + + ) : ( + + )} + + + {folder.name} + +
{subFolders.length > 0 && (