Add hover-revealed delete button to script sidebar rows, routed through the confirm-delete modal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bauer, Georg
2026-07-22 15:20:47 +02:00
parent 5968cf47ba
commit cc520d517a
2 changed files with 141 additions and 13 deletions

View File

@@ -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<Message> {
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<Message> {
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();

View File

@@ -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,6 +1011,7 @@ pub fn view(
} else {
item_style
};
let open_button =
button(container(label_text).width(Length::Fill).clip(true))
.on_press(Message::OpenTab(Tab {
id: s.id.clone(),
@@ -1002,8 +1022,26 @@ pub fn view(
}))
.padding([5, 8])
.width(Length::Fill)
.style(style_fn)
.into()
.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()