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