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:
@@ -8,14 +8,16 @@ import {
|
||||
Info,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Check,
|
||||
ShoppingBasket,
|
||||
Trash2,
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { format } from 'date-fns'
|
||||
import { usePhotoStore } from '../../store/photoStore'
|
||||
import { photos as photosApi } from '../../services/api'
|
||||
import { photos as photosApi, heaps as heapsApi } from '../../services/api'
|
||||
import { useActiveHeapMembers } from '../../hooks/useActiveHeapMembersQuery'
|
||||
import { HEAPS_QUERY_KEY } from '../../hooks/useHeapsQuery'
|
||||
|
||||
interface PhotoDetails {
|
||||
id: string
|
||||
@@ -26,7 +28,6 @@ interface PhotoDetails {
|
||||
file_size: number | null
|
||||
taken_at: string | null
|
||||
rating: number
|
||||
is_picked: boolean
|
||||
is_discarded: boolean
|
||||
user_title: string | null
|
||||
user_notes: string | null
|
||||
@@ -122,7 +123,6 @@ export function RightSidebar() {
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (data: {
|
||||
rating?: number
|
||||
is_picked?: boolean
|
||||
is_discarded?: boolean
|
||||
user_title?: string | null
|
||||
user_notes?: string | null
|
||||
@@ -134,6 +134,29 @@ export function RightSidebar() {
|
||||
},
|
||||
})
|
||||
|
||||
// Membership in the active heap (for the Pick toggle button).
|
||||
const { activeHeap, memberIds: activeHeapMembers } = useActiveHeapMembers()
|
||||
const isInActiveHeap =
|
||||
!!activePhotoId && activeHeapMembers.has(activePhotoId)
|
||||
|
||||
const heapMutation = useMutation({
|
||||
mutationFn: ({ remove }: { remove: boolean }) => {
|
||||
if (!activeHeap || !activePhotoId) return Promise.resolve(null)
|
||||
return remove
|
||||
? heapsApi.removePhotos(activeHeap.id, [activePhotoId])
|
||||
: heapsApi.addPhotos(activeHeap.id, [activePhotoId])
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: HEAPS_QUERY_KEY })
|
||||
if (activeHeap) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['heap-photo-ids', activeHeap.id],
|
||||
})
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['photos'] })
|
||||
},
|
||||
})
|
||||
|
||||
// Local drafts for the editable text fields. These mirror the server value
|
||||
// but stay independent while the user is typing, so we don't fight focus or
|
||||
// clobber edits with stale refetches.
|
||||
@@ -178,7 +201,6 @@ export function RightSidebar() {
|
||||
|
||||
const multipleSelected = selectedPhotos.length > 1
|
||||
const rating = photo?.rating ?? 0
|
||||
const isPicked = photo?.is_picked ?? false
|
||||
const isDiscarded = photo?.is_discarded ?? false
|
||||
const colorLabel = (photo?.color_label ?? null) as ColorLabel | null
|
||||
|
||||
@@ -299,28 +321,28 @@ export function RightSidebar() {
|
||||
<label className="mb-1 block text-xs text-text-muted">Flag</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
updateMutation.mutate({
|
||||
is_picked: !isPicked,
|
||||
is_discarded: false,
|
||||
})
|
||||
}
|
||||
onClick={() => heapMutation.mutate({ remove: isInActiveHeap })}
|
||||
disabled={!activeHeap || heapMutation.isPending}
|
||||
className={clsx(
|
||||
'flex items-center gap-1 rounded px-2 py-1 text-sm transition-colors',
|
||||
isPicked
|
||||
'flex items-center gap-1 rounded px-2 py-1 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50',
|
||||
isInActiveHeap
|
||||
? 'bg-pick/20 text-pick'
|
||||
: 'bg-surface-2 text-text-muted hover:bg-surface-offset'
|
||||
)}
|
||||
title={
|
||||
activeHeap
|
||||
? isInActiveHeap
|
||||
? `Remove from "${activeHeap.name}"`
|
||||
: `Add to "${activeHeap.name}"`
|
||||
: 'Set an active heap first'
|
||||
}
|
||||
>
|
||||
<Check className="h-3 w-3" />
|
||||
Pick
|
||||
<ShoppingBasket className="h-3 w-3" />
|
||||
{isInActiveHeap ? 'Picked' : 'Pick'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
updateMutation.mutate({
|
||||
is_discarded: !isDiscarded,
|
||||
is_picked: false,
|
||||
})
|
||||
updateMutation.mutate({ is_discarded: !isDiscarded })
|
||||
}
|
||||
className={clsx(
|
||||
'flex items-center gap-1 rounded px-2 py-1 text-sm transition-colors',
|
||||
|
||||
Reference in New Issue
Block a user