fix: and evren more gaps in M3

This commit is contained in:
2026-04-09 16:11:24 +02:00
parent 476dc99aac
commit 6955defbcf
11 changed files with 889 additions and 126 deletions

View File

@@ -156,9 +156,6 @@ impl MediaEditorState {
/// Build translation flags for the view.
pub fn translation_flags(&self) -> Vec<TranslationFlag> {
if self.translation_drafts.is_empty() {
return Vec::new();
}
let mut flags = Vec::new();
let canon = &self.canonical_language;
let canon_locale = i18n::normalize_language(canon);
@@ -168,15 +165,29 @@ impl MediaEditorState {
status: "canonical".to_string(),
is_active: self.active_language == *canon,
});
let mut langs: Vec<&String> = self.translation_drafts.keys().collect();
let mut langs: Vec<String> = self
.blog_languages
.iter()
.filter(|lang| **lang != *canon)
.cloned()
.collect();
for lang in self.translation_drafts.keys() {
if lang != canon && !langs.contains(lang) {
langs.push(lang.clone());
}
}
langs.sort();
for lang in langs {
let locale = i18n::normalize_language(lang);
let locale = i18n::normalize_language(&lang);
flags.push(TranslationFlag {
language: lang.clone(),
flag_emoji: locale.flag_emoji().to_string(),
status: "translation".to_string(),
is_active: self.active_language == **lang,
status: if self.translation_drafts.contains_key(&lang) {
"translation".to_string()
} else {
"missing".to_string()
},
is_active: self.active_language == lang,
});
}
flags
@@ -448,6 +459,50 @@ pub fn view<'a>(
.into()
}
#[cfg(test)]
mod tests {
use super::MediaEditorState;
use bds_core::model::Media;
fn make_media() -> Media {
Media {
id: "m1".to_string(),
project_id: "proj".to_string(),
filename: "image.png".to_string(),
original_name: "image.png".to_string(),
mime_type: "image/png".to_string(),
size: 10,
width: Some(1),
height: Some(1),
title: Some("Image".to_string()),
alt: Some("Alt".to_string()),
caption: None,
author: None,
language: Some("en".to_string()),
file_path: "media/test/image.png".to_string(),
sidecar_path: "media/test/image.png.meta".to_string(),
checksum: None,
tags: Vec::new(),
created_at: 0,
updated_at: 0,
}
}
#[test]
fn translation_flags_include_configured_languages_without_translations() {
let state = MediaEditorState::from_media(
&make_media(),
&["en".to_string(), "de".to_string()],
&[],
Vec::new(),
);
let flags = state.translation_flags();
let languages = flags.into_iter().map(|flag| flag.language).collect::<Vec<_>>();
assert_eq!(languages, vec!["en".to_string(), "de".to_string()]);
}
}
fn no_preview<'a>() -> Element<'a, Message> {
container(
text("No preview available")

View File

@@ -276,14 +276,9 @@ impl PostEditorState {
}
/// Build the translation flags list for the view.
/// Flags are driven by the post's actual translations, not blog-level languages.
pub fn translation_flags(&self) -> Vec<TranslationFlag> {
if self.translation_drafts.is_empty() {
return Vec::new();
}
let mut flags = Vec::new();
// Canonical language first (always shown when translations exist)
let canon = &self.canonical_language;
let canon_locale = i18n::normalize_language(canon);
flags.push(TranslationFlag {
@@ -293,20 +288,30 @@ impl PostEditorState {
is_active: self.active_language == *canon,
});
// Each existing translation for this post
let mut langs: Vec<&String> = self.translation_drafts.keys().collect();
let mut langs: Vec<String> = self
.blog_languages
.iter()
.filter(|lang| **lang != *canon)
.cloned()
.collect();
for lang in self.translation_drafts.keys() {
if lang != canon && !langs.contains(lang) {
langs.push(lang.clone());
}
}
langs.sort();
for lang in langs {
let locale = i18n::normalize_language(lang);
let status = match self.translation_drafts[lang].status {
PostStatus::Published => "published",
_ => "draft",
let locale = i18n::normalize_language(&lang);
let status = match self.translation_drafts.get(&lang).map(|draft| &draft.status) {
Some(PostStatus::Published) => "published",
Some(_) => "draft",
None => "missing",
};
flags.push(TranslationFlag {
language: lang.clone(),
flag_emoji: locale.flag_emoji().to_string(),
status: status.to_string(),
is_active: self.active_language == **lang,
is_active: self.active_language == lang,
});
}

View File

@@ -386,6 +386,50 @@ fn chip_selector(
iced::widget::Column::with_children(items).spacing(2).into()
}
fn single_select_chip_selector(
label: &str,
available: &[(String, String)],
selected: Option<&str>,
on_toggle: impl Fn(Option<String>) -> Message + 'static + Clone,
locale: UiLocale,
) -> Element<'static, Message> {
let muted = Color::from_rgb(0.50, 0.50, 0.55);
let mut items: Vec<Element<'static, Message>> = Vec::new();
items.push(
text(t(locale, label))
.size(10)
.shaping(Shaping::Advanced)
.color(muted)
.into(),
);
let chips = available
.iter()
.map(|(value, display)| {
let is_selected = selected == Some(value.as_str());
let style_fn = if is_selected { chip_selected_style } else { chip_style };
let value = value.clone();
let on_toggle = on_toggle.clone();
button(text(display.clone()).size(10).shaping(Shaping::Advanced))
.on_press(if is_selected {
on_toggle(None)
} else {
on_toggle(Some(value))
})
.padding([2, 6])
.style(style_fn)
.into()
})
.collect::<Vec<Element<'static, Message>>>();
if !chips.is_empty() {
items.push(row(chips).spacing(4).into());
}
iced::widget::Column::with_children(items).spacing(2).into()
}
/// Build the filter panel for Posts/Pages view.
fn post_filter_panel(
filter: &PostFilter,
@@ -394,6 +438,35 @@ fn post_filter_panel(
) -> Element<'static, Message> {
let mut sections: Vec<Element<'static, Message>> = Vec::new();
sections.push(single_select_chip_selector(
"sidebar.filter.status",
&[
("draft".to_string(), t(locale, "sidebar.drafts")),
("published".to_string(), t(locale, "sidebar.published")),
("archived".to_string(), t(locale, "sidebar.archived")),
],
filter.status_filter.as_deref(),
Message::SetPostStatusFilter,
locale,
));
sections.push(Space::with_height(4.0).into());
if !filter.available_languages.is_empty() {
let languages = filter
.available_languages
.iter()
.map(|lang| (lang.clone(), lang.to_uppercase()))
.collect::<Vec<_>>();
sections.push(single_select_chip_selector(
"sidebar.filter.language",
&languages,
filter.language_filter.as_deref(),
Message::SetPostLanguageFilter,
locale,
));
sections.push(Space::with_height(4.0).into());
}
// Calendar archive
if !filter.calendar_years.is_empty() {
sections.push(calendar_widget(
@@ -431,6 +504,33 @@ fn post_filter_panel(
sections.push(Space::with_height(4.0).into());
}
sections.push(
iced::widget::Column::with_children(vec![
text(t(locale, "sidebar.filter.dateRange"))
.size(10)
.shaping(Shaping::Advanced)
.color(Color::from_rgb(0.50, 0.50, 0.55))
.into(),
row![
text_input(&t(locale, "sidebar.filter.from"), &filter.from_date)
.on_input(Message::SetPostFromDate)
.size(10)
.padding([3, 5])
.style(search_input_style),
text_input(&t(locale, "sidebar.filter.to"), &filter.to_date)
.on_input(Message::SetPostToDate)
.size(10)
.padding([3, 5])
.style(search_input_style),
]
.spacing(4)
.into(),
])
.spacing(2)
.into(),
);
sections.push(Space::with_height(4.0).into());
// Clear all filters button
if filter.has_active_filters() {
sections.push(
@@ -520,6 +620,30 @@ pub fn view(
.shaping(Shaping::Advanced)
.color(Color::from_rgb(0.85, 0.85, 0.90));
let create_action = match sidebar_view {
SidebarView::Posts => Some(Message::CreatePost),
SidebarView::Pages => Some(Message::CreatePage),
SidebarView::Media => Some(Message::CreateMedia),
SidebarView::Scripts => Some(Message::CreateScript),
SidebarView::Templates => Some(Message::CreateTemplate),
_ => None,
};
let header_row: Element<'static, Message> = if let Some(action) = create_action {
row![
header,
Space::with_width(Length::Fill),
button(text(t(locale, "common.add")).size(11).shaping(Shaping::Advanced))
.on_press(action)
.padding([3, 8])
.style(filter_button_style),
]
.align_y(iced::Alignment::Center)
.into()
} else {
header.into()
};
let body: Element<'static, Message> = match sidebar_view {
SidebarView::Posts | SidebarView::Pages => {
let is_pages = sidebar_view == SidebarView::Pages;
@@ -1020,7 +1144,7 @@ pub fn view(
};
let content = column![
header,
header_row,
Space::with_height(8.0),
body,
]