refactor: unify Pick with active heap membership
Pick and add-to-heap were two ways of saying "I want to keep this
one". Merging them: P now toggles the selection's membership in the
active heap. The is_picked flag goes away (orphaned in the DB the
same way is_trashed was).
Backend
- Drop is_picked from PhotoBase / PhotoUpdate / PhotoResponse and
from the photos list filter param.
- Drop the is_picked Column from the Photo model (DB column stays
on legacy installs but is no longer read or written).
- Drop the bulk action 'pick' branch.
- New GET /heaps/{id}/photo_ids returns just the flat string list.
Used by the frontend for fast client-side membership lookups
without fetching full photo records.
Frontend
- New hooks/useActiveHeapMembersQuery.ts → returns
{ activeHeap, memberIds: Set<string> }. Subscribes once at the
Timeline level and passes a derived isInActiveHeap bool down to
each PhotoThumbnail (avoids hundreds of thumbnails subscribing
to the same query).
- PhotoThumbnail: replaces the old check-icon Pick affordance with
a clear basket badge in the bottom-right corner — a small filled
pick-colour pill containing a ShoppingBasket icon — visible only
when the photo belongs to the active heap.
- P shortcut (useKeyboardShortcuts) now toggles membership: if every
selected photo is already a member, it removes them; otherwise it
adds the missing ones. T binding removed (P fully replaces it).
- RightSidebar Pick button is now a Pick / Picked toggle bound to
the active heap. Disabled with a hint when no heap is active.
Shows the heap name in its title attr.
- filterStore drops 'picked' and 'unflagged' from FlagFilter.
FilterBar's flag dropdown is now just Any / Discarded.
- LeftSidebar drops the "Flagged" virtual node (it just set
flag=picked, which no longer exists).
- KeyboardHints: P → "Pick → heap".
- Photo TS type drops is_picked.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { Star, Check, Trash2, RefreshCw } from 'lucide-react'
|
||||
import { Star, ShoppingBasket, Trash2, RefreshCw, Check } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { photos as photosApi } from '../../services/api'
|
||||
import type { Photo } from '../../types/photo'
|
||||
@@ -12,11 +12,20 @@ interface PhotoThumbnailProps {
|
||||
photo: Photo
|
||||
size: number
|
||||
isSelected: boolean
|
||||
/** True when the photo belongs to the currently active heap. */
|
||||
isInActiveHeap?: boolean
|
||||
onClick: (e: React.MouseEvent) => void
|
||||
onDoubleClick?: (e: React.MouseEvent) => void
|
||||
}
|
||||
|
||||
export function PhotoThumbnail({ photo, size, isSelected, onClick, onDoubleClick }: PhotoThumbnailProps) {
|
||||
export function PhotoThumbnail({
|
||||
photo,
|
||||
size,
|
||||
isSelected,
|
||||
isInActiveHeap = false,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
}: PhotoThumbnailProps) {
|
||||
const [imageError, setImageError] = useState(false)
|
||||
const [imageLoaded, setImageLoaded] = useState(false)
|
||||
const [retryCount, setRetryCount] = useState(0)
|
||||
@@ -167,9 +176,14 @@ export function PhotoThumbnail({ photo, size, isSelected, onClick, onDoubleClick
|
||||
)}
|
||||
|
||||
{/* Flag Indicators */}
|
||||
<div className="absolute bottom-1 right-1">
|
||||
{photo.is_picked && (
|
||||
<Check className="h-4 w-4 text-pick" />
|
||||
<div className="absolute bottom-1 right-1 flex items-center gap-1">
|
||||
{isInActiveHeap && (
|
||||
<div
|
||||
className="flex h-5 w-5 items-center justify-center rounded-full bg-pick text-white shadow-md"
|
||||
title="In active heap"
|
||||
>
|
||||
<ShoppingBasket className="h-3 w-3" />
|
||||
</div>
|
||||
)}
|
||||
{photo.is_discarded && (
|
||||
<Trash2 className="h-4 w-4 text-reject" />
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { usePhotoStore } from '../../store/photoStore'
|
||||
import { PhotoThumbnail } from './PhotoThumbnail'
|
||||
import { usePhotosQuery } from '../../hooks/usePhotosQuery'
|
||||
import { useActiveHeapMembers } from '../../hooks/useActiveHeapMembersQuery'
|
||||
import type { Photo } from '../../types/photo'
|
||||
|
||||
export function Timeline() {
|
||||
@@ -48,6 +49,11 @@ export function Timeline() {
|
||||
// they share one cache entry, regardless of filter state.
|
||||
const { data: photos = [], isLoading } = usePhotosQuery()
|
||||
|
||||
// Membership in the active heap (for the basket affordance). Subscribed
|
||||
// once at this level so we don't have hundreds of thumbnails each
|
||||
// subscribing to the same query.
|
||||
const { memberIds: activeHeapMembers } = useActiveHeapMembers()
|
||||
|
||||
// Group photos into rows for grid layout
|
||||
const rows = useMemo(() => {
|
||||
const result: Photo[][] = []
|
||||
@@ -212,6 +218,7 @@ export function Timeline() {
|
||||
photo={photo}
|
||||
size={thumbnailSize}
|
||||
isSelected={selectedPhotos.includes(photo.id)}
|
||||
isInActiveHeap={activeHeapMembers.has(photo.id)}
|
||||
onClick={(e) => {
|
||||
if (e.shiftKey && lastSelectedIndex !== null) {
|
||||
selectRange(globalIndex)
|
||||
|
||||
Reference in New Issue
Block a user