diff --git a/frontend/src/components/filter/ActiveFilterChips.tsx b/frontend/src/components/filter/ActiveFilterChips.tsx index 9d144ca..d5503ff 100644 --- a/frontend/src/components/filter/ActiveFilterChips.tsx +++ b/frontend/src/components/filter/ActiveFilterChips.tsx @@ -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 (
diff --git a/frontend/src/components/layout/LeftSidebar.tsx b/frontend/src/components/layout/LeftSidebar.tsx index 67e53fa..9e0e627 100644 --- a/frontend/src/components/layout/LeftSidebar.tsx +++ b/frontend/src/components/layout/LeftSidebar.tsx @@ -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 (
diff --git a/frontend/src/hooks/useFilterUrlSync.ts b/frontend/src/hooks/useFilterUrlSync.ts index 8a9f6fb..54fb202 100644 --- a/frontend/src/hooks/useFilterUrlSync.ts +++ b/frontend/src/hooks/useFilterUrlSync.ts @@ -68,6 +68,9 @@ function parseUrl(): Partial { const heapId = sp.get('heap_id') if (heapId) out.heapId = heapId + const folderId = sp.get('folder_id') + if (folderId) out.folderId = folderId + const sortBy = sp.get('sort') if (sortBy && ALLOWED_SORT_FIELDS.includes(sortBy as SortField)) { out.sortBy = sortBy as SortField @@ -91,6 +94,7 @@ function writeUrl(f: FilterState) { if (f.colorLabel) sp.set('color_label', f.colorLabel) if (f.flag !== 'any') sp.set('flag', f.flag) if (f.heapId) sp.set('heap_id', f.heapId) + if (f.folderId) sp.set('folder_id', f.folderId) if (f.sortBy !== 'taken_at') sp.set('sort', f.sortBy) if (f.sortOrder !== 'desc') sp.set('order', f.sortOrder) diff --git a/frontend/src/hooks/usePhotosQuery.ts b/frontend/src/hooks/usePhotosQuery.ts index c8f310d..749d1c0 100644 --- a/frontend/src/hooks/usePhotosQuery.ts +++ b/frontend/src/hooks/usePhotosQuery.ts @@ -19,6 +19,7 @@ export function usePhotosQuery() { const colorLabel = useFilterStore((s) => s.colorLabel) const flag = useFilterStore((s) => s.flag) const heapId = useFilterStore((s) => s.heapId) + const folderId = useFilterStore((s) => s.folderId) const sortBy = useFilterStore((s) => s.sortBy) const sortOrder = useFilterStore((s) => s.sortOrder) @@ -33,10 +34,11 @@ export function usePhotosQuery() { colorLabel, flag, heapId, + folderId, sortBy, sortOrder, }), - [q, dateFrom, dateTo, mediaTypes, ratingMin, colorLabel, flag, heapId, sortBy, sortOrder] + [q, dateFrom, dateTo, mediaTypes, ratingMin, colorLabel, flag, heapId, folderId, sortBy, sortOrder] ) return useQuery({ diff --git a/frontend/src/store/filterStore.ts b/frontend/src/store/filterStore.ts index c27d8a7..3931304 100644 --- a/frontend/src/store/filterStore.ts +++ b/frontend/src/store/filterStore.ts @@ -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((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 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 ) }