LRC-LIB-FOLDERS: no library switch while Move Folder To… waits for its destination (#3140)

This commit is contained in:
Bruno-Alumn
2026-10-29 23:17:08 +01:00
parent f3b332c484
commit 5d92a471de
3 changed files with 52 additions and 2 deletions
@@ -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(())
}
+43
View File
@@ -50,8 +50,16 @@ type Asked = Arc<Mutex<Vec<(PickRequest, Sender<Vec<String>>)>>>;
/// 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<AtomicUsize>) {
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<AtomicUsize>) {
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));
}
+2 -1
View File
@@ -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