feat: completed base feature set for the app
This commit is contained in:
@@ -18,6 +18,7 @@ open = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
wry = "0.55"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,20 @@ pub fn pick_folder(title: String) -> Task<Message> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Pick an existing portable project folder.
|
||||
pub fn pick_project_folder(title: String) -> Task<Message> {
|
||||
Task::perform(
|
||||
async move {
|
||||
rfd::AsyncFileDialog::new()
|
||||
.set_title(&title)
|
||||
.pick_folder()
|
||||
.await
|
||||
.map(|h| h.path().to_path_buf())
|
||||
},
|
||||
Message::ProjectFolderPicked,
|
||||
)
|
||||
}
|
||||
|
||||
/// Pick one or more image/media files using the native file dialog.
|
||||
pub fn pick_media_files(title: String, filter_label: String) -> Task<Message> {
|
||||
Task::perform(
|
||||
@@ -33,3 +47,26 @@ pub fn pick_media_files(title: String, filter_label: String) -> Task<Message> {
|
||||
Message::MediaFilesPicked,
|
||||
)
|
||||
}
|
||||
|
||||
/// Pick a replacement image for an existing media item.
|
||||
pub fn pick_media_replacement(
|
||||
media_id: String,
|
||||
title: String,
|
||||
filter_label: String,
|
||||
) -> Task<Message> {
|
||||
Task::perform(
|
||||
async move {
|
||||
let path = rfd::AsyncFileDialog::new()
|
||||
.set_title(&title)
|
||||
.add_filter(
|
||||
&filter_label,
|
||||
&["jpg", "jpeg", "png", "gif", "webp", "tiff", "bmp"],
|
||||
)
|
||||
.pick_file()
|
||||
.await
|
||||
.map(|handle| handle.path().to_path_buf());
|
||||
(media_id, path)
|
||||
},
|
||||
|(media_id, path)| Message::MediaReplacementPicked { media_id, path },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -220,6 +220,7 @@ pub enum MediaEditorMsg {
|
||||
OpenLinkedPost(String),
|
||||
UnlinkPost(String),
|
||||
Save,
|
||||
ReplaceFile,
|
||||
Delete,
|
||||
}
|
||||
|
||||
@@ -263,6 +264,11 @@ pub fn view<'a>(
|
||||
.style(inputs::primary_button)
|
||||
.padding([6, 16])
|
||||
.into(),
|
||||
button(text(t(locale, "editor.replaceFile")).size(13))
|
||||
.on_press(Message::MediaEditor(MediaEditorMsg::ReplaceFile))
|
||||
.style(inputs::secondary_button)
|
||||
.padding([6, 16])
|
||||
.into(),
|
||||
button(text(t(locale, "modal.confirmDelete.delete")).size(13))
|
||||
.on_press(Message::MediaEditor(MediaEditorMsg::Delete))
|
||||
.style(inputs::danger_button)
|
||||
|
||||
153
crates/bds-ui/src/views/metadata_diff.rs
Normal file
153
crates/bds-ui/src/views/metadata_diff.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use iced::widget::text::Shaping;
|
||||
use iced::widget::{Space, button, column, container, row, scrollable, text};
|
||||
use iced::{Color, Element, Length};
|
||||
|
||||
use bds_core::engine::metadata_diff::{DiffReport, RepairDirection};
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::components::inputs;
|
||||
use crate::i18n::{t, tw};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MetadataDiffState {
|
||||
pub is_running: bool,
|
||||
pub is_repairing: bool,
|
||||
pub report: Option<DiffReport>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
pub fn view<'a>(state: &'a MetadataDiffState, locale: UiLocale) -> Element<'a, Message> {
|
||||
let run = button(text(t(locale, "metadataDiff.run")).size(13))
|
||||
.on_press_maybe(
|
||||
(!state.is_running && !state.is_repairing).then_some(Message::RunMetadataDiff),
|
||||
)
|
||||
.style(inputs::primary_button)
|
||||
.padding([6, 16]);
|
||||
let mut content = column![
|
||||
row![
|
||||
text(t(locale, "tabBar.metadataDiff"))
|
||||
.size(24)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(Color::from_rgb(0.88, 0.88, 0.92)),
|
||||
Space::with_width(Length::Fill),
|
||||
run,
|
||||
]
|
||||
.align_y(iced::Alignment::Center),
|
||||
]
|
||||
.spacing(16);
|
||||
|
||||
if state.is_running {
|
||||
content = content.push(message_card(t(locale, "metadataDiff.running")));
|
||||
} else if let Some(error) = &state.error_message {
|
||||
content = content.push(message_card(error.clone()));
|
||||
} else if let Some(report) = &state.report {
|
||||
if report.diffs.is_empty() && report.orphans.is_empty() && report.errors.is_empty() {
|
||||
content = content.push(message_card(t(locale, "metadataDiff.clean")));
|
||||
}
|
||||
for (index, item) in report.diffs.iter().enumerate() {
|
||||
let fields = item
|
||||
.fields
|
||||
.iter()
|
||||
.fold(column!().spacing(4), |column, field| {
|
||||
column.push(
|
||||
text(tw(
|
||||
locale,
|
||||
"metadataDiff.field",
|
||||
&[
|
||||
("field", &field.field_name),
|
||||
("database", &field.db_value),
|
||||
("file", &field.file_value),
|
||||
],
|
||||
))
|
||||
.size(12)
|
||||
.color(Color::from_rgb(0.72, 0.72, 0.78)),
|
||||
)
|
||||
});
|
||||
let actions = row![
|
||||
button(text(t(locale, "metadataDiff.fileToDb")).size(12))
|
||||
.on_press_maybe((!state.is_repairing).then_some(
|
||||
Message::RepairMetadataDiffItem {
|
||||
index,
|
||||
direction: RepairDirection::FileToDatabase,
|
||||
},
|
||||
))
|
||||
.style(inputs::secondary_button)
|
||||
.padding([5, 10]),
|
||||
button(text(t(locale, "metadataDiff.dbToFile")).size(12))
|
||||
.on_press_maybe((!state.is_repairing).then_some(
|
||||
Message::RepairMetadataDiffItem {
|
||||
index,
|
||||
direction: RepairDirection::DatabaseToFile,
|
||||
},
|
||||
))
|
||||
.style(inputs::secondary_button)
|
||||
.padding([5, 10]),
|
||||
]
|
||||
.spacing(8);
|
||||
content = content.push(inputs::card(
|
||||
column![
|
||||
text(tw(
|
||||
locale,
|
||||
"metadataDiff.entity",
|
||||
&[("entity", &item.entity_type), ("path", &item.file_path)],
|
||||
))
|
||||
.size(15)
|
||||
.color(Color::from_rgb(0.86, 0.86, 0.92)),
|
||||
fields,
|
||||
actions,
|
||||
]
|
||||
.spacing(10),
|
||||
));
|
||||
}
|
||||
if !report.orphans.is_empty() {
|
||||
let rows = report
|
||||
.orphans
|
||||
.iter()
|
||||
.fold(column!().spacing(4), |column, orphan| {
|
||||
column.push(
|
||||
text(tw(
|
||||
locale,
|
||||
"metadataDiff.orphan",
|
||||
&[("reason", &orphan.reason), ("path", &orphan.file_path)],
|
||||
))
|
||||
.size(12),
|
||||
)
|
||||
});
|
||||
content = content.push(inputs::card(
|
||||
column![text(t(locale, "metadataDiff.orphans")).size(16), rows].spacing(8),
|
||||
));
|
||||
}
|
||||
if !report.errors.is_empty() {
|
||||
content = content.push(inputs::card(
|
||||
column![
|
||||
text(t(locale, "metadataDiff.errors")).size(16),
|
||||
report
|
||||
.errors
|
||||
.iter()
|
||||
.fold(column!().spacing(4), |column, error| {
|
||||
column.push(text(error.clone()).size(12))
|
||||
}),
|
||||
]
|
||||
.spacing(8),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
content = content.push(message_card(t(locale, "metadataDiff.idle")));
|
||||
}
|
||||
|
||||
container(scrollable(content))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(24)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn message_card(value: String) -> Element<'static, Message> {
|
||||
inputs::card(
|
||||
text(value)
|
||||
.size(14)
|
||||
.color(Color::from_rgb(0.66, 0.66, 0.72)),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod activity_bar;
|
||||
pub mod dashboard;
|
||||
pub mod media_editor;
|
||||
pub mod metadata_diff;
|
||||
pub mod modal;
|
||||
pub mod panel;
|
||||
pub mod post_editor;
|
||||
@@ -14,5 +15,6 @@ pub mod tab_bar;
|
||||
pub mod tags_view;
|
||||
pub mod template_editor;
|
||||
pub mod toast;
|
||||
pub mod translation_validation;
|
||||
pub mod welcome;
|
||||
pub mod workspace;
|
||||
|
||||
@@ -65,6 +65,11 @@ pub enum ModalState {
|
||||
},
|
||||
SearchIndexRepair,
|
||||
SearchIndexRebuilding,
|
||||
FindReplace {
|
||||
query: String,
|
||||
replacement: String,
|
||||
show_replace: bool,
|
||||
},
|
||||
/// Per modals.allium InsertPostLinkModal: Ctrl+K link insertion.
|
||||
PostInsertLink {
|
||||
post_id: String,
|
||||
@@ -369,6 +374,60 @@ pub fn view(
|
||||
.style(modal_box_style)
|
||||
.into()
|
||||
}
|
||||
ModalState::FindReplace {
|
||||
query,
|
||||
replacement,
|
||||
show_replace,
|
||||
} => {
|
||||
let find = text_input(&t(locale, "find.query"), &query)
|
||||
.on_input(Message::FindQueryChanged)
|
||||
.on_submit(Message::FindNext)
|
||||
.style(inputs::field_style);
|
||||
let mut fields = column![find].spacing(8);
|
||||
if show_replace {
|
||||
fields = fields.push(
|
||||
text_input(&t(locale, "find.replacement"), &replacement)
|
||||
.on_input(Message::ReplaceQueryChanged)
|
||||
.style(inputs::field_style),
|
||||
);
|
||||
}
|
||||
let mut actions = row![
|
||||
button(text(t(locale, "find.next")))
|
||||
.on_press(Message::FindNext)
|
||||
.style(inputs::primary_button),
|
||||
]
|
||||
.spacing(8);
|
||||
if show_replace {
|
||||
actions = actions
|
||||
.push(
|
||||
button(text(t(locale, "find.replace")))
|
||||
.on_press(Message::ReplaceCurrent)
|
||||
.style(inputs::secondary_button),
|
||||
)
|
||||
.push(
|
||||
button(text(t(locale, "find.replaceAll")))
|
||||
.on_press(Message::ReplaceAll)
|
||||
.style(inputs::secondary_button),
|
||||
);
|
||||
}
|
||||
container(
|
||||
column![
|
||||
text(if show_replace {
|
||||
t(locale, "find.titleReplace")
|
||||
} else {
|
||||
t(locale, "find.titleFind")
|
||||
})
|
||||
.size(16),
|
||||
fields,
|
||||
actions,
|
||||
]
|
||||
.spacing(12),
|
||||
)
|
||||
.width(Length::Fixed(440.0))
|
||||
.padding(20)
|
||||
.style(modal_box_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
ModalState::SearchIndexRebuilding => container(
|
||||
column![
|
||||
|
||||
@@ -1200,7 +1200,7 @@ mod tests {
|
||||
fn unsupported_default_mode_falls_back_to_markdown() {
|
||||
let mut state = sample_state();
|
||||
|
||||
state.set_editor_mode("wysiwyg");
|
||||
state.set_editor_mode("visual");
|
||||
assert_eq!(state.editor_mode, "markdown");
|
||||
|
||||
state.set_editor_mode("preview");
|
||||
|
||||
@@ -168,7 +168,27 @@ pub fn view(
|
||||
.into(),
|
||||
);
|
||||
|
||||
// New project button
|
||||
// Open and create project actions
|
||||
items.push(
|
||||
button(
|
||||
row![
|
||||
text("↗")
|
||||
.size(14)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(Color::from_rgb(0.55, 0.75, 0.95)),
|
||||
text(t(locale, "projectSelector.openProject"))
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced),
|
||||
]
|
||||
.spacing(6),
|
||||
)
|
||||
.on_press(Message::RequestOpenProject)
|
||||
.padding([4, 8])
|
||||
.width(Length::Fill)
|
||||
.style(new_project_btn)
|
||||
.into(),
|
||||
);
|
||||
|
||||
items.push(
|
||||
button(
|
||||
row![
|
||||
|
||||
@@ -205,12 +205,8 @@ pub fn view<'a>(state: &'a ScriptEditorState, locale: UiLocale) -> Element<'a, M
|
||||
.align_y(iced::Alignment::End)
|
||||
.width(Length::Fill);
|
||||
|
||||
// Content editor (CodeEditor with syntax highlighting based on file extension)
|
||||
let syntax_ext = if state.file_path.ends_with(".py") {
|
||||
"py"
|
||||
} else {
|
||||
"lua"
|
||||
};
|
||||
// Content editor (Lua is the only scripting runtime).
|
||||
let syntax_ext = "lua";
|
||||
let content_section: Element<'a, Message> = inputs::card(
|
||||
column![
|
||||
inputs::section_header(&t(locale, "editor.content")),
|
||||
|
||||
@@ -111,6 +111,7 @@ pub struct SettingsViewState {
|
||||
pub available_languages: Vec<String>,
|
||||
pub default_author: String,
|
||||
pub max_posts_per_page: String,
|
||||
pub image_import_concurrency: String,
|
||||
pub blogmark_category: String,
|
||||
// Editor
|
||||
pub default_mode: String,
|
||||
@@ -167,6 +168,7 @@ impl Clone for SettingsViewState {
|
||||
available_languages: self.available_languages.clone(),
|
||||
default_author: self.default_author.clone(),
|
||||
max_posts_per_page: self.max_posts_per_page.clone(),
|
||||
image_import_concurrency: self.image_import_concurrency.clone(),
|
||||
blogmark_category: self.blogmark_category.clone(),
|
||||
default_mode: self.default_mode.clone(),
|
||||
diff_view_style: self.diff_view_style.clone(),
|
||||
@@ -218,6 +220,7 @@ impl Default for SettingsViewState {
|
||||
],
|
||||
default_author: String::new(),
|
||||
max_posts_per_page: "50".to_string(),
|
||||
image_import_concurrency: "4".to_string(),
|
||||
blogmark_category: String::new(),
|
||||
default_mode: "markdown".to_string(),
|
||||
diff_view_style: "inline".to_string(),
|
||||
@@ -287,6 +290,7 @@ pub enum SettingsMsg {
|
||||
ToggleBlogLanguage(String),
|
||||
DefaultAuthorChanged(String),
|
||||
MaxPostsPerPageChanged(String),
|
||||
ImageImportConcurrencyChanged(String),
|
||||
BlogmarkCategoryChanged(String),
|
||||
SaveProject,
|
||||
// Editor
|
||||
@@ -524,6 +528,12 @@ fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
|
||||
&state.max_posts_per_page,
|
||||
|s| Message::Settings(SettingsMsg::MaxPostsPerPageChanged(s)),
|
||||
);
|
||||
let image_import_concurrency = inputs::labeled_input(
|
||||
&t(locale, "settings.imageImportConcurrency"),
|
||||
"4",
|
||||
&state.image_import_concurrency,
|
||||
|s| Message::Settings(SettingsMsg::ImageImportConcurrencyChanged(s)),
|
||||
);
|
||||
let blogmark_category = inputs::labeled_select(
|
||||
&t(locale, "settings.blogmarkCategory"),
|
||||
&state.categories,
|
||||
@@ -554,6 +564,7 @@ fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
|
||||
.spacing(4),
|
||||
author,
|
||||
max_posts,
|
||||
image_import_concurrency,
|
||||
blogmark_category,
|
||||
save,
|
||||
]
|
||||
@@ -563,11 +574,7 @@ fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
|
||||
}
|
||||
|
||||
fn section_editor<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, Message> {
|
||||
let mode_options = vec![
|
||||
"wysiwyg".to_string(),
|
||||
"markdown".to_string(),
|
||||
"preview".to_string(),
|
||||
];
|
||||
let mode_options = vec!["markdown".to_string(), "preview".to_string()];
|
||||
let diff_options = vec!["inline".to_string(), "side-by-side".to_string()];
|
||||
let mode = inputs::labeled_select(
|
||||
&t(locale, "settings.defaultMode"),
|
||||
|
||||
114
crates/bds-ui/src/views/translation_validation.rs
Normal file
114
crates/bds-ui/src/views/translation_validation.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use iced::widget::text::Shaping;
|
||||
use iced::widget::{Space, button, column, container, row, scrollable, text};
|
||||
use iced::{Color, Element, Length};
|
||||
|
||||
use bds_core::engine::validate_translations::{
|
||||
TranslationIssue, TranslationIssueKind, TranslationValidationReport,
|
||||
};
|
||||
use bds_core::i18n::UiLocale;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::components::inputs;
|
||||
use crate::i18n::t;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TranslationValidationState {
|
||||
pub is_running: bool,
|
||||
pub report: Option<TranslationValidationReport>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
pub fn view<'a>(state: &'a TranslationValidationState, locale: UiLocale) -> Element<'a, Message> {
|
||||
let run = button(text(t(locale, "translationValidation.run")).size(13))
|
||||
.on_press_maybe((!state.is_running).then_some(Message::ValidateTranslations))
|
||||
.style(inputs::primary_button)
|
||||
.padding([6, 16]);
|
||||
let mut content = column![
|
||||
row![
|
||||
text(t(locale, "tabBar.translationValidation"))
|
||||
.size(24)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(Color::from_rgb(0.88, 0.88, 0.92)),
|
||||
Space::with_width(Length::Fill),
|
||||
run,
|
||||
]
|
||||
.align_y(iced::Alignment::Center),
|
||||
]
|
||||
.spacing(16);
|
||||
|
||||
if state.is_running {
|
||||
content = content.push(message_card(t(locale, "translationValidation.running")));
|
||||
} else if let Some(error) = &state.error_message {
|
||||
content = content.push(message_card(error.clone()));
|
||||
} else if let Some(report) = &state.report {
|
||||
if report.db_issues.is_empty() && report.fs_issues.is_empty() {
|
||||
content = content.push(message_card(t(locale, "translationValidation.clean")));
|
||||
} else {
|
||||
content = content.push(issue_section(
|
||||
t(locale, "translationValidation.databaseIssues"),
|
||||
&report.db_issues,
|
||||
locale,
|
||||
));
|
||||
content = content.push(issue_section(
|
||||
t(locale, "translationValidation.filesystemIssues"),
|
||||
&report.fs_issues,
|
||||
locale,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
content = content.push(message_card(t(locale, "translationValidation.idle")));
|
||||
}
|
||||
|
||||
container(scrollable(content))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(24)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn issue_section<'a>(
|
||||
title: String,
|
||||
issues: &'a [TranslationIssue],
|
||||
locale: UiLocale,
|
||||
) -> Element<'a, Message> {
|
||||
if issues.is_empty() {
|
||||
return Space::new(0, 0).into();
|
||||
}
|
||||
let rows = issues.iter().fold(column!().spacing(6), |column, issue| {
|
||||
let location = issue.file_path.as_deref().unwrap_or(issue.post_id.as_str());
|
||||
column.push(
|
||||
text(format!(
|
||||
"{} · {} · {}",
|
||||
issue.language,
|
||||
location,
|
||||
issue_kind(locale, &issue.kind)
|
||||
))
|
||||
.size(12),
|
||||
)
|
||||
});
|
||||
inputs::card(column![text(title).size(16), rows].spacing(8)).into()
|
||||
}
|
||||
|
||||
fn issue_kind(locale: UiLocale, kind: &TranslationIssueKind) -> String {
|
||||
let key = match kind {
|
||||
TranslationIssueKind::MissingSourcePost => "translationValidation.missingSourcePost",
|
||||
TranslationIssueKind::SameLanguageAsCanonical => {
|
||||
"translationValidation.sameLanguageAsCanonical"
|
||||
}
|
||||
TranslationIssueKind::DoNotTranslateHasTranslations => {
|
||||
"translationValidation.doNotTranslateHasTranslations"
|
||||
}
|
||||
TranslationIssueKind::ContentInDatabase => "translationValidation.contentInDatabase",
|
||||
TranslationIssueKind::MissingTranslation => "translationValidation.missingTranslation",
|
||||
};
|
||||
t(locale, key)
|
||||
}
|
||||
|
||||
fn message_card<'a>(message: String) -> Element<'a, Message> {
|
||||
inputs::card(
|
||||
text(message)
|
||||
.size(13)
|
||||
.color(Color::from_rgb(0.72, 0.72, 0.78)),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
@@ -17,6 +17,7 @@ use crate::views::{
|
||||
activity_bar,
|
||||
dashboard::DashboardState,
|
||||
media_editor::{self, MediaEditorState},
|
||||
metadata_diff::{self, MetadataDiffState},
|
||||
modal, panel,
|
||||
post_editor::{self, PostEditorState},
|
||||
project_selector,
|
||||
@@ -27,7 +28,9 @@ use crate::views::{
|
||||
status_bar, tab_bar,
|
||||
tags_view::{self, TagsViewState},
|
||||
template_editor::{self, TemplateEditorState},
|
||||
toast, welcome,
|
||||
toast,
|
||||
translation_validation::{self, TranslationValidationState},
|
||||
welcome,
|
||||
};
|
||||
|
||||
/// Main content area background.
|
||||
@@ -117,6 +120,8 @@ pub fn view<'a>(
|
||||
settings_state: Option<&'a SettingsViewState>,
|
||||
dashboard_state: Option<&'a DashboardState>,
|
||||
site_validation_state: &'a SiteValidationState,
|
||||
metadata_diff_state: &'a MetadataDiffState,
|
||||
translation_validation_state: &'a TranslationValidationState,
|
||||
) -> Element<'a, Message> {
|
||||
// Activity bar (leftmost column)
|
||||
let activity = activity_bar::view(sidebar_view, sidebar_visible, locale);
|
||||
@@ -140,6 +145,8 @@ pub fn view<'a>(
|
||||
settings_state,
|
||||
dashboard_state,
|
||||
site_validation_state,
|
||||
metadata_diff_state,
|
||||
translation_validation_state,
|
||||
);
|
||||
|
||||
// Right column: tab bar + content + panel
|
||||
@@ -368,6 +375,8 @@ fn route_content_area<'a>(
|
||||
settings_state: Option<&'a SettingsViewState>,
|
||||
dashboard_state: Option<&'a DashboardState>,
|
||||
site_validation_state: &'a SiteValidationState,
|
||||
metadata_diff_state: &'a MetadataDiffState,
|
||||
translation_validation_state: &'a TranslationValidationState,
|
||||
) -> Element<'a, Message> {
|
||||
match route_kind(
|
||||
tabs,
|
||||
@@ -439,6 +448,10 @@ fn route_content_area<'a>(
|
||||
}
|
||||
}
|
||||
ContentRoute::SiteValidation => site_validation::view(site_validation_state, locale),
|
||||
ContentRoute::MetadataDiff => metadata_diff::view(metadata_diff_state, locale),
|
||||
ContentRoute::TranslationValidation => {
|
||||
translation_validation::view(translation_validation_state, locale)
|
||||
}
|
||||
ContentRoute::Placeholder(title) => welcome::tab_placeholder(locale, title, None),
|
||||
}
|
||||
}
|
||||
@@ -455,6 +468,8 @@ enum ContentRoute<'a> {
|
||||
Tags,
|
||||
Settings,
|
||||
SiteValidation,
|
||||
MetadataDiff,
|
||||
TranslationValidation,
|
||||
Placeholder(&'a str),
|
||||
}
|
||||
|
||||
@@ -529,16 +544,16 @@ fn route_kind<'a>(
|
||||
}
|
||||
}
|
||||
TabType::SiteValidation => ContentRoute::SiteValidation,
|
||||
TabType::MetadataDiff => ContentRoute::MetadataDiff,
|
||||
TabType::Style
|
||||
| TabType::Chat
|
||||
| TabType::Import
|
||||
| TabType::MenuEditor
|
||||
| TabType::MetadataDiff
|
||||
| TabType::GitDiff
|
||||
| TabType::Documentation
|
||||
| TabType::ApiDocumentation
|
||||
| TabType::TranslationValidation
|
||||
| TabType::FindDuplicates => ContentRoute::Placeholder(&tab.title),
|
||||
TabType::TranslationValidation => ContentRoute::TranslationValidation,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,11 +613,9 @@ mod tests {
|
||||
TabType::Chat,
|
||||
TabType::Import,
|
||||
TabType::MenuEditor,
|
||||
TabType::MetadataDiff,
|
||||
TabType::GitDiff,
|
||||
TabType::Documentation,
|
||||
TabType::ApiDocumentation,
|
||||
TabType::TranslationValidation,
|
||||
TabType::FindDuplicates,
|
||||
];
|
||||
|
||||
@@ -627,6 +640,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_tabs_route_to_real_views() {
|
||||
let empty_posts = HashMap::new();
|
||||
let empty_media = HashMap::new();
|
||||
let empty_templates = HashMap::new();
|
||||
let empty_scripts = HashMap::new();
|
||||
let site_validation_state = SiteValidationState::default();
|
||||
|
||||
for (tab_type, expected) in [
|
||||
(TabType::MetadataDiff, "metadata diff"),
|
||||
(TabType::TranslationValidation, "translation validation"),
|
||||
] {
|
||||
let tabs = vec![tab("tool", tab_type, "Tool")];
|
||||
let route = route_kind(
|
||||
&tabs,
|
||||
Some("tool"),
|
||||
&empty_posts,
|
||||
&empty_media,
|
||||
&empty_templates,
|
||||
&empty_scripts,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&site_validation_state,
|
||||
);
|
||||
|
||||
let matches_expected = matches!(
|
||||
(route, expected),
|
||||
(ContentRoute::MetadataDiff, "metadata diff")
|
||||
| (
|
||||
ContentRoute::TranslationValidation,
|
||||
"translation validation"
|
||||
)
|
||||
);
|
||||
assert!(matches_expected, "expected {expected} route");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dashboard_is_used_when_available_and_no_tab_is_active() {
|
||||
let dashboard = DashboardState::new("Test Project".to_string());
|
||||
|
||||
Reference in New Issue
Block a user