feat: heap convert can create a subfolder under the target

The previous /heaps/{id}/convert dropped photos directly into a chosen
source root, which is rarely what you want — Lightroom-style behaviour
is "make a folder named after the collection inside the library".
Now the dialog lets you do that.

Backend
- HeapConvertBody gains an optional subfolder_name field. Path
  separators and dot-segments are rejected. When set, the handler
  joins it onto the resolved parent_dir, mkdir's it if missing, and
  uses the resulting path as the move/copy destination. Otherwise
  the parent_dir itself is used (unchanged behaviour).
- The Folder DB row for the destination is created via the existing
  scanner get_or_create_folder helper so dedupe + path normalization
  stay consistent across the codebase.
- The target source root id is propagated through both the source-
  root and folder branches so the new Folder row is correctly
  parented when subfolder_name is set on a folder target too.

Frontend
- HeapConvertDialog grows a "Subfolder name" input that prefills
  with the heap name when the dialog opens. Trimmed empty value
  drops directly into the parent. A live hint below the input
  shows exactly which path will be created (or that the parent
  will be used).
- api.ts heaps.convert() signature accepts an optional
  subfolder_name field; the dialog sends it via mutationFn.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 11:31:14 +02:00
parent bed817d274
commit bb7c2b12d6
3 changed files with 77 additions and 12 deletions

View File

@@ -190,10 +190,16 @@ export const heaps = {
},
/** Convert a heap into a folder by moving (or copying) every member
* photo into the target directory. */
* photo into the target directory. Optionally creates a subfolder
* inside the target by name. */
convert: async (
heapId: string,
body: { target_id: string; mode: 'move' | 'copy'; delete_heap: boolean }
body: {
target_id: string
mode: 'move' | 'copy'
delete_heap: boolean
subfolder_name?: string | null
}
) => {
const response = await api.post(`/heaps/${heapId}/convert`, body)
return response.data as {