diff --git a/backend/app/routers/heaps.py b/backend/app/routers/heaps.py
index 85e10cc..7ce7fa5 100644
--- a/backend/app/routers/heaps.py
+++ b/backend/app/routers/heaps.py
@@ -1,27 +1,191 @@
"""
Heaps API router
"""
+from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
-from sqlalchemy import select
+from pydantic import BaseModel
+from sqlalchemy import select, func, update, insert, delete
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models import Heap
+from app.models.heaps import heap_photos
router = APIRouter()
+
+# ── Schemas ───────────────────────────────────────────────────────────────
+
+class HeapCreate(BaseModel):
+ name: str
+
+
+class HeapUpdate(BaseModel):
+ name: Optional[str] = None
+ is_active: Optional[bool] = None
+
+
+class HeapPhotosBody(BaseModel):
+ photo_ids: list[str]
+
+
+# ── Endpoints ─────────────────────────────────────────────────────────────
+
@router.get("")
async def list_heaps(db: AsyncSession = Depends(get_db)):
- """List all heaps"""
- result = await db.execute(select(Heap))
- heaps = result.scalars().all()
- return heaps
+ """List all heaps with photo counts."""
+ # LEFT JOIN heap_photos and group so we can return counts in one query.
+ count_subq = (
+ select(
+ heap_photos.c.heap_id,
+ func.count(heap_photos.c.photo_id).label("photo_count"),
+ )
+ .group_by(heap_photos.c.heap_id)
+ .subquery()
+ )
-@router.post("")
-async def create_heap(name: str, db: AsyncSession = Depends(get_db)):
- """Create a new heap"""
+ stmt = (
+ select(Heap, count_subq.c.photo_count)
+ .outerjoin(count_subq, Heap.id == count_subq.c.heap_id)
+ .order_by(Heap.created_at.asc())
+ )
+ result = await db.execute(stmt)
+ rows = result.all()
+
+ return [
+ {
+ "id": h.id,
+ "name": h.name,
+ "is_active": bool(h.is_active),
+ "created_at": h.created_at,
+ "updated_at": h.updated_at,
+ "photo_count": int(count or 0),
+ }
+ for h, count in rows
+ ]
+
+
+@router.post("", status_code=201)
+async def create_heap(body: HeapCreate, db: AsyncSession = Depends(get_db)):
+ """Create a new heap."""
+ name = (body.name or "").strip()
+ if not name:
+ raise HTTPException(status_code=400, detail="Heap name is required")
heap = Heap(name=name)
db.add(heap)
await db.commit()
await db.refresh(heap)
- return heap
\ No newline at end of file
+ return {
+ "id": heap.id,
+ "name": heap.name,
+ "is_active": bool(heap.is_active),
+ "created_at": heap.created_at,
+ "updated_at": heap.updated_at,
+ "photo_count": 0,
+ }
+
+
+@router.patch("/{heap_id}")
+async def update_heap(
+ heap_id: str, body: HeapUpdate, db: AsyncSession = Depends(get_db)
+):
+ """Rename a heap and/or toggle active state. Setting is_active=true on
+ one heap deactivates all others (single-active invariant)."""
+ result = await db.execute(select(Heap).where(Heap.id == heap_id))
+ heap = result.scalar_one_or_none()
+ if not heap:
+ raise HTTPException(status_code=404, detail="Heap not found")
+
+ if body.name is not None:
+ name = body.name.strip()
+ if not name:
+ raise HTTPException(status_code=400, detail="Heap name is required")
+ heap.name = name
+
+ if body.is_active is not None:
+ if body.is_active:
+ # Clear active flag on all other heaps in one statement
+ await db.execute(update(Heap).values(is_active=False))
+ heap.is_active = True
+ else:
+ heap.is_active = False
+
+ await db.commit()
+ await db.refresh(heap)
+ return {
+ "id": heap.id,
+ "name": heap.name,
+ "is_active": bool(heap.is_active),
+ "created_at": heap.created_at,
+ "updated_at": heap.updated_at,
+ }
+
+
+@router.delete("/{heap_id}", status_code=204)
+async def delete_heap(heap_id: str, db: AsyncSession = Depends(get_db)):
+ """Delete a heap. Photos themselves are unaffected — only the membership
+ rows in heap_photos cascade-delete."""
+ result = await db.execute(select(Heap).where(Heap.id == heap_id))
+ heap = result.scalar_one_or_none()
+ if not heap:
+ raise HTTPException(status_code=404, detail="Heap not found")
+ await db.delete(heap)
+ await db.commit()
+ return None
+
+
+@router.post("/{heap_id}/photos")
+async def add_photos_to_heap(
+ heap_id: str, body: HeapPhotosBody, db: AsyncSession = Depends(get_db)
+):
+ """Add photos to a heap. Idempotent: re-adding existing members is a
+ no-op (handled by an INSERT OR IGNORE-style filter on duplicates)."""
+ result = await db.execute(select(Heap).where(Heap.id == heap_id))
+ heap = result.scalar_one_or_none()
+ if not heap:
+ raise HTTPException(status_code=404, detail="Heap not found")
+
+ if not body.photo_ids:
+ return {"status": "success", "added": 0}
+
+ # Find which ids are already members so we don't violate the PK.
+ existing = await db.execute(
+ select(heap_photos.c.photo_id).where(
+ heap_photos.c.heap_id == heap_id,
+ heap_photos.c.photo_id.in_(body.photo_ids),
+ )
+ )
+ existing_ids = {row[0] for row in existing.all()}
+ new_ids = [pid for pid in body.photo_ids if pid not in existing_ids]
+
+ if new_ids:
+ await db.execute(
+ insert(heap_photos),
+ [{"heap_id": heap_id, "photo_id": pid} for pid in new_ids],
+ )
+ await db.commit()
+
+ return {"status": "success", "added": len(new_ids), "already_present": len(existing_ids)}
+
+
+@router.delete("/{heap_id}/photos")
+async def remove_photos_from_heap(
+ heap_id: str, body: HeapPhotosBody, db: AsyncSession = Depends(get_db)
+):
+ """Remove photos from a heap. Removing a non-member is a no-op."""
+ result = await db.execute(select(Heap).where(Heap.id == heap_id))
+ heap = result.scalar_one_or_none()
+ if not heap:
+ raise HTTPException(status_code=404, detail="Heap not found")
+
+ if not body.photo_ids:
+ return {"status": "success", "removed": 0}
+
+ res = await db.execute(
+ delete(heap_photos).where(
+ heap_photos.c.heap_id == heap_id,
+ heap_photos.c.photo_id.in_(body.photo_ids),
+ )
+ )
+ await db.commit()
+ return {"status": "success", "removed": res.rowcount or 0}
diff --git a/backend/app/routers/photos.py b/backend/app/routers/photos.py
index 0b963b7..bb17f15 100644
--- a/backend/app/routers/photos.py
+++ b/backend/app/routers/photos.py
@@ -16,6 +16,7 @@ logger = logging.getLogger(__name__)
from app.database import get_db
from app.models import Photo, Folder, Tag, PhotoTag
+from app.models.heaps import heap_photos
from app.schemas.photos import PhotoResponse, PhotoUpdate, PhotoListResponse, BulkAction
from app.config import settings
@@ -95,7 +96,15 @@ async def list_photos(
# Discard filter — defaults to hiding discarded photos
filters.append(Photo.is_discarded == is_discarded)
-
+
+ # Heap membership filter — restrict to photos that belong to the heap.
+ if heap_id:
+ filters.append(
+ Photo.id.in_(
+ select(heap_photos.c.photo_id).where(heap_photos.c.heap_id == heap_id)
+ )
+ )
+
# Apply all filters
if filters:
query = query.where(and_(*filters))
diff --git a/frontend/src/components/KeyboardHints.tsx b/frontend/src/components/KeyboardHints.tsx
index d182398..90a98f5 100644
--- a/frontend/src/components/KeyboardHints.tsx
+++ b/frontend/src/components/KeyboardHints.tsx
@@ -13,6 +13,7 @@ export function KeyboardHints() {
{ key: '1-5', action: 'Rate' },
{ key: 'P', action: 'Pick' },
{ key: 'X', action: 'Discard' },
+ { key: 'T', action: 'Add to heap' },
{ key: 'Space', action: 'Preview' },
{ key: 'Esc', action: 'Deselect' },
]
diff --git a/frontend/src/components/heaps/HeapsPanel.tsx b/frontend/src/components/heaps/HeapsPanel.tsx
new file mode 100644
index 0000000..664b575
--- /dev/null
+++ b/frontend/src/components/heaps/HeapsPanel.tsx
@@ -0,0 +1,224 @@
+import { useState } from 'react'
+import {
+ ShoppingBasket,
+ Plus,
+ Target,
+ X,
+ ChevronDown,
+ ChevronRight,
+} from 'lucide-react'
+import clsx from 'clsx'
+import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { useHeapsQuery, HEAPS_QUERY_KEY } from '../../hooks/useHeapsQuery'
+import { heaps as heapsApi } from '../../services/api'
+import { useFilterStore } from '../../store/filterStore'
+import { toast } from '../ToastContainer'
+
+/**
+ * Heaps panel for the left sidebar. Renders the list of heaps with the
+ * basket icon, lets the user create a new heap, click one to filter the
+ * timeline to its contents, set one as the "active" target for the T
+ * shortcut, and delete heaps.
+ *
+ * Heap state:
+ * - filter heapId: which heap is currently filtered to (visual)
+ * - heap.is_active: which heap T adds to (server-side, single per row)
+ */
+export function HeapsPanel() {
+ const { data: heaps = [] } = useHeapsQuery()
+ const filterHeapId = useFilterStore((s) => s.heapId)
+ const setFilterHeapId = useFilterStore((s) => s.setHeapId)
+ const queryClient = useQueryClient()
+
+ const [expanded, setExpanded] = useState(true)
+ const [creating, setCreating] = useState(false)
+ const [newName, setNewName] = useState('')
+
+ const invalidate = () => {
+ queryClient.invalidateQueries({ queryKey: HEAPS_QUERY_KEY })
+ }
+
+ const createMutation = useMutation({
+ mutationFn: (name: string) => heapsApi.create(name),
+ onSuccess: () => {
+ invalidate()
+ setNewName('')
+ setCreating(false)
+ },
+ onError: (e: any) =>
+ toast.error('Failed to create heap', e.message || 'Unknown error'),
+ })
+
+ const setActiveMutation = useMutation({
+ mutationFn: (heapId: string) =>
+ heapsApi.update(heapId, { is_active: true }),
+ onSuccess: (heap) => {
+ invalidate()
+ toast.success('Active heap', `Now adding to "${heap.name}" with T`)
+ },
+ onError: (e: any) =>
+ toast.error('Failed to set active', e.message || 'Unknown error'),
+ })
+
+ const deleteMutation = useMutation({
+ mutationFn: (heapId: string) => heapsApi.delete(heapId),
+ onSuccess: (_, heapId) => {
+ invalidate()
+ // If we were filtering by this heap, clear the filter
+ if (filterHeapId === heapId) setFilterHeapId(null)
+ },
+ onError: (e: any) =>
+ toast.error('Failed to delete heap', e.message || 'Unknown error'),
+ })
+
+ const handleCreate = () => {
+ const name = newName.trim()
+ if (!name) return
+ createMutation.mutate(name)
+ }
+
+ return (
+
+ {/* Section header */}
+
setExpanded((v) => !v)}
+ >
+
+
+
Heaps
+
+
+
+ {expanded && (
+
+ {/* Inline create form */}
+ {creating && (
+
+ setNewName(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') handleCreate()
+ if (e.key === 'Escape') {
+ setCreating(false)
+ setNewName('')
+ }
+ }}
+ placeholder="Heap name"
+ className="flex-1 rounded border border-border bg-bg px-2 py-0.5 text-xs text-text focus:border-primary focus:outline-none"
+ />
+
+
+ )}
+
+ {heaps.length === 0 && !creating && (
+
+ No heaps yet
+
+ )}
+
+ {heaps.map((heap) => {
+ const isFiltered = filterHeapId === heap.id
+ const isActive = heap.is_active
+ return (
+
setFilterHeapId(heap.id)}
+ >
+
+
+ {heap.name}
+
+ {isActive && (
+
+ )}
+ {heap.photo_count > 0 && (
+
+ {heap.photo_count}
+
+ )}
+
+
+
+ )
+ })}
+
+ )}
+
+ )
+}
diff --git a/frontend/src/components/layout/LeftSidebar.tsx b/frontend/src/components/layout/LeftSidebar.tsx
index 15eaa74..84ec8ef 100644
--- a/frontend/src/components/layout/LeftSidebar.tsx
+++ b/frontend/src/components/layout/LeftSidebar.tsx
@@ -19,6 +19,7 @@ import { sourceFolders, library } from '../../services/api'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from '../ToastContainer'
import { useFilterStore } from '../../store/filterStore'
+import { HeapsPanel } from '../heaps/HeapsPanel'
interface TreeItem {
id: string
@@ -153,12 +154,6 @@ export function LeftSidebar() {
type: 'folder',
})) || [],
},
- {
- id: 'heaps',
- label: 'Heaps',
- icon: ,
- children: [], // Will be populated from API
- },
]
const renderTreeItem = (item: TreeItem, depth: number = 0) => {
@@ -257,6 +252,7 @@ export function LeftSidebar() {
{/* Tree View */}
{libraryTree.map((item) => renderTreeItem(item))}
+
{/* Bottom Actions */}
diff --git a/frontend/src/components/layout/TopBar.tsx b/frontend/src/components/layout/TopBar.tsx
index fc73b25..41ddaac 100644
--- a/frontend/src/components/layout/TopBar.tsx
+++ b/frontend/src/components/layout/TopBar.tsx
@@ -10,6 +10,7 @@ import {
Menu,
Trash2,
X,
+ ShoppingBasket,
} from 'lucide-react'
import clsx from 'clsx'
import { usePhotoStore } from '../../store/photoStore'
@@ -17,6 +18,7 @@ import { useFilterStore, hasActiveFilters } from '../../store/filterStore'
import { photos } from '../../services/api'
import { toast } from '../ToastContainer'
import { useMutation, useQueryClient } from '@tanstack/react-query'
+import { useHeapsQuery } from '../../hooks/useHeapsQuery'
import muliLogo from '../../assets/muli-logo.png'
const SEARCH_DEBOUNCE_MS = 300
@@ -57,6 +59,11 @@ export function TopBar() {
const clearSelection = usePhotoStore((state) => state.clearSelection)
const selectedCount = selectedPhotos.length
const queryClient = useQueryClient()
+
+ // Currently active heap (for the T shortcut). Shown as a pill so the user
+ // always knows where their next T-press will land.
+ const { data: heapsList = [] } = useHeapsQuery()
+ const activeHeap = heapsList.find((h) => h.is_active)
// Mutation for discarding selected photos
const discardPhotosMutation = useMutation({
@@ -90,6 +97,15 @@ export function TopBar() {
Mulita
+ {activeHeap && (
+
+
+ {activeHeap.name}
+
+ )}
{selectedCount > 0 && (
<>
diff --git a/frontend/src/hooks/useFilterUrlSync.ts b/frontend/src/hooks/useFilterUrlSync.ts
index 978ac37..c165999 100644
--- a/frontend/src/hooks/useFilterUrlSync.ts
+++ b/frontend/src/hooks/useFilterUrlSync.ts
@@ -55,6 +55,9 @@ function parseUrl(): Partial {
out.flag = flag as FlagFilter
}
+ const heapId = sp.get('heap_id')
+ if (heapId) out.heapId = heapId
+
return out
}
@@ -67,6 +70,7 @@ function writeUrl(f: FilterState) {
if (f.ratingMin > 0) sp.set('rating_min', String(f.ratingMin))
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)
const search = sp.toString()
const next = search ? `?${search}` : window.location.pathname
diff --git a/frontend/src/hooks/useHeapsQuery.ts b/frontend/src/hooks/useHeapsQuery.ts
new file mode 100644
index 0000000..18f76de
--- /dev/null
+++ b/frontend/src/hooks/useHeapsQuery.ts
@@ -0,0 +1,12 @@
+import { useQuery } from '@tanstack/react-query'
+import { heaps as heapsApi, type Heap } from '../services/api'
+
+export const HEAPS_QUERY_KEY = ['heaps'] as const
+
+export function useHeapsQuery() {
+ return useQuery({
+ queryKey: HEAPS_QUERY_KEY,
+ queryFn: heapsApi.list,
+ staleTime: 30_000,
+ })
+}
diff --git a/frontend/src/hooks/useKeyboardShortcuts.ts b/frontend/src/hooks/useKeyboardShortcuts.ts
index a043186..81c306b 100644
--- a/frontend/src/hooks/useKeyboardShortcuts.ts
+++ b/frontend/src/hooks/useKeyboardShortcuts.ts
@@ -2,7 +2,9 @@ import { useHotkeys } from 'react-hotkeys-hook'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { usePhotoStore } from '../store/photoStore'
import { useFilterStore } from '../store/filterStore'
-import { photos as photosApi } from '../services/api'
+import { photos as photosApi, heaps as heapsApi, type Heap } from '../services/api'
+import { HEAPS_QUERY_KEY } from './useHeapsQuery'
+import { toast } from '../components/ToastContainer'
interface KeyboardShortcutsProps {
onToggleLeftSidebar: () => void
@@ -60,6 +62,52 @@ export function useKeyboardShortcuts(props: KeyboardShortcutsProps) {
updateMutation.mutate({ id, data })
}
+ // T key: add the current selection to the active heap. If no heap is
+ // active or no photos are selected, it's a no-op with a toast hint.
+ const addToHeapMutation = useMutation({
+ mutationFn: ({ heapId, photoIds }: { heapId: string; photoIds: string[] }) =>
+ heapsApi.addPhotos(heapId, photoIds),
+ onSuccess: (data, vars) => {
+ const added = data?.added ?? 0
+ const already = data?.already_present ?? 0
+ const heap = (queryClient.getQueryData(HEAPS_QUERY_KEY) ?? []).find(
+ (h) => h.id === vars.heapId
+ )
+ const heapName = heap?.name ?? 'heap'
+ if (added > 0) {
+ toast.success(
+ `Added to ${heapName}`,
+ `${added} photo${added > 1 ? 's' : ''}${already > 0 ? ` (${already} already present)` : ''}`
+ )
+ } else if (already > 0) {
+ toast.info(`Already in ${heapName}`, `${already} photo${already > 1 ? 's' : ''}`)
+ }
+ queryClient.invalidateQueries({ queryKey: HEAPS_QUERY_KEY })
+ queryClient.invalidateQueries({ queryKey: ['photos'] })
+ },
+ onError: (e: any) => toast.error('Failed to add to heap', e.message || 'Unknown error'),
+ })
+
+ const addSelectionToActiveHeap = () => {
+ const state = usePhotoStore.getState()
+ const ids = state.selectedPhotos.length > 0
+ ? state.selectedPhotos
+ : state.activePhotoId
+ ? [state.activePhotoId]
+ : []
+ if (ids.length === 0) {
+ toast.info('Nothing selected', 'Select photos first, then press T')
+ return
+ }
+ const heapsList = queryClient.getQueryData(HEAPS_QUERY_KEY) ?? []
+ const active = heapsList.find((h) => h.is_active)
+ if (!active) {
+ toast.info('No active heap', 'Click the target icon next to a heap to set it as active')
+ return
+ }
+ addToHeapMutation.mutate({ heapId: active.id, photoIds: ids })
+ }
+
// Toggle sidebars
useHotkeys('tab', onToggleLeftSidebar, HK_OPTS)
useHotkeys('i', onToggleRightSidebar, HK_OPTS)
@@ -133,4 +181,7 @@ export function useKeyboardShortcuts(props: KeyboardShortcutsProps) {
},
HK_OPTS
)
+
+ // T: add selection (or active photo) to the active heap.
+ useHotkeys('t', addSelectionToActiveHeap, HK_OPTS)
}
diff --git a/frontend/src/hooks/usePhotosQuery.ts b/frontend/src/hooks/usePhotosQuery.ts
index 996a049..580d3cf 100644
--- a/frontend/src/hooks/usePhotosQuery.ts
+++ b/frontend/src/hooks/usePhotosQuery.ts
@@ -18,6 +18,7 @@ export function usePhotosQuery() {
const ratingMin = useFilterStore((s) => s.ratingMin)
const colorLabel = useFilterStore((s) => s.colorLabel)
const flag = useFilterStore((s) => s.flag)
+ const heapId = useFilterStore((s) => s.heapId)
const filterParams = useMemo(
() =>
@@ -29,8 +30,9 @@ export function usePhotosQuery() {
ratingMin,
colorLabel,
flag,
+ heapId,
}),
- [q, dateFrom, dateTo, mediaTypes, ratingMin, colorLabel, flag]
+ [q, dateFrom, dateTo, mediaTypes, ratingMin, colorLabel, flag, heapId]
)
return useQuery({
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index be7d72e..89c8df7 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -112,30 +112,49 @@ export const library = {
}
// Heaps API
+export interface Heap {
+ id: string
+ name: string
+ is_active: boolean
+ created_at: string
+ updated_at: string | null
+ photo_count: number
+}
+
export const heaps = {
- list: async () => {
+ list: async (): Promise => {
const response = await api.get('/heaps')
return response.data
},
- create: async (name: string, description?: string) => {
- const response = await api.post('/heaps', {
- name,
- description,
- })
+ create: async (name: string): Promise => {
+ const response = await api.post('/heaps', { name })
return response.data
},
- update: async (heapId: string, data: {
- name?: string
- description?: string
- }) => {
+ update: async (
+ heapId: string,
+ data: { name?: string; is_active?: boolean }
+ ): Promise => {
const response = await api.patch(`/heaps/${heapId}`, data)
return response.data
},
- delete: async (heapId: string) => {
- const response = await api.delete(`/heaps/${heapId}`)
+ delete: async (heapId: string): Promise => {
+ await api.delete(`/heaps/${heapId}`)
+ },
+
+ addPhotos: async (heapId: string, photoIds: string[]) => {
+ const response = await api.post(`/heaps/${heapId}/photos`, {
+ photo_ids: photoIds,
+ })
+ return response.data
+ },
+
+ removePhotos: async (heapId: string, photoIds: string[]) => {
+ const response = await api.delete(`/heaps/${heapId}/photos`, {
+ data: { photo_ids: photoIds },
+ })
return response.data
},
}
diff --git a/frontend/src/store/filterStore.ts b/frontend/src/store/filterStore.ts
index d295bdf..a447352 100644
--- a/frontend/src/store/filterStore.ts
+++ b/frontend/src/store/filterStore.ts
@@ -12,6 +12,9 @@ export interface FilterState {
ratingMin: number // 0-5; 0 means no filter
colorLabel: ColorLabel | null
flag: FlagFilter
+ /** 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
}
interface FilterStore extends FilterState {
@@ -24,6 +27,7 @@ interface FilterStore extends FilterState {
setRatingMin: (rating: number) => void
setColorLabel: (label: ColorLabel | null) => void
setFlag: (flag: FlagFilter) => void
+ setHeapId: (id: string | null) => void
setFilterBarOpen: (open: boolean) => void
toggleFilterBar: () => void
@@ -40,6 +44,7 @@ export const INITIAL_FILTERS: FilterState = {
ratingMin: 0,
colorLabel: null,
flag: 'any',
+ heapId: null,
}
export const useFilterStore = create((set) => ({
@@ -58,6 +63,7 @@ export const useFilterStore = create((set) => ({
setRatingMin: (ratingMin) => set({ ratingMin }),
setColorLabel: (colorLabel) => set({ colorLabel }),
setFlag: (flag) => set({ flag }),
+ setHeapId: (heapId) => set({ heapId }),
setFilterBarOpen: (filterBarOpen) => set({ filterBarOpen }),
toggleFilterBar: () => set((s) => ({ filterBarOpen: !s.filterBarOpen })),
@@ -79,6 +85,7 @@ export function filtersToParams(f: FilterState): Record
if (f.flag === 'picked') params.is_picked = 'true'
else if (f.flag === 'discarded') params.is_discarded = 'true'
else if (f.flag === 'unflagged') params.is_picked = 'false'
+ if (f.heapId) params.heap_id = f.heapId
return params
}
@@ -91,6 +98,7 @@ export function hasActiveFilters(f: FilterState): boolean {
f.mediaTypes.length > 0 ||
f.ratingMin > 0 ||
f.colorLabel !== null ||
- f.flag !== 'any'
+ f.flag !== 'any' ||
+ f.heapId !== null
)
}