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,
|
CreateScript,
|
||||||
CreateTemplate,
|
CreateTemplate,
|
||||||
CreateImport,
|
CreateImport,
|
||||||
|
/// Per sidebar_views.allium ScriptListItemEntry: row-level delete affordance.
|
||||||
|
ScriptDeleteRequested(String),
|
||||||
|
|
||||||
// Conversational AI
|
// Conversational AI
|
||||||
ChatCreate,
|
ChatCreate,
|
||||||
@@ -1385,6 +1387,9 @@ impl BdsApp {
|
|||||||
Message::CreateScript => self.create_sidebar_script(),
|
Message::CreateScript => self.create_sidebar_script(),
|
||||||
Message::CreateTemplate => self.create_sidebar_template(),
|
Message::CreateTemplate => self.create_sidebar_template(),
|
||||||
Message::CreateImport => self.create_sidebar_import(),
|
Message::CreateImport => self.create_sidebar_import(),
|
||||||
|
Message::ScriptDeleteRequested(script_id) => {
|
||||||
|
self.show_script_delete_confirmation(&script_id)
|
||||||
|
}
|
||||||
Message::ChatCreate => self.create_chat_conversation(),
|
Message::ChatCreate => self.create_chat_conversation(),
|
||||||
Message::ChatRenameInputChanged(value) => {
|
Message::ChatRenameInputChanged(value) => {
|
||||||
if let Some(state) = self.active_chat_state_mut() {
|
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> {
|
fn delete_script_editor(&mut self, script_id: &str) -> Task<Message> {
|
||||||
let Some(db) = &self.db else {
|
let Some(db) = &self.db else {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -12529,6 +12553,72 @@ mod tests {
|
|||||||
assert!(!app.script_editors.contains_key(&script_id));
|
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]
|
#[test]
|
||||||
fn rebuild_completion_refreshes_project_metadata_in_settings() {
|
fn rebuild_completion_refreshes_project_metadata_in_settings() {
|
||||||
let (db, project, tmp) = setup();
|
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.
|
/// Month name abbreviation for calendar display.
|
||||||
fn month_abbr(month: u32) -> &'static str {
|
fn month_abbr(month: u32) -> &'static str {
|
||||||
match month {
|
match month {
|
||||||
@@ -981,7 +999,8 @@ pub fn view(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|s| {
|
.map(|s| {
|
||||||
let is_active = active_tab == Some(s.id.as_str());
|
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 display_title = truncate_to_fit(&s.title, text_px);
|
||||||
let label_text = text(display_title.clone())
|
let label_text = text(display_title.clone())
|
||||||
.size(12)
|
.size(12)
|
||||||
@@ -992,6 +1011,7 @@ pub fn view(
|
|||||||
} else {
|
} else {
|
||||||
item_style
|
item_style
|
||||||
};
|
};
|
||||||
|
let open_button =
|
||||||
button(container(label_text).width(Length::Fill).clip(true))
|
button(container(label_text).width(Length::Fill).clip(true))
|
||||||
.on_press(Message::OpenTab(Tab {
|
.on_press(Message::OpenTab(Tab {
|
||||||
id: s.id.clone(),
|
id: s.id.clone(),
|
||||||
@@ -1002,8 +1022,26 @@ pub fn view(
|
|||||||
}))
|
}))
|
||||||
.padding([5, 8])
|
.padding([5, 8])
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.style(style_fn)
|
.style(style_fn);
|
||||||
.into()
|
// 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();
|
.collect();
|
||||||
iced::widget::Column::with_children(items).spacing(1).into()
|
iced::widget::Column::with_children(items).spacing(1).into()
|
||||||
|
|||||||
Reference in New Issue
Block a user