diff --git a/crates/ui-egui/src/menubar.rs b/crates/ui-egui/src/menubar.rs index c5e4e45c7..96463df61 100644 --- a/crates/ui-egui/src/menubar.rs +++ b/crates/ui-egui/src/menubar.rs @@ -962,9 +962,10 @@ pub fn show_in_window(app: &mut LightcraftApp, ui: &mut egui::Ui, max_width: f32 } if let Some((id, params)) = clicked { let r = run_item(app, &id, params); - // an export that can't start (e.g. no folder) says why instead of doing nothing + // an export that can't start (e.g. no folder), or a backup / restore the host refuses + // (changes still unsaved, nothing stored in this browser), says why instead of doing nothing if let Err(e) = r - && matches!(id.as_str(), "app.export" | "app.exportPrevious") + && matches!(id.as_str(), "app.export" | "app.exportPrevious" | "file.backupLibrary" | "file.restoreLibrary") { app.toast(ui.ctx(), e); } @@ -1044,6 +1045,7 @@ fn nodes_ui(ui: &mut egui::Ui, nodes: &[MenuNode], mac: bool, clicked: &mut Opti b = b.shortcut_text(shortcut_text(sc, mac)); } let r = ui.add_enabled(*enabled, b); + crate::widgets::register(ui.ctx(), format!("menuitem:{id}"), r.rect); if *checked == Some(true) { let t = crate::theme::Tokens::get(ui.ctx()); let c = egui::Rect::from_center_size(egui::pos2(r.rect.left() + 11.0, r.rect.center().y), egui::vec2(13.0, 13.0)); @@ -1390,6 +1392,30 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } + /// A backup the host refuses (changes still unsaved, issue #1508) says why when started from + /// the menu, as a refused export does; the refusal used to reach only the control channel. + #[test] + fn a_refused_backup_from_the_menu_shows_why() { + // the host refuses, as the web one does while changes are still unsaved (issue #1508) + let refusal = "Backup not started: 1 change could not be saved to browser storage yet"; + let services = crate::Services { + backup_library: Some(Box::new(move |_: &mut lightcraft_engine::Session| Err(refusal.to_string()))), + ..Default::default() + }; + let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services); + let mut h = crate::headless::Headless::new(app, [1400.0, 900.0], 1.0); + let t = std::time::Duration::from_secs(30); + h.settle(t); + for id in ["menu:File", "menuitem:file.backupLibrary"] { + let r = h.request("ui.clickWidget", json!({"id": id}), t); + assert_eq!(r["ok"], true, "{id}: {r}"); + h.step(); + } + // (read at once: a toast keeps the UI repainting until it expires, so settling would outlive it) + let toast = h.app.ui.toast.as_ref().map(|t| t.0.clone()).unwrap_or_default(); + assert!(toast.contains(refusal), "the refusal is shown: {toast:?}"); + } + /// #1528: `file.findMissing` with `wait: true` must propagate folder validation and search /// failures as errors instead of returning `Ok(null)`. #[test]