feat: folder navigation from sidebar

Folders in the LeftSidebar were decorative — clicking one did
nothing. Now they actually filter the timeline.

filterStore: new folderId field, setFolderId, hasActiveFilters check,
filtersToParams sends folder_id to the backend (the param was already
declared and applied server-side, just nothing was setting it).
useFilterUrlSync round-trips ?folder_id= so the filter persists in
the URL. usePhotosQuery threads it through.

LeftSidebar:
- Clicking a folder row calls clearAllFilters() then setFolderId(id)
  so the user lands cleanly on that folder.
- Library virtual nodes (All Photos, Rated, Discarded) clear the
  folder filter as part of their normal action.
- The active-row visual highlight is now derived from the filter
  store: a folder row is selected when filterStore.folderId matches
  it, "All Photos" is selected when no folder is set. Keeps the
  sidebar in sync if filters change externally (URL hydrate, the
  ActiveFilterChips X button, FilterBar Clear all).

ActiveFilterChips: shows "Folder: {name}" and "Heap: {name}" chips,
looking up the names from the folders / heaps queries (lazy-enabled
only when the corresponding filter is set). Clicking the X clears
the filter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 23:35:59 +02:00
parent 61486a503e
commit 66ffd94c48
5 changed files with 76 additions and 4 deletions

View File

@@ -1,9 +1,29 @@
import { X } from 'lucide-react'
import { useQuery } from '@tanstack/react-query'
import { useFilterStore, hasActiveFilters } from '../../store/filterStore'
import { sourceFolders, heaps as heapsApi } from '../../services/api'
export function ActiveFilterChips() {
const f = useFilterStore()
// Look up names for id-based filters so the chips show something
// human-readable instead of opaque uuids.
const { data: foldersData } = useQuery({
queryKey: ['folders'],
queryFn: sourceFolders.list,
enabled: f.folderId !== null,
})
const folder = f.folderId
? (foldersData?.folders ?? []).find((x: any) => x.id === f.folderId)
: null
const { data: heaps = [] } = useQuery({
queryKey: ['heaps'],
queryFn: heapsApi.list,
enabled: f.heapId !== null,
})
const heap = f.heapId ? heaps.find((h) => h.id === f.heapId) : null
if (!hasActiveFilters(f)) return null
const chips: { key: string; label: string; onRemove: () => void }[] = []
@@ -57,6 +77,20 @@ export function ActiveFilterChips() {
onRemove: () => f.setFlag('any'),
})
}
if (f.folderId) {
chips.push({
key: 'folder',
label: `Folder: ${folder?.name || folder?.path?.split('/').pop() || f.folderId}`,
onRemove: () => f.setFolderId(null),
})
}
if (f.heapId) {
chips.push({
key: 'heap',
label: `Heap: ${heap?.name ?? f.heapId}`,
onRemove: () => f.setHeapId(null),
})
}
return (
<div className="flex flex-wrap items-center gap-2 border-b border-border bg-surface-2 px-4 py-2 text-xs">

View File

@@ -39,6 +39,8 @@ export function LeftSidebar() {
const clearAllFilters = useFilterStore((s) => s.clearAll)
const setRatingMin = useFilterStore((s) => s.setRatingMin)
const setFlag = useFilterStore((s) => s.setFlag)
const setFolderId = useFilterStore((s) => s.setFolderId)
const filterFolderId = useFilterStore((s) => s.folderId)
// Map a library tree id to a filter-store mutation. Each "virtual node" in
// the library tree is just a saved filter preset.
@@ -55,7 +57,16 @@ export function LeftSidebar() {
clearAllFilters()
setFlag('discarded')
break
// 'by-date' is purely visual until we add a date-grouping UI
default:
if (id.startsWith('folder-')) {
// Folder rows: filter to that folder, clear other filters that
// would compete (heap, discarded, etc.) so the user sees what they
// expect when they click a folder.
const folderId = id.slice('folder-'.length)
clearAllFilters()
setFolderId(folderId)
}
// 'by-date' is still visual-only.
}
}
@@ -150,10 +161,24 @@ export function LeftSidebar() {
},
]
// Derive whether a tree item is currently the "active" filter target.
// Folder rows are selected when the filter store's folderId matches; the
// library "All Photos" virtual node is selected when no folder/heap filter
// is set.
const isItemActive = (id: string): boolean => {
if (id.startsWith('folder-')) {
return filterFolderId === id.slice('folder-'.length)
}
if (id === 'all-photos') {
return filterFolderId === null && selectedItem === 'all-photos'
}
return selectedItem === id
}
const renderTreeItem = (item: TreeItem, depth: number = 0) => {
const hasChildren = item.children && item.children.length > 0
const isExpanded = expandedItems.has(item.id)
const isSelected = selectedItem === item.id
const isSelected = isItemActive(item.id)
return (
<div key={item.id}>