fix: small findings with regard to deletes from sidebar
This commit is contained in:
@@ -22,7 +22,7 @@ uuid = { version = "1", features = ["v4", "serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
chrono = { version = "0.4", features = ["serde", "unstable-locales"] }
|
||||
sha2 = "0.10"
|
||||
md5 = "0.8.1"
|
||||
unicode-normalization = "0.1"
|
||||
|
||||
@@ -382,6 +382,10 @@ pub enum Message {
|
||||
CreateImport,
|
||||
/// Per sidebar_views.allium ScriptListItemEntry: row-level delete affordance.
|
||||
ScriptDeleteRequested(String),
|
||||
/// Per sidebar_views.allium TemplateListItemEntry: row-level delete affordance.
|
||||
TemplateDeleteRequested(String),
|
||||
/// Per sidebar_views.allium ImportListItemEntry: row-level delete affordance.
|
||||
ImportDeleteRequested(String),
|
||||
|
||||
// Conversational AI
|
||||
ChatCreate,
|
||||
@@ -1390,6 +1394,12 @@ impl BdsApp {
|
||||
Message::ScriptDeleteRequested(script_id) => {
|
||||
self.show_script_delete_confirmation(&script_id)
|
||||
}
|
||||
Message::TemplateDeleteRequested(template_id) => {
|
||||
self.show_template_delete_confirmation(&template_id)
|
||||
}
|
||||
Message::ImportDeleteRequested(definition_id) => {
|
||||
self.show_import_delete_confirmation(&definition_id)
|
||||
}
|
||||
Message::ChatCreate => self.create_chat_conversation(),
|
||||
Message::ChatRenameInputChanged(value) => {
|
||||
if let Some(state) = self.active_chat_state_mut() {
|
||||
@@ -4677,17 +4687,7 @@ impl BdsApp {
|
||||
ImportEditorMsg::Execute => self.start_import_execution(&definition_id),
|
||||
ImportEditorMsg::AutoMapTaxonomy => self.start_import_auto_mapping(&definition_id),
|
||||
ImportEditorMsg::DeleteRequested => {
|
||||
let name = self
|
||||
.import_editors
|
||||
.get(&definition_id)
|
||||
.map(|state| state.definition.name.clone())
|
||||
.unwrap_or_default();
|
||||
self.active_modal = Some(modal::ModalState::Confirm {
|
||||
title: t(self.ui_locale, "import.deleteTitle"),
|
||||
message: tw(self.ui_locale, "import.deleteMessage", &[("name", &name)]),
|
||||
on_confirm: modal::ConfirmAction::DeleteImport(definition_id),
|
||||
});
|
||||
Task::none()
|
||||
self.show_import_delete_confirmation(&definition_id)
|
||||
}
|
||||
ImportEditorMsg::SetResolution {
|
||||
kind,
|
||||
@@ -7046,18 +7046,20 @@ impl BdsApp {
|
||||
("tags", &referencing_tags.to_string()),
|
||||
],
|
||||
);
|
||||
return Task::done(Message::ShowModal(modal::ModalState::Confirm {
|
||||
self.active_modal = Some(modal::ModalState::Confirm {
|
||||
title,
|
||||
message,
|
||||
on_confirm: modal::ConfirmAction::ForceDeleteTemplate(template_id.to_string()),
|
||||
}));
|
||||
});
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
Task::done(Message::ShowModal(modal::ModalState::ConfirmDelete {
|
||||
self.active_modal = Some(modal::ModalState::ConfirmDelete {
|
||||
entity_name: template.title,
|
||||
references: Vec::new(),
|
||||
on_confirm: modal::ConfirmAction::DeleteTemplate(template_id.to_string()),
|
||||
}))
|
||||
});
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Per editor_script.allium ScriptDeleteAction / action_patterns.allium
|
||||
@@ -7079,6 +7081,29 @@ impl BdsApp {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Per sidebar_views.allium ImportListItemEntry: confirming an import
|
||||
/// deletion must not depend on an open editor tab, so the definition
|
||||
/// name comes from the database.
|
||||
fn show_import_delete_confirmation(&mut self, definition_id: &str) -> Task<Message> {
|
||||
let Some(db) = &self.db else {
|
||||
return Task::none();
|
||||
};
|
||||
let Ok(definition) = engine::wordpress_import::get_definition(db.conn(), definition_id)
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
self.active_modal = Some(modal::ModalState::Confirm {
|
||||
title: t(self.ui_locale, "import.deleteTitle"),
|
||||
message: tw(
|
||||
self.ui_locale,
|
||||
"import.deleteMessage",
|
||||
&[("name", &definition.name)],
|
||||
),
|
||||
on_confirm: modal::ConfirmAction::DeleteImport(definition_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();
|
||||
@@ -12619,6 +12644,166 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// sidebar_views.allium TemplateListItemEntry provides
|
||||
/// TemplateDeleteRequested(item.template_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_template_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::CreateTemplate);
|
||||
let template_id = app.sidebar_templates[0].id.clone();
|
||||
|
||||
// Close the editor tab: the sidebar row must not depend on one.
|
||||
let _ = app.update(Message::CloseTab(template_id.clone()));
|
||||
assert!(!app.tabs.iter().any(|tab| tab.id == template_id));
|
||||
|
||||
// Requesting deletion only opens the confirm modal; nothing is deleted.
|
||||
let _ = app.update(Message::TemplateDeleteRequested(template_id.clone()));
|
||||
assert!(matches!(
|
||||
app.active_modal,
|
||||
Some(modal::ModalState::ConfirmDelete {
|
||||
on_confirm: modal::ConfirmAction::DeleteTemplate(ref id),
|
||||
..
|
||||
}) if id == &template_id
|
||||
));
|
||||
assert!(
|
||||
bds_core::db::queries::template::get_template_by_id(
|
||||
app.db.as_ref().unwrap().conn(),
|
||||
&template_id
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
let _ = app.update(Message::ConfirmModal(modal::ConfirmAction::DeleteTemplate(
|
||||
template_id.clone(),
|
||||
)));
|
||||
assert!(
|
||||
bds_core::db::queries::template::get_template_by_id(
|
||||
app.db.as_ref().unwrap().conn(),
|
||||
&template_id
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(!app.template_editors.contains_key(&template_id));
|
||||
}
|
||||
|
||||
/// editor_template.allium TemplateDelete / TemplateListItemEntry
|
||||
/// DeleteConfirmation: a referenced template prompts the force-delete
|
||||
/// confirmation with reference counts instead of the plain delete modal.
|
||||
#[test]
|
||||
fn sidebar_template_delete_with_references_prompts_force_delete() {
|
||||
let (db, project, tmp) = setup();
|
||||
let mut app = make_app(db, project, &tmp);
|
||||
let _ = app.update(Message::CreateTemplate);
|
||||
let template = app.sidebar_templates[0].clone();
|
||||
|
||||
let referencing_post = post::create_post(
|
||||
app.db.as_ref().unwrap().conn(),
|
||||
tmp.path(),
|
||||
&template.project_id,
|
||||
"Uses Template",
|
||||
None,
|
||||
vec![],
|
||||
vec![],
|
||||
None,
|
||||
Some("en"),
|
||||
Some(&template.slug),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let _ = app.update(Message::TemplateDeleteRequested(template.id.clone()));
|
||||
assert!(matches!(
|
||||
app.active_modal,
|
||||
Some(modal::ModalState::Confirm {
|
||||
on_confirm: modal::ConfirmAction::ForceDeleteTemplate(ref id),
|
||||
..
|
||||
}) if id == &template.id
|
||||
));
|
||||
|
||||
// Force delete removes the template and clears the post reference.
|
||||
let _ = app.update(Message::ConfirmModal(
|
||||
modal::ConfirmAction::ForceDeleteTemplate(template.id.clone()),
|
||||
));
|
||||
assert!(
|
||||
bds_core::db::queries::template::get_template_by_id(
|
||||
app.db.as_ref().unwrap().conn(),
|
||||
&template.id
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
bds_core::db::queries::post::get_post_by_id(
|
||||
app.db.as_ref().unwrap().conn(),
|
||||
&referencing_post.id
|
||||
)
|
||||
.unwrap()
|
||||
.template_slug,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// sidebar_views.allium ChatListItemEntry provides
|
||||
/// ChatDeleteRequested(item.conversation_id); per DeleteBehaviour and
|
||||
/// action_patterns.allium the delete is immediate (no confirmation) and
|
||||
/// must not depend on an open tab.
|
||||
#[test]
|
||||
fn sidebar_chat_delete_works_without_open_tab() {
|
||||
let (db, project, tmp) = setup();
|
||||
let mut app = make_app(db, project, &tmp);
|
||||
let _ = app.update(Message::ChatCreate);
|
||||
let conversation_id = app.chat_conversations[0].id.clone();
|
||||
|
||||
let _ = app.update(Message::CloseTab(conversation_id.clone()));
|
||||
assert!(!app.tabs.iter().any(|tab| tab.id == conversation_id));
|
||||
|
||||
let _ = app.update(Message::ChatDelete(conversation_id.clone()));
|
||||
assert!(app.active_modal.is_none());
|
||||
assert!(app.chat_conversations.is_empty());
|
||||
assert!(
|
||||
chat::list_conversations(app.db.as_ref().unwrap().conn())
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
/// sidebar_views.allium ImportListItemEntry provides
|
||||
/// ImportDeleteRequested(item.definition_id): deleting from the sidebar
|
||||
/// row confirms first and works without an open editor tab.
|
||||
#[test]
|
||||
fn sidebar_import_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::CreateImport);
|
||||
let definition_id = app.sidebar_imports[0].id.clone();
|
||||
|
||||
let _ = app.update(Message::CloseTab(definition_id.clone()));
|
||||
assert!(!app.tabs.iter().any(|tab| tab.id == definition_id));
|
||||
|
||||
let _ = app.update(Message::ImportDeleteRequested(definition_id.clone()));
|
||||
assert!(matches!(
|
||||
app.active_modal,
|
||||
Some(modal::ModalState::Confirm {
|
||||
on_confirm: modal::ConfirmAction::DeleteImport(ref id),
|
||||
..
|
||||
}) if id == &definition_id
|
||||
));
|
||||
assert!(
|
||||
wordpress_import::get_definition(app.db.as_ref().unwrap().conn(), &definition_id)
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
let _ = app.update(Message::ConfirmModal(modal::ConfirmAction::DeleteImport(
|
||||
definition_id.clone(),
|
||||
)));
|
||||
assert!(
|
||||
wordpress_import::get_definition(app.db.as_ref().unwrap().conn(), &definition_id)
|
||||
.is_err()
|
||||
);
|
||||
assert!(app.sidebar_imports.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuild_completion_refreshes_project_metadata_in_settings() {
|
||||
let (db, project, tmp) = setup();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod relative_date;
|
||||
|
||||
use bds_core::i18n::{UiLocale, translate, translate_with};
|
||||
|
||||
/// Shorthand for translate in view code.
|
||||
|
||||
163
crates/bds-ui/src/i18n/relative_date.rs
Normal file
163
crates/bds-ui/src/i18n/relative_date.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use bds_core::i18n::{UiLocale, translate, translate_with};
|
||||
use chrono::{DateTime, Datelike, Local, NaiveDate};
|
||||
|
||||
/// chrono format locale per sidebar_views.allium LocaleMapping
|
||||
/// (ui_locale → format_locale, e.g. "de" → "de-DE").
|
||||
fn format_locale(locale: UiLocale) -> chrono::Locale {
|
||||
match locale {
|
||||
UiLocale::En => chrono::Locale::en_US,
|
||||
UiLocale::De => chrono::Locale::de_DE,
|
||||
UiLocale::Fr => chrono::Locale::fr_FR,
|
||||
UiLocale::It => chrono::Locale::it_IT,
|
||||
UiLocale::Es => chrono::Locale::es_ES,
|
||||
}
|
||||
}
|
||||
|
||||
/// Per sidebar_views.allium RelativeDateFormat, with
|
||||
/// diff_days = (today - timestamp.date).days on local calendar dates:
|
||||
/// diff_days = 0 → locale time string
|
||||
/// diff_days = 1 → localized "Yesterday"
|
||||
/// diff_days < 7 → short weekday name
|
||||
/// otherwise → short month name + numeric day
|
||||
pub fn format_relative_date(locale: UiLocale, unix_ms: i64, today: NaiveDate) -> String {
|
||||
let timestamp = DateTime::from_timestamp_millis(unix_ms)
|
||||
.unwrap_or_default()
|
||||
.with_timezone(&Local);
|
||||
let chrono_locale = format_locale(locale);
|
||||
let diff_days = (today - timestamp.date_naive()).num_days();
|
||||
|
||||
if diff_days == 0 {
|
||||
timestamp.format_localized("%X", chrono_locale).to_string()
|
||||
} else if diff_days == 1 {
|
||||
translate(locale, "sidebar.chatYesterday")
|
||||
} else if diff_days < 7 {
|
||||
timestamp.format_localized("%a", chrono_locale).to_string()
|
||||
} else {
|
||||
let month = timestamp.format_localized("%b", chrono_locale).to_string();
|
||||
let day = timestamp.day().to_string();
|
||||
translate_with(
|
||||
locale,
|
||||
"sidebar.relativeDateMonthDay",
|
||||
&[("month", &month), ("day", &day)],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::TimeZone;
|
||||
|
||||
fn local_ms(y: i32, mo: u32, d: u32, h: u32, mi: u32, s: u32) -> i64 {
|
||||
Local
|
||||
.with_ymd_and_hms(y, mo, d, h, mi, s)
|
||||
.single()
|
||||
.expect("unambiguous local time")
|
||||
.timestamp_millis()
|
||||
}
|
||||
|
||||
fn day(y: i32, mo: u32, d: u32) -> NaiveDate {
|
||||
NaiveDate::from_ymd_opt(y, mo, d).expect("valid date")
|
||||
}
|
||||
|
||||
// Reference "today" throughout: Tuesday, 2026-02-17.
|
||||
const TODAY: (i32, u32, u32) = (2026, 2, 17);
|
||||
|
||||
fn today() -> NaiveDate {
|
||||
day(TODAY.0, TODAY.1, TODAY.2)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_day_shows_locale_time() {
|
||||
let ts = local_ms(2026, 2, 17, 14, 5, 7);
|
||||
let en = format_relative_date(UiLocale::En, ts, today());
|
||||
assert!(en.contains("05:07"), "expected time string, got {en}");
|
||||
assert!(en.contains("PM"), "en-US uses 12-hour clock, got {en}");
|
||||
let de = format_relative_date(UiLocale::De, ts, today());
|
||||
assert!(de.contains("14:05"), "de-DE uses 24-hour clock, got {de}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_day_ago_is_yesterday_even_under_24_hours() {
|
||||
// 23:59 yesterday vs. any time today: calendar-day diff is 1.
|
||||
let ts = local_ms(2026, 2, 16, 23, 59, 0);
|
||||
assert_eq!(format_relative_date(UiLocale::En, ts, today()), "Yesterday");
|
||||
assert_eq!(format_relative_date(UiLocale::De, ts, today()), "Gestern");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_days_ago_shows_short_weekday() {
|
||||
// 2026-02-15 is a Sunday.
|
||||
let ts = local_ms(2026, 2, 15, 12, 0, 0);
|
||||
assert_eq!(format_relative_date(UiLocale::En, ts, today()), "Sun");
|
||||
assert_eq!(format_relative_date(UiLocale::De, ts, today()), "So");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn six_days_ago_still_shows_weekday() {
|
||||
// 2026-02-11 is a Wednesday: diff_days = 6 is the last weekday case.
|
||||
let ts = local_ms(2026, 2, 11, 12, 0, 0);
|
||||
assert_eq!(format_relative_date(UiLocale::En, ts, today()), "Wed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_days_ago_shows_month_and_day() {
|
||||
// diff_days = 7 is the first month+day case.
|
||||
let ts = local_ms(2026, 2, 10, 12, 0, 0);
|
||||
assert_eq!(format_relative_date(UiLocale::En, ts, today()), "Feb 10");
|
||||
assert_eq!(format_relative_date(UiLocale::De, ts, today()), "10. Feb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn month_day_order_is_day_first_in_romance_locales() {
|
||||
let ts = local_ms(2026, 2, 10, 12, 0, 0);
|
||||
for locale in [UiLocale::Fr, UiLocale::It, UiLocale::Es] {
|
||||
let result = format_relative_date(locale, ts, today());
|
||||
assert!(
|
||||
result.starts_with("10 "),
|
||||
"expected day-first order for {locale}, got {result}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distant_past_shows_month_and_day() {
|
||||
let ts = local_ms(2025, 6, 3, 12, 0, 0);
|
||||
assert_eq!(format_relative_date(UiLocale::En, ts, today()), "Jun 3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn future_timestamp_uses_weekday_branch() {
|
||||
// Spec formula: diff_days < 7 → weekday; a future date has a
|
||||
// negative diff and therefore falls into the weekday case.
|
||||
// 2026-02-20 is a Friday.
|
||||
let ts = local_ms(2026, 2, 20, 12, 0, 0);
|
||||
assert_eq!(format_relative_date(UiLocale::En, ts, today()), "Fri");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yesterday_is_translated_in_all_locales() {
|
||||
let ts = local_ms(2026, 2, 16, 12, 0, 0);
|
||||
for locale in UiLocale::all() {
|
||||
let result = format_relative_date(*locale, ts, today());
|
||||
assert_ne!(
|
||||
result, "sidebar.chatYesterday",
|
||||
"missing yesterday translation for {locale}"
|
||||
);
|
||||
assert!(!result.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn month_day_pattern_exists_in_all_locales() {
|
||||
let ts = local_ms(2025, 6, 3, 12, 0, 0);
|
||||
for locale in UiLocale::all() {
|
||||
let result = format_relative_date(*locale, ts, today());
|
||||
assert_ne!(
|
||||
result, "sidebar.relativeDateMonthDay",
|
||||
"missing month-day pattern for {locale}"
|
||||
);
|
||||
assert!(result.contains('3'), "day missing for {locale}: {result}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use bds_core::model::{ChatConversation, ImportDefinition, Media, Post, Script, T
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::components::inputs;
|
||||
use crate::i18n::relative_date::format_relative_date;
|
||||
use crate::i18n::t;
|
||||
use crate::state::navigation::SidebarView;
|
||||
use crate::state::sidebar_filter::{CalendarYear, MediaFilter, PostFilter};
|
||||
@@ -287,6 +288,32 @@ fn row_delete_style(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap a sidebar row in a hover overlay carrying its delete button (×).
|
||||
/// Per sidebar_views.allium *ListItemEntry RowLayout: right-aligned, visible
|
||||
/// only on row hover, routed to the row's delete message.
|
||||
fn with_row_delete(
|
||||
open_button: iced::widget::Button<'static, Message>,
|
||||
on_delete: Message,
|
||||
) -> Element<'static, Message> {
|
||||
let delete_button = button(
|
||||
text("\u{2715}") // ✕
|
||||
.size(11)
|
||||
.shaping(Shaping::Advanced),
|
||||
)
|
||||
.on_press(on_delete)
|
||||
.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]),
|
||||
)
|
||||
}
|
||||
|
||||
/// Month name abbreviation for calendar display.
|
||||
fn month_abbr(month: u32) -> &'static str {
|
||||
match month {
|
||||
@@ -661,6 +688,8 @@ pub fn view(
|
||||
) -> Element<'static, Message> {
|
||||
let header_text = t(locale, sidebar_view.i18n_key());
|
||||
let muted = Color::from_rgb(0.50, 0.50, 0.55);
|
||||
// Reference date for RelativeDateFormat's diff_days (sidebar_views.allium).
|
||||
let today = chrono::Local::now().date_naive();
|
||||
|
||||
let header = text(header_text)
|
||||
.size(13)
|
||||
@@ -1006,42 +1035,33 @@ pub fn view(
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced)
|
||||
.wrapping(iced::widget::text::Wrapping::None);
|
||||
// sidebar_views.allium ScriptListItemEntry RowLayout:
|
||||
// line 2 is the date in relative format, smaller and muted.
|
||||
let date_text = text(format_relative_date(locale, s.updated_at, today))
|
||||
.size(10)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(muted);
|
||||
let style_fn = if is_active {
|
||||
item_active_style
|
||||
} else {
|
||||
item_style
|
||||
};
|
||||
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])
|
||||
let open_button = button(
|
||||
container(column![label_text, date_text].spacing(1))
|
||||
.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]),
|
||||
.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);
|
||||
with_row_delete(open_button, Message::ScriptDeleteRequested(s.id.clone()))
|
||||
})
|
||||
.collect();
|
||||
iced::widget::Column::with_children(items).spacing(1).into()
|
||||
@@ -1059,29 +1079,43 @@ pub fn view(
|
||||
.iter()
|
||||
.map(|tmpl| {
|
||||
let is_active = active_tab == Some(tmpl.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(&tmpl.title, text_px);
|
||||
let label_text = text(display_title.clone())
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced)
|
||||
.wrapping(iced::widget::text::Wrapping::None);
|
||||
// sidebar_views.allium TemplateListItemEntry RowLayout:
|
||||
// line 2 is the date in relative format, smaller and muted.
|
||||
let date_text = text(format_relative_date(locale, tmpl.updated_at, today))
|
||||
.size(10)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(muted);
|
||||
let style_fn = if is_active {
|
||||
item_active_style
|
||||
} else {
|
||||
item_style
|
||||
};
|
||||
button(container(label_text).width(Length::Fill).clip(true))
|
||||
.on_press(Message::OpenTab(Tab {
|
||||
id: tmpl.id.clone(),
|
||||
tab_type: TabType::Templates,
|
||||
title: display_title,
|
||||
is_transient: true,
|
||||
is_dirty: false,
|
||||
}))
|
||||
.padding([5, 8])
|
||||
.width(Length::Fill)
|
||||
.style(style_fn)
|
||||
.into()
|
||||
let open_button = button(
|
||||
container(column![label_text, date_text].spacing(1))
|
||||
.width(Length::Fill)
|
||||
.clip(true),
|
||||
)
|
||||
.on_press(Message::OpenTab(Tab {
|
||||
id: tmpl.id.clone(),
|
||||
tab_type: TabType::Templates,
|
||||
title: display_title,
|
||||
is_transient: true,
|
||||
is_dirty: false,
|
||||
}))
|
||||
.padding([5, 8])
|
||||
.width(Length::Fill)
|
||||
.style(style_fn);
|
||||
with_row_delete(
|
||||
open_button,
|
||||
Message::TemplateDeleteRequested(tmpl.id.clone()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
iced::widget::Column::with_children(items).spacing(1).into()
|
||||
@@ -1104,19 +1138,18 @@ pub fn view(
|
||||
} else {
|
||||
item_style
|
||||
};
|
||||
let analyzed = if definition.last_analysis_result.is_some() {
|
||||
t(locale, "import.sidebar.analyzed")
|
||||
} else {
|
||||
t(locale, "import.sidebar.pending")
|
||||
};
|
||||
button(
|
||||
// sidebar_views.allium ImportListItemEntry RowLayout:
|
||||
// line 2 is the date in relative format, smaller and muted.
|
||||
let date_text =
|
||||
text(format_relative_date(locale, definition.updated_at, today))
|
||||
.size(10)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(muted);
|
||||
let open_button = button(
|
||||
container(
|
||||
column![
|
||||
text(definition_name).size(12).shaping(Shaping::Advanced),
|
||||
text(analyzed)
|
||||
.size(10)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(Color::from_rgb(0.50, 0.50, 0.55)),
|
||||
date_text,
|
||||
]
|
||||
.spacing(1),
|
||||
)
|
||||
@@ -1131,8 +1164,11 @@ pub fn view(
|
||||
}))
|
||||
.padding([5, 8])
|
||||
.width(Length::Fill)
|
||||
.style(style)
|
||||
.into()
|
||||
.style(style);
|
||||
with_row_delete(
|
||||
open_button,
|
||||
Message::ImportDeleteRequested(definition.id.clone()),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<Element<'static, Message>>>();
|
||||
iced::widget::Column::with_children(items).spacing(1).into()
|
||||
@@ -1154,17 +1190,20 @@ pub fn view(
|
||||
} else {
|
||||
item_style
|
||||
};
|
||||
let model = conversation
|
||||
.model
|
||||
.clone()
|
||||
.unwrap_or_else(|| t(locale, "chat.model.none"));
|
||||
button(
|
||||
// sidebar_views.allium ChatListItemEntry RowLayout:
|
||||
// line 2 is the date in relative format, smaller and muted.
|
||||
let date_text =
|
||||
text(format_relative_date(locale, conversation.updated_at, today))
|
||||
.size(10)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(muted);
|
||||
let open_button = button(
|
||||
container(
|
||||
column![
|
||||
text(conversation.title.clone())
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced),
|
||||
text(model).size(10).shaping(Shaping::Advanced).color(muted),
|
||||
date_text,
|
||||
]
|
||||
.spacing(1),
|
||||
)
|
||||
@@ -1179,8 +1218,11 @@ pub fn view(
|
||||
}))
|
||||
.padding([5, 8])
|
||||
.width(Length::Fill)
|
||||
.style(style)
|
||||
.into()
|
||||
.style(style);
|
||||
// Per action_patterns.allium confirmation_assignments chat
|
||||
// delete has no confirmation; ChatDelete removes the
|
||||
// conversation and closes its tab.
|
||||
with_row_delete(open_button, Message::ChatDelete(conversation.id.clone()))
|
||||
})
|
||||
.collect::<Vec<Element<'static, Message>>>();
|
||||
iced::widget::Column::with_children(items).spacing(1).into()
|
||||
|
||||
@@ -329,6 +329,7 @@ tags-nav-manage = Tags verwalten
|
||||
tags-nav-merge = Tags zusammenführen
|
||||
tags-nav-discover = Tags entdecken
|
||||
sidebar-chatYesterday = Gestern
|
||||
sidebar-relativeDateMonthDay = { $day }. { $month }
|
||||
modal-confirmDelete-title = Löschen bestätigen
|
||||
modal-confirmDelete-warning = Diese Aktion kann nicht rückgängig gemacht werden.
|
||||
modal-confirmDelete-cancel = Abbrechen
|
||||
@@ -763,8 +764,6 @@ import-phase-complete = Import abgeschlossen
|
||||
import-error-wxrRequired = Vor der Analyse eine WXR-Datei auswählen.
|
||||
import-toast-complete = WordPress-Import abgeschlossen.
|
||||
import-toast-mapped = KI hat { $count } Taxonomiebegriffe zugeordnet.
|
||||
import-sidebar-analyzed = Analyse gespeichert
|
||||
import-sidebar-pending = Nicht analysiert
|
||||
import-deleteTitle = Importdefinition löschen?
|
||||
import-deleteMessage = „{ $name }“ und die gespeicherte Analyse löschen? Bereits importierte Projektinhalte bleiben erhalten.
|
||||
import-toast-deleted = Importdefinition gelöscht.
|
||||
|
||||
@@ -329,6 +329,7 @@ tags-nav-manage = Manage Tags
|
||||
tags-nav-merge = Merge Tags
|
||||
tags-nav-discover = Discover Tags
|
||||
sidebar-chatYesterday = Yesterday
|
||||
sidebar-relativeDateMonthDay = { $month } { $day }
|
||||
modal-confirmDelete-title = Confirm Delete
|
||||
modal-confirmDelete-warning = This action cannot be undone.
|
||||
modal-confirmDelete-cancel = Cancel
|
||||
@@ -763,8 +764,6 @@ import-phase-complete = Import complete
|
||||
import-error-wxrRequired = Select a WXR file before analyzing.
|
||||
import-toast-complete = WordPress import completed.
|
||||
import-toast-mapped = AI mapped { $count } taxonomy terms.
|
||||
import-sidebar-analyzed = Analysis saved
|
||||
import-sidebar-pending = Not analyzed
|
||||
import-deleteTitle = Delete import definition?
|
||||
import-deleteMessage = Delete “{ $name }” and its saved analysis? Imported project content is not removed.
|
||||
import-toast-deleted = Import definition deleted.
|
||||
|
||||
@@ -329,6 +329,7 @@ tags-nav-manage = Gestionar etiquetas
|
||||
tags-nav-merge = Fusionar etiquetas
|
||||
tags-nav-discover = Descubrir etiquetas
|
||||
sidebar-chatYesterday = Ayer
|
||||
sidebar-relativeDateMonthDay = { $day } { $month }
|
||||
modal-confirmDelete-title = Confirmar eliminación
|
||||
modal-confirmDelete-warning = Esta acción no se puede deshacer.
|
||||
modal-confirmDelete-cancel = Cancelar
|
||||
@@ -763,8 +764,6 @@ import-phase-complete = Importación completa
|
||||
import-error-wxrRequired = Selecciona un archivo WXR antes de analizar.
|
||||
import-toast-complete = Importación de WordPress completa.
|
||||
import-toast-mapped = La IA asoció { $count } términos de taxonomía.
|
||||
import-sidebar-analyzed = Análisis guardado
|
||||
import-sidebar-pending = Sin analizar
|
||||
import-deleteTitle = ¿Eliminar la definición de importación?
|
||||
import-deleteMessage = ¿Eliminar «{ $name }» y su análisis guardado? El contenido ya importado se conservará.
|
||||
import-toast-deleted = Definición de importación eliminada.
|
||||
|
||||
@@ -329,6 +329,7 @@ tags-nav-manage = Gérer les tags
|
||||
tags-nav-merge = Fusionner les tags
|
||||
tags-nav-discover = Découvrir les tags
|
||||
sidebar-chatYesterday = Hier
|
||||
sidebar-relativeDateMonthDay = { $day } { $month }
|
||||
modal-confirmDelete-title = Confirmer la suppression
|
||||
modal-confirmDelete-warning = Cette action est irréversible.
|
||||
modal-confirmDelete-cancel = Annuler
|
||||
@@ -763,8 +764,6 @@ import-phase-complete = Import terminé
|
||||
import-error-wxrRequired = Sélectionnez un fichier WXR avant l’analyse.
|
||||
import-toast-complete = Import WordPress terminé.
|
||||
import-toast-mapped = L’IA a associé { $count } termes de taxonomie.
|
||||
import-sidebar-analyzed = Analyse enregistrée
|
||||
import-sidebar-pending = Non analysé
|
||||
import-deleteTitle = Supprimer la définition d’import ?
|
||||
import-deleteMessage = Supprimer « { $name } » et son analyse enregistrée ? Le contenu déjà importé sera conservé.
|
||||
import-toast-deleted = Définition d’import supprimée.
|
||||
|
||||
@@ -329,6 +329,7 @@ tags-nav-manage = Gestisci tag
|
||||
tags-nav-merge = Unisci tag
|
||||
tags-nav-discover = Scopri tag
|
||||
sidebar-chatYesterday = Ieri
|
||||
sidebar-relativeDateMonthDay = { $day } { $month }
|
||||
modal-confirmDelete-title = Conferma eliminazione
|
||||
modal-confirmDelete-warning = Questa azione non può essere annullata.
|
||||
modal-confirmDelete-cancel = Annulla
|
||||
@@ -763,8 +764,6 @@ import-phase-complete = Importazione completata
|
||||
import-error-wxrRequired = Seleziona un file WXR prima dell’analisi.
|
||||
import-toast-complete = Importazione WordPress completata.
|
||||
import-toast-mapped = L’IA ha associato { $count } termini di tassonomia.
|
||||
import-sidebar-analyzed = Analisi salvata
|
||||
import-sidebar-pending = Non analizzato
|
||||
import-deleteTitle = Eliminare la definizione di importazione?
|
||||
import-deleteMessage = Eliminare “{ $name }” e l’analisi salvata? I contenuti già importati resteranno invariati.
|
||||
import-toast-deleted = Definizione di importazione eliminata.
|
||||
|
||||
@@ -211,6 +211,8 @@ config {
|
||||
confirmation_assignments: Set<ConfirmationAssignment> = {
|
||||
ConfirmationAssignment(action: "post_delete", pattern: system_confirm),
|
||||
ConfirmationAssignment(action: "post_discard", pattern: system_confirm),
|
||||
ConfirmationAssignment(action: "template_delete", pattern: confirm_delete_modal),
|
||||
-- shows template title, no reference list
|
||||
ConfirmationAssignment(action: "template_delete_with_references", pattern: system_confirm),
|
||||
ConfirmationAssignment(action: "media_delete", pattern: confirm_delete_modal),
|
||||
-- shows linked posts
|
||||
|
||||
@@ -40,6 +40,7 @@ surface TemplateEditorSurface {
|
||||
when editor.can_publish
|
||||
TemplateValidateRequested(editor.template_id)
|
||||
TemplateDeleteRequested(editor.template_id)
|
||||
TemplateDeleteConfirmed(editor.template_id)
|
||||
TemplateForceDeleteConfirmed(editor.template_id)
|
||||
|
||||
@guarantee HeaderLayout
|
||||
@@ -119,9 +120,20 @@ rule TemplateDelete {
|
||||
-- and M tags. Force delete?" (action_patterns.allium
|
||||
-- confirmation_assignments: template_delete_with_references)
|
||||
else:
|
||||
DeleteTemplateRequested(target)
|
||||
closeTab(template_id)
|
||||
-- no confirmation when nothing references the template
|
||||
ConfirmDeleteModalOpened(template_id)
|
||||
-- custom modal (not a native dialog): template title, no
|
||||
-- reference list; Cancel + Delete (destructive red style)
|
||||
-- — action_patterns.allium
|
||||
-- confirmation_assignments: template_delete
|
||||
}
|
||||
|
||||
rule TemplateDeleteExecute {
|
||||
when: TemplateDeleteConfirmed(template_id)
|
||||
let target = template/Template{id: template_id}
|
||||
ensures: DeleteTemplateRequested(target)
|
||||
-- DB record + .liquid file on disk
|
||||
ensures: closeTab(template_id)
|
||||
-- sidebar removes the item reactively
|
||||
}
|
||||
|
||||
rule TemplateForceDelete {
|
||||
|
||||
@@ -412,8 +412,13 @@ surface TemplateListItemEntry {
|
||||
-- Row is focusable with tabIndex and has aria-label from title.
|
||||
|
||||
@guarantee DeleteConfirmation
|
||||
-- If template is referenced by posts or tags, shows confirmation dialog
|
||||
-- with reference counts before force-delete.
|
||||
-- Delete always confirms before executing. Unreferenced templates
|
||||
-- use the confirm-delete modal (template title, no reference list);
|
||||
-- templates referenced by posts or tags use the force-delete
|
||||
-- confirmation dialog with reference counts.
|
||||
-- See editor_template.allium TemplateDelete and action_patterns.allium
|
||||
-- confirmation_assignments (template_delete,
|
||||
-- template_delete_with_references).
|
||||
|
||||
@guarantee CreateDefaults
|
||||
-- New templates default to: kind=post, content='', enabled=true.
|
||||
|
||||
Reference in New Issue
Block a user