feat: real recursive folder tree in the sidebar

The Folders section in the LeftSidebar previously rendered the flat
list of source roots — actual subdirectories were invisible. Now it
shows the full nested tree, click any node to filter, drop targets
work at every depth.

Backend
- New GET /folders/tree returning a list of root nodes (one per
  active SourceRoot). Each node is { id, name, path, photo_count,
  children: [...] } with children sorted alphabetically at every
  level. Walks Folder rows whose source_root_id matches and whose
  path is at or beneath the source root, then attaches them by
  parent path so partial scans don't break the tree.
- The source root's display label is overlaid on the root folder
  node so the top-level entry reads as "Library" instead of
  "/photos".
- list_photos folder_id filter now does descendant matching: when
  a Folder id is given, it includes the folder itself and every
  Folder whose path is a sep-prefixed descendant. Matches the
  Lightroom mental model: clicking "Library" or any parent folder
  shows everything beneath it. The existing source-root-id branch
  is unchanged.

Frontend
- New types/api.ts FolderTreeNode interface and sourceFolders.tree()
  helper.
- New hooks/useFolderTreeQuery.ts with a 30s staleTime and a
  findFolderInTree() walker for id-based name lookups.
- LeftSidebar drops the flat foldersData list and uses the tree
  query. folderNodeToTreeItem recursively maps backend nodes into
  the existing TreeItem shape; renderTreeItem already knew how to
  recurse into children, so the tree just works at any depth.
  Drop targets, drag-to-move, drag-to-copy, double-click rename,
  and active-state highlighting all carry over to nested folders.
- The renameMutation now also invalidates ['folders', 'tree'] so a
  source-root rename refreshes the tree label immediately.
- ActiveFilterChips switches to the tree query and uses the new
  findFolderInTree walker so the chip label resolves correctly for
  sub-folder filters too — not just top-level source roots.
- The "Scan all folders" button visibility now keys off the tree
  length instead of the flat folders length.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 11:36:01 +02:00
parent bb7c2b12d6
commit 914eb58ac5
6 changed files with 169 additions and 33 deletions

View File

@@ -12,12 +12,26 @@ const api = axios.create({
// Source Folders API. Source roots are config-driven now (PHOTO_DIRS in
// .env → bootstrap on backend startup), so the UI only reads them and
// optionally renames the display label.
export interface FolderTreeNode {
id: string
name: string
path: string
photo_count: number
children: FolderTreeNode[]
}
export const sourceFolders = {
list: async () => {
const response = await api.get('/folders')
return response.data
},
/** Recursive folder tree, one root per active source root. */
tree: async (): Promise<FolderTreeNode[]> => {
const response = await api.get('/folders/tree')
return response.data
},
scan: async (folderId: string) => {
const response = await api.post(`/folders/${folderId}/scan`)
return response.data