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

@@ -22,6 +22,8 @@ export interface FilterState {
/** When set, restrict to photos in this heap. Independent of `activeHeapId`
* on the heap store — that's the target for the T shortcut. */
heapId: string | null
/** When set, restrict to photos in this folder. */
folderId: string | null
sortBy: SortField
sortOrder: SortOrder
}
@@ -37,6 +39,7 @@ interface FilterStore extends FilterState {
setColorLabel: (label: ColorLabel | null) => void
setFlag: (flag: FlagFilter) => void
setHeapId: (id: string | null) => void
setFolderId: (id: string | null) => void
setSortBy: (field: SortField) => void
setSortOrder: (order: SortOrder) => void
toggleSortOrder: () => void
@@ -57,6 +60,7 @@ export const INITIAL_FILTERS: FilterState = {
colorLabel: null,
flag: 'any',
heapId: null,
folderId: null,
sortBy: 'taken_at',
sortOrder: 'desc',
}
@@ -78,6 +82,7 @@ export const useFilterStore = create<FilterStore>((set) => ({
setColorLabel: (colorLabel) => set({ colorLabel }),
setFlag: (flag) => set({ flag }),
setHeapId: (heapId) => set({ heapId }),
setFolderId: (folderId) => set({ folderId }),
setSortBy: (sortBy) => set({ sortBy }),
setSortOrder: (sortOrder) => set({ sortOrder }),
toggleSortOrder: () =>
@@ -102,6 +107,7 @@ export function filtersToParams(f: FilterState): Record<string, string | number>
if (f.colorLabel) params.color_label = f.colorLabel
if (f.flag === 'discarded') params.is_discarded = 'true'
if (f.heapId) params.heap_id = f.heapId
if (f.folderId) params.folder_id = f.folderId
params.sort = f.sortBy
params.order = f.sortOrder
return params
@@ -117,6 +123,7 @@ export function hasActiveFilters(f: FilterState): boolean {
f.ratingMin > 0 ||
f.colorLabel !== null ||
f.flag !== 'any' ||
f.heapId !== null
f.heapId !== null ||
f.folderId !== null
)
}