fix: and evren more gaps in M3
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user