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:
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user