feat: drag photos onto Discarded sidebar node to discard them

Same drag pattern as the heap drop, but the target is the Discarded
library node. The dropped photos go to is_discarded=true via a
single bulk request.

Backend: the /photos/bulk endpoint already had a 'discard' action
branch; the missing piece was a frontend client that sent the right
shape. The previous photos.bulkUpdate sent
{ photo_ids, discard: true } against a backend that wanted
{ ids, action } — silently broken since day one. Replaced with two
narrow helpers that match BulkAction exactly: photos.bulkDiscard(ids)
and photos.bulkRestore(ids).

Frontend: LeftSidebar grows a small dnd state machine — dropTargetId
for the hovered row, isDropTarget(id) for which library nodes accept
drops, handleDrop(id, ids) for the dispatch. Today only the
'discarded' node is wired; folder rows for bulk move come next.
Drop highlight uses the reject ring/tint to match the destructive
nature of the action. Toast confirms; photos query is invalidated
so the timeline immediately drops them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 00:11:23 +02:00
parent 485b60ff20
commit 066acb64ec
2 changed files with 75 additions and 11 deletions

View File

@@ -69,15 +69,20 @@ export const photos = {
return response.data
},
bulkUpdate: async (photoIds: string[], data: {
rating?: number
flag?: string
heap_id?: string
discard?: boolean
}) => {
/** Bulk discard — matches the backend BulkAction schema. */
bulkDiscard: async (photoIds: string[]) => {
const response = await api.post('/photos/bulk', {
photo_ids: photoIds,
...data,
ids: photoIds,
action: 'discard',
})
return response.data
},
/** Bulk restore from discarded. */
bulkRestore: async (photoIds: string[]) => {
const response = await api.post('/photos/bulk', {
ids: photoIds,
action: 'restore',
})
return response.data
},