feat: drag photos onto a folder to move them

Bulk-move via drag-and-drop. Drop a photo (or multi-selection) on
any folder row in the LeftSidebar and the files move on disk +
photo.folder_id updates atomically.

Backend
- New POST /photos/move accepting { photo_ids, target_id }. The
  target_id can be either a Folder id OR a SourceRoot id (the
  sidebar exposes source roots today, so the same drag target
  needs to resolve either).
- Resolves source roots to their on-disk path and looks up / creates
  the canonical Folder row via the existing scan get_or_create_folder
  helper, so dedupe + path normalization stay consistent with the
  scanner.
- Per-photo loop with shutil.move; per-file failures (target name
  collision, missing source, OS error) are collected into a
  structured `errors` array and don't abort the batch.
- Skips photos that are already in the target folder so re-drops
  are a no-op.

Frontend
- New photos.move(ids, targetId) helper in api.ts.
- LeftSidebar grows a moveDropMutation alongside the existing
  discard one. handleDrop dispatches by id prefix:
  'discarded' → discard, 'folder-{id}' → move.
- Folder rows now report acceptsDrop and get the same drag-over
  highlight as heap drops, in primary tint instead of reject.
- onSuccess invalidates both the photos query and the folders
  query so the new folder counts in the sidebar refresh.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 00:13:04 +02:00
parent 066acb64ec
commit 16481730b7
3 changed files with 137 additions and 3 deletions

View File

@@ -87,6 +87,16 @@ export const photos = {
return response.data
},
/** Move photos into a target folder (or source root). Returns
* { moved, errors[] }. */
move: async (photoIds: string[], targetId: string) => {
const response = await api.post('/photos/move', {
photo_ids: photoIds,
target_id: targetId,
})
return response.data as { status: string; moved: number; errors: Array<{ id: string; error: string }> }
},
getThumbnailUrl: (photoId: string, size: 'small' | 'medium' | 'large' = 'medium') => {
return `${API_BASE_URL}/photos/${photoId}/thumb/${size}`
},