From 6917e618e5f4efd77b935de369012326d4ccdb67 Mon Sep 17 00:00:00 2001 From: dtoro Date: Wed, 8 Apr 2026 11:54:31 +0200 Subject: [PATCH] fix: parent folders clickable; refresh photos when scan completes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related sidebar UX bugs. 1. Parent folders weren't clickable renderTreeItem's onClick called toggleExpanded(item.id) for any row with children — so a parent folder only expanded/collapsed, never applied its filter. Restructured: folder rows always call applyLibraryNode (which the photos endpoint already expands to include descendants), and the chevron remains a separate stopPropagation button for expansion. Other group headers (Library, Folders, Tags) still toggle expansion on row click since they have no associated filter. Result: clicking any folder at any depth filters the timeline to that folder + every descendant, matching the Lightroom model the user expects. 2. New files not appearing after Scan all folders scanLibraryMutation.onSettled invalidated ['photos'] when the trigger returned, but POST /library/scan just queues the celery task and returns immediately. By the time the worker finishes walking the directory and inserting new rows, the photos query has already refetched (with no new data) and is sitting on a 30-second staleTime — so newly-indexed photos stayed invisible until the next manual refetch. Fix: ScanProgress already polls /library/scan/status. Track the previous is_scanning value via a ref; when it transitions from true → false, invalidate ['photos'], ['folders'], ['folders', 'tree'], ['heaps'], and ['tags']. That's the actual moment new data is available, regardless of how the scan was triggered (button, watcher, startup). Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/components/ScanProgress.tsx | 40 ++++++++++++++----- .../src/components/layout/LeftSidebar.tsx | 9 ++++- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/ScanProgress.tsx b/frontend/src/components/ScanProgress.tsx index b3f7991..3aa8434 100644 --- a/frontend/src/components/ScanProgress.tsx +++ b/frontend/src/components/ScanProgress.tsx @@ -1,6 +1,6 @@ -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { FolderOpen, Loader2, Check, AlertCircle, X } from 'lucide-react' -import { useQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { library } from '../services/api' import clsx from 'clsx' @@ -15,6 +15,8 @@ interface ScanStatus { export function ScanProgress() { const [isVisible, setIsVisible] = useState(false) const [isMinimized, setIsMinimized] = useState(false) + const queryClient = useQueryClient() + const wasScanningRef = useRef(false) // Poll scan status every 2 seconds when scanning const { data: scanStatus } = useQuery({ @@ -31,18 +33,34 @@ export function ScanProgress() { }) useEffect(() => { - if (scanStatus?.is_scanning) { + const isScanning = scanStatus?.is_scanning ?? false + + if (isScanning) { setIsVisible(true) setIsMinimized(false) - } else if (isVisible && !scanStatus?.is_scanning && (scanStatus?.processed_files ?? 0) > 0) { - // Keep showing for 3 seconds after scan completes - setTimeout(() => { - if (!scanStatus?.is_scanning) { - setIsVisible(false) - } - }, 3000) + wasScanningRef.current = true + } else if (wasScanningRef.current) { + // Just transitioned from scanning → done. THIS is the right moment + // to invalidate caches that might have new data: the photos query + // (new files indexed), the folder tree (new folders walked), the + // heap counts (in case a heap photo got reattached). + wasScanningRef.current = false + queryClient.invalidateQueries({ queryKey: ['photos'] }) + queryClient.invalidateQueries({ queryKey: ['folders'] }) + queryClient.invalidateQueries({ queryKey: ['folders', 'tree'] }) + queryClient.invalidateQueries({ queryKey: ['heaps'] }) + queryClient.invalidateQueries({ queryKey: ['tags'] }) + + if (isVisible && (scanStatus?.processed_files ?? 0) > 0) { + // Keep showing for 3 seconds after scan completes + setTimeout(() => { + if (!scanStatus?.is_scanning) { + setIsVisible(false) + } + }, 3000) + } } - }, [scanStatus?.is_scanning, scanStatus?.processed_files, isVisible]) + }, [scanStatus?.is_scanning, scanStatus?.processed_files, isVisible, queryClient]) if (!isVisible || !scanStatus) return null diff --git a/frontend/src/components/layout/LeftSidebar.tsx b/frontend/src/components/layout/LeftSidebar.tsx index 893c37c..ab24f15 100644 --- a/frontend/src/components/layout/LeftSidebar.tsx +++ b/frontend/src/components/layout/LeftSidebar.tsx @@ -305,7 +305,14 @@ export function LeftSidebar() { onClick={() => { if (renamingId === item.id) return setSelectedItem(item.id) - if (hasChildren) { + // Folder rows are always filterable, parent or leaf — clicking + // anywhere on the row applies the filter and the chevron + // (separate button below) handles expansion. Other group + // headers (Library, Folders) just toggle expansion since + // they have no associated filter. + if (item.id.startsWith('folder-')) { + applyLibraryNode(item.id) + } else if (hasChildren) { toggleExpanded(item.id) } else { applyLibraryNode(item.id)