From 5d92a471debf5afd348f0d74b98ad4523af0c40c Mon Sep 17 00:00:00 2001 From: Bruno-Alumn <171815746+Bruno-Alumn@users.noreply.github.com> Date: Thu, 29 Oct 2026 23:17:08 +0100 Subject: [PATCH] =?UTF-8?q?LRC-LIB-FOLDERS:=20no=20library=20switch=20whil?= =?UTF-8?q?e=20Move=20Folder=20To=E2=80=A6=20waits=20for=20its=20destinati?= =?UTF-8?q?on=20(#3140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui-egui/src/dialogs/settings/libraries.rs | 8 +++- crates/ui-egui/src/tests/move_folder.rs | 43 +++++++++++++++++++ docs/whats-new.md | 3 +- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/dialogs/settings/libraries.rs b/crates/ui-egui/src/dialogs/settings/libraries.rs index 8e41e8dd4..6f63530c1 100644 --- a/crates/ui-egui/src/dialogs/settings/libraries.rs +++ b/crates/ui-egui/src/dialogs/settings/libraries.rs @@ -3,7 +3,10 @@ use super::{LightcraftApp, Value, json}; // ------------------------------------------------------------------------------- Open Library… /// Switching libraries is refused while an import (or the folder scan before one) is running: its -/// remaining batches would be committed into the new library (issue #1509). +/// remaining batches would be committed into the new library (issue #1509). So is a switch while +/// Move Folder To… waits for its destination: the move runs when the dialog closes, and in another +/// library it would move the folder on disk with nothing to relink, leaving this library's photos +/// missing (issue #3140). pub(crate) fn can_switch_library(app: &LightcraftApp) -> Result<(), String> { if crate::lightroom_import::is_running(app) { return Err("wait for Lightroom catalog import to finish before switching libraries".into()); @@ -11,6 +14,9 @@ pub(crate) fn can_switch_library(app: &LightcraftApp) -> Result<(), String> { if app.import.is_some() || app.scan.is_some() { return Err("wait for the import to finish before switching libraries".into()); } + if app.pending_picks.iter().any(|p| p.command == "folder.move") { + return Err("wait for Move Folder To… to finish before switching libraries".into()); + } Ok(()) } diff --git a/crates/ui-egui/src/tests/move_folder.rs b/crates/ui-egui/src/tests/move_folder.rs index 9e6797c76..58358c251 100644 --- a/crates/ui-egui/src/tests/move_folder.rs +++ b/crates/ui-egui/src/tests/move_folder.rs @@ -50,8 +50,16 @@ type Asked = Arc>)>>>; /// recorded with the sender that answers it), and a synchronous folder dialog that only counts /// how often it was opened on the UI thread. fn app_with_trip(dir: &Scratch) -> (Headless, Asked, Arc) { + app_with_trip_in(dir, None) +} + +/// [`app_with_trip`], with the photo imported into the library at `library` (`None`: in memory). +fn app_with_trip_in(dir: &Scratch, library: Option<&str>) -> (Headless, Asked, Arc) { write_png(&dir.path("trip/a.png"), 1); let mut session = lightcraft_engine::Session::new().with_fs(); + if let Some(library) = library { + session.open_library(library, false).unwrap(); + } session.execute("library.import", &json!({"paths": [dir.path("trip")]})).unwrap(); let mut app = LightcraftApp::new(session, Services { png: None, ..Default::default() }); let asked: Asked = Default::default(); @@ -136,3 +144,38 @@ fn move_folder_to_asks_off_the_ui_thread_and_moves_with_the_answer() { h.settle(SETTLE); assert!(std::path::Path::new(&dir.path("trip/a.png")).exists(), "moved back"); } + +/// Issue #3140: while Move Folder To… waits for its destination, the library can't be switched +/// (the move would run in the other library, moving the folder with nothing to relink). The answer +/// moves the folder in the library it was asked in; then switching works again. +#[test] +fn no_library_switch_while_move_folder_to_waits_for_its_destination() { + let dir = Scratch::new("switch"); + std::fs::create_dir_all(dir.path("dest")).unwrap(); + let (first, other) = (dir.path("First"), dir.path("Other")); + let (mut h, asked, _) = app_with_trip_in(&dir, Some(&first)); + open_row_menu(&mut h, &format!("source:libfolder:{}", dir.path("trip"))); + let r = h.request("ui.clickWidget", json!({"id": "folderMoveTo"}), T); + assert_eq!(r["ok"], true, "{r}"); + h.step(); + assert_eq!(asked.lock().unwrap().len(), 1, "the dialog is up"); + let library = |h: &Headless| h.app.session.library.as_ref().map(|l| l.dir.to_string_lossy().replace('\\', "/")); + // another library (from the control channel or an agent, say) is refused meanwhile + let e = h.app.run("app.openLibrary", json!({"path": other})).expect_err("no switch while the dialog is up"); + assert!(e.contains("Move Folder To…"), "{e}"); + assert_eq!(library(&h), Some(first.clone()), "the first library stays open"); + // the answer moves the folder there, and its photo follows + asked.lock().unwrap()[0].1.send(vec![dir.path("dest")]).unwrap(); + h.settle(SETTLE); + assert!(h.app.pending_picks.is_empty()); + assert!(std::path::Path::new(&dir.path("dest/trip/a.png")).exists(), "moved on disk"); + assert_eq!(library(&h), Some(first)); + let relinked = h.app.session.catalog.photos().filter(|p| p.in_library()).all(|p| match &p.source { + lightcraft_catalog::Source::File { path } => path.replace('\\', "/").starts_with(&dir.path("dest/trip")), + lightcraft_catalog::Source::Demo { .. } => false, + }); + assert!(relinked, "the photo is relinked in the first library"); + // and now the switch goes through + h.app.run("app.openLibrary", json!({"path": other})).expect("the switch works once the move is done"); + assert_eq!(library(&h), Some(other)); +} diff --git a/docs/whats-new.md b/docs/whats-new.md index 210f6d96e..9c197f39c 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -6,7 +6,8 @@ - Move Folder To…, in the menu of a folder in Folders or Local, now opens its folder dialog the way Import and Move to Folder… do: the window keeps drawing and answering while you choose, and the folder moves when the dialog closes, with the same "Moved; N photo(s) relinked" message. Before, the dialog held up the whole window until it closed; - on Linux with X11 the app could stop responding altogether. + on Linux with X11 the app could stop responding altogether. While the dialog is open, another library can't be + opened (the move belongs to the library it was started in). ## 2026-10-24