From cc520d517a37ac81d0d558d709534e0873cba341 Mon Sep 17 00:00:00 2001 From: "Bauer, Georg" Date: Wed, 22 Jul 2026 15:20:47 +0200 Subject: [PATCH] Add hover-revealed delete button to script sidebar rows, routed through the confirm-delete modal. Co-Authored-By: Claude Fable 5 --- crates/bds-ui/src/app.rs | 90 ++++++++++++++++++++++++++++++ crates/bds-ui/src/views/sidebar.rs | 64 ++++++++++++++++----- 2 files changed, 141 insertions(+), 13 deletions(-) diff --git a/crates/bds-ui/src/app.rs b/crates/bds-ui/src/app.rs index ad2576d..3ffb4aa 100644 --- a/crates/bds-ui/src/app.rs +++ b/crates/bds-ui/src/app.rs @@ -380,6 +380,8 @@ pub enum Message { CreateScript, CreateTemplate, CreateImport, + /// Per sidebar_views.allium ScriptListItemEntry: row-level delete affordance. + ScriptDeleteRequested(String), // Conversational AI ChatCreate, @@ -1385,6 +1387,9 @@ impl BdsApp { Message::CreateScript => self.create_sidebar_script(), Message::CreateTemplate => self.create_sidebar_template(), Message::CreateImport => self.create_sidebar_import(), + Message::ScriptDeleteRequested(script_id) => { + self.show_script_delete_confirmation(&script_id) + } Message::ChatCreate => self.create_chat_conversation(), Message::ChatRenameInputChanged(value) => { if let Some(state) = self.active_chat_state_mut() { @@ -7055,6 +7060,25 @@ impl BdsApp { })) } + /// Per editor_script.allium ScriptDeleteAction / action_patterns.allium + /// confirmation_assignments: script_delete uses the confirm-delete modal + /// showing the script title with no reference list. + fn show_script_delete_confirmation(&mut self, script_id: &str) -> Task { + let Some(db) = &self.db else { + return Task::none(); + }; + let Ok(script) = bds_core::db::queries::script::get_script_by_id(db.conn(), script_id) + else { + return Task::none(); + }; + self.active_modal = Some(modal::ModalState::ConfirmDelete { + entity_name: script.title, + references: Vec::new(), + on_confirm: modal::ConfirmAction::DeleteScript(script_id.to_string()), + }); + Task::none() + } + fn delete_script_editor(&mut self, script_id: &str) -> Task { let Some(db) = &self.db else { return Task::none(); @@ -12529,6 +12553,72 @@ mod tests { assert!(!app.script_editors.contains_key(&script_id)); } + /// sidebar_views.allium ScriptListItemEntry provides + /// ScriptDeleteRequested(item.script_id): deleting from the sidebar row + /// routes through the same confirm modal as the editor delete button and + /// works without an open editor tab. + #[test] + fn sidebar_script_delete_requires_confirmation_and_works_without_open_tab() { + let (db, project, tmp) = setup(); + let mut app = make_app(db, project, &tmp); + let _ = app.update(Message::CreateScript); + let script_id = app.sidebar_scripts[0].id.clone(); + + // Close the editor tab: the sidebar row must not depend on one. + let _ = app.update(Message::CloseTab(script_id.clone())); + assert!(!app.tabs.iter().any(|tab| tab.id == script_id)); + + // Requesting deletion only opens the confirm modal; nothing is deleted. + let _ = app.update(Message::ScriptDeleteRequested(script_id.clone())); + assert!(matches!( + app.active_modal, + Some(modal::ModalState::ConfirmDelete { .. }) + )); + assert!( + bds_core::db::queries::script::get_script_by_id( + app.db.as_ref().unwrap().conn(), + &script_id + ) + .is_ok() + ); + + let _ = app.update(Message::ConfirmModal(modal::ConfirmAction::DeleteScript( + script_id.clone(), + ))); + assert!( + bds_core::db::queries::script::get_script_by_id( + app.db.as_ref().unwrap().conn(), + &script_id + ) + .is_err() + ); + assert!(!app.script_editors.contains_key(&script_id)); + } + + /// editor_script.allium ScriptDeleteAction: the confirm modal shows the + /// script title with no reference list. + #[test] + fn sidebar_script_delete_confirmation_shows_script_title() { + let (db, project, tmp) = setup(); + let mut app = make_app(db, project, &tmp); + let _ = app.update(Message::CreateScript); + let script = app.sidebar_scripts[0].clone(); + + let _ = app.update(Message::ScriptDeleteRequested(script.id.clone())); + match app.active_modal { + Some(modal::ModalState::ConfirmDelete { + ref entity_name, + ref references, + on_confirm: modal::ConfirmAction::DeleteScript(ref id), + }) => { + assert_eq!(entity_name, &script.title); + assert!(references.is_empty()); + assert_eq!(id, &script.id); + } + ref other => panic!("expected ConfirmDelete modal, got {other:?}"), + } + } + #[test] fn rebuild_completion_refreshes_project_metadata_in_settings() { let (db, project, tmp) = setup(); diff --git a/crates/bds-ui/src/views/sidebar.rs b/crates/bds-ui/src/views/sidebar.rs index eae653c..7fe4ba3 100644 --- a/crates/bds-ui/src/views/sidebar.rs +++ b/crates/bds-ui/src/views/sidebar.rs @@ -269,6 +269,24 @@ fn clear_filters_style(_theme: &Theme, status: button::Status) -> button::Style } } +/// Row delete button (×) revealed on row hover; turns red on button hover. +/// Per sidebar_views.allium ScriptListItemEntry RowLayout. +fn row_delete_style(_theme: &Theme, status: button::Status) -> button::Style { + let text_color = match status { + button::Status::Hovered | button::Status::Pressed => Color::from_rgb(0.90, 0.30, 0.30), + _ => Color::from_rgb(0.60, 0.60, 0.65), + }; + button::Style { + background: Some(Background::Color(Color::from_rgb(0.20, 0.20, 0.25))), + text_color, + border: Border { + radius: 4.0.into(), + ..Border::default() + }, + ..button::Style::default() + } +} + /// Month name abbreviation for calendar display. fn month_abbr(month: u32) -> &'static str { match month { @@ -981,7 +999,8 @@ pub fn view( .iter() .map(|s| { let is_active = active_tab == Some(s.id.as_str()); - let text_px = width - SIDEBAR_TEXT_OVERHEAD_PX; + // Reserve room on the right for the hover-revealed delete button. + let text_px = width - SIDEBAR_TEXT_OVERHEAD_PX - 20.0; let display_title = truncate_to_fit(&s.title, text_px); let label_text = text(display_title.clone()) .size(12) @@ -992,18 +1011,37 @@ pub fn view( } else { item_style }; - button(container(label_text).width(Length::Fill).clip(true)) - .on_press(Message::OpenTab(Tab { - id: s.id.clone(), - tab_type: TabType::Scripts, - title: display_title, - is_transient: true, - is_dirty: false, - })) - .padding([5, 8]) - .width(Length::Fill) - .style(style_fn) - .into() + let open_button = + button(container(label_text).width(Length::Fill).clip(true)) + .on_press(Message::OpenTab(Tab { + id: s.id.clone(), + tab_type: TabType::Scripts, + title: display_title, + is_transient: true, + is_dirty: false, + })) + .padding([5, 8]) + .width(Length::Fill) + .style(style_fn); + // sidebar_views.allium ScriptListItemEntry: delete button (x), + // visible only on row hover, routed through the confirm modal. + let delete_button = button( + text("\u{2715}") // ✕ + .size(11) + .shaping(Shaping::Advanced), + ) + .on_press(Message::ScriptDeleteRequested(s.id.clone())) + .padding([2, 6]) + .style(row_delete_style); + iced::widget::hover( + open_button, + container(delete_button) + .width(Length::Fill) + .height(Length::Fill) + .align_x(iced::alignment::Horizontal::Right) + .align_y(iced::alignment::Vertical::Center) + .padding([0, 4]), + ) }) .collect(); iced::widget::Column::with_children(items).spacing(1).into()