fix: parent folders clickable; refresh photos when scan completes

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 11:54:31 +02:00
parent 6985026106
commit 6917e618e5
2 changed files with 37 additions and 12 deletions

View File

@@ -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<ScanStatus>({
@@ -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

View File

@@ -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)