feat(move): "move to folder" for grid selections, folders, and m shortcut

Extends the heap-only "move to folder" action to grid single/bulk
selections, sidebar folders, and an `m` keyboard shortcut — all through
one shared dialog driven by a moveDialog store.

Backend (sidecar):
- Extract the heap move/copy + reindex loop into a reusable movePhotoFiles
  helper plus resolveMoveTarget
- POST /photos/move: move/copy an arbitrary UID list into a folder
- POST /folders/:rel/move: reparent a folder dir (whole subtree) under a
  new parent, guarding against moving into itself/a descendant

Frontend:
- moveDialog store + generalized MoveToFolderDialog (heap | photos | folder
  subjects); mounted once in +layout.svelte. Replaces HeapConvertDialog
- movePhotosToFolder / moveFolder service fns
- Entry points: BulkActionBar button, gridKeyNav `m`, FolderTree kebab,
  heap kebab — all call openMove()

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 00:10:19 +02:00
parent 5be6fd9047
commit b2b6060872
12 changed files with 725 additions and 383 deletions

View File

@@ -1006,6 +1006,43 @@ export async function convertHeap(
return callSidecar('POST', `/albums/${uid}/convert`, body) as Promise<HeapConvertResult>;
}
// ── Move arbitrary photos (by UID) to a folder ──────────────────────────────
// Same on-disk move/copy + reindex as convertHeap, but the sidecar resolves the
// photos from a UID list instead of an album. Backs the grid's move-to-folder.
export interface PhotosMoveBody {
uids: string[];
/** Originals-relative target folder. Empty string = originals root. */
targetFolder: string;
mode: 'move' | 'copy';
/** Optional subfolder to create under `targetFolder` and place files into. */
subfolder?: string | null;
}
export interface PhotosMoveResult {
moved: number;
copied: number;
errors: { uid: string; reason: string }[];
}
export async function movePhotosToFolder(body: PhotosMoveBody): Promise<PhotosMoveResult> {
return callSidecar('POST', '/photos/move', body) as Promise<PhotosMoveResult>;
}
// ── Reparent a folder (move the directory under a different parent) ──────────
export interface FolderMoveResult {
ok: boolean;
oldPath: string;
newPath: string;
}
export async function moveFolder(rel: string, targetParent: string): Promise<FolderMoveResult> {
return callSidecar('POST', `/folders/${encodeURIComponent(rel)}/move`, {
targetParent
}) as Promise<FolderMoveResult>;
}
// ── Photo marks (rating + color) ─────────────────────────────────────────────
// PhotoPrism's PUT silently drops Rating and Color (they're auto-computed
// internal fields). We store them in mule-sidecar instead.