Implement complete WordPress WXR import workflow.

This commit is contained in:
2026-07-19 13:17:45 +02:00
parent 9dab0ca57e
commit 429f471e4e
29 changed files with 4927 additions and 26 deletions

View File

@@ -0,0 +1,777 @@
use std::collections::HashSet;
use bds_core::i18n::UiLocale;
use bds_core::model::{
ImportCandidate, ImportDefinition, ImportExecutionResult, ImportItemKind, ImportItemStatus,
ImportPhase, ImportProgress, ImportReport, ImportResolution, TaxonomyKind,
};
use iced::widget::{Space, button, column, container, progress_bar, row, scrollable, text};
use iced::{Alignment, Color, Element, Length};
use crate::app::Message;
use crate::components::inputs;
use crate::i18n::{t, tw};
#[derive(Debug, Clone)]
pub struct ImportEditorState {
pub definition: ImportDefinition,
pub report: Option<ImportReport>,
pub category_options: Vec<String>,
pub tag_options: Vec<String>,
pub expanded: HashSet<ImportSection>,
pub is_analyzing: bool,
pub is_executing: bool,
pub progress: Option<ImportProgress>,
pub result: Option<ImportExecutionResult>,
pub error: Option<String>,
}
impl ImportEditorState {
pub fn new(
definition: ImportDefinition,
category_options: Vec<String>,
tag_options: Vec<String>,
) -> Self {
let report = definition.analysis().ok().flatten();
Self {
definition,
report,
category_options,
tag_options,
expanded: HashSet::from([
ImportSection::Conflicts,
ImportSection::Taxonomy,
ImportSection::Posts,
]),
is_analyzing: false,
is_executing: false,
progress: None,
result: None,
error: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ImportSection {
Conflicts,
Posts,
Pages,
Media,
Taxonomy,
Macros,
}
#[derive(Debug, Clone)]
pub enum ImportEditorMsg {
NameChanged(String),
PickUploads,
PickWxr,
Analyze,
Execute,
AutoMapTaxonomy,
DeleteRequested,
SetResolution {
kind: ImportItemKind,
identity: String,
resolution: ImportResolution,
},
SetTaxonomyMapping {
kind: TaxonomyKind,
source: String,
target: Option<String>,
},
ToggleSection(ImportSection),
}
#[derive(Debug, Clone)]
pub enum ImportAnalysisEvent {
Progress(ImportProgress),
Finished(Box<Result<(ImportDefinition, ImportReport), String>>),
}
#[derive(Debug, Clone)]
pub enum ImportExecutionEvent {
Progress(ImportProgress),
Finished(Result<ImportExecutionResult, String>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ResolutionOption {
value: ImportResolution,
label: String,
}
impl std::fmt::Display for ResolutionOption {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.label)
}
}
pub fn view<'a>(state: &'a ImportEditorState, locale: UiLocale) -> Element<'a, Message> {
let busy = state.is_analyzing || state.is_executing;
let name = inputs::labeled_input(
&t(locale, "import.name"),
&t(locale, "import.namePlaceholder"),
&state.definition.name,
|value| Message::ImportEditor(ImportEditorMsg::NameChanged(value)),
);
let header = inputs::card(
column![
row![
name,
button(text(t(locale, "modal.confirmDelete.delete")))
.on_press_maybe(
(!busy).then_some(Message::ImportEditor(ImportEditorMsg::DeleteRequested,))
)
.padding([8, 12])
.style(inputs::danger_button),
button(text(t(locale, "import.analyze")))
.on_press_maybe(
(!busy && state.definition.wxr_file_path.is_some())
.then_some(Message::ImportEditor(ImportEditorMsg::Analyze),)
)
.padding([8, 12])
.style(inputs::primary_button),
]
.spacing(12)
.align_y(Alignment::End),
text(t(locale, "import.description"))
.size(12)
.color(Color::from_rgb(0.60, 0.60, 0.65)),
]
.spacing(10),
);
let uploads = path_row(
locale,
"import.uploadsFolder",
state.definition.uploads_folder_path.as_deref(),
"import.noFolder",
ImportEditorMsg::PickUploads,
);
let wxr = path_row(
locale,
"import.wxrFile",
state.definition.wxr_file_path.as_deref(),
"import.noWxr",
ImportEditorMsg::PickWxr,
);
let files = inputs::card(column![uploads, wxr].spacing(12));
let mut content: Vec<Element<'a, Message>> = vec![header.into(), files.into()];
if busy || state.progress.is_some() {
content.push(progress_view(state, locale));
}
if let Some(error) = &state.error {
content.push(
inputs::card(
text(tw(locale, "import.failed", &[("error", error)]))
.size(13)
.color(Color::from_rgb(0.95, 0.38, 0.36)),
)
.into(),
);
}
if let Some(result) = &state.result {
content.push(result_view(result, locale));
}
if let Some(report) = &state.report {
content.push(summary_view(report, locale));
content.push(execute_toolbar(report, state, locale));
let conflicts = report
.posts
.iter()
.chain(&report.pages)
.chain(&report.media)
.filter(|item| item.status == ImportItemStatus::Conflict)
.collect::<Vec<_>>();
if !conflicts.is_empty() {
content.extend(section(
state,
locale,
ImportSection::Conflicts,
"import.conflicts",
conflict_rows(&conflicts, locale),
));
}
if !report.posts.is_empty() {
content.extend(section(
state,
locale,
ImportSection::Posts,
"import.posts",
candidate_rows(&report.posts, locale),
));
}
if !report.pages.is_empty() {
content.extend(section(
state,
locale,
ImportSection::Pages,
"import.pages",
candidate_rows(&report.pages, locale),
));
}
if !report.media.is_empty() {
content.extend(section(
state,
locale,
ImportSection::Media,
"import.media",
candidate_rows(&report.media, locale),
));
}
if !report.taxonomies.is_empty() {
content.extend(section(
state,
locale,
ImportSection::Taxonomy,
"import.taxonomy",
taxonomy_rows(state, report, locale),
));
}
if !report.macros.is_empty() {
content.extend(section(
state,
locale,
ImportSection::Macros,
"import.macros",
macro_rows(report, locale),
));
}
} else if !state.is_analyzing {
content.push(
inputs::card(
text(t(locale, "import.empty"))
.size(13)
.color(Color::from_rgb(0.60, 0.60, 0.65)),
)
.into(),
);
}
scrollable(
iced::widget::Column::with_children(content)
.spacing(10)
.padding(16)
.width(Length::Fill),
)
.height(Length::Fill)
.into()
}
fn path_row<'a>(
locale: UiLocale,
label_key: &str,
path: Option<&str>,
empty_key: &str,
action: ImportEditorMsg,
) -> Element<'a, Message> {
row![
column![
text(t(locale, label_key))
.size(12)
.color(inputs::LABEL_COLOR),
text(
path.map(str::to_string)
.unwrap_or_else(|| t(locale, empty_key))
)
.size(13)
.color(if path.is_some() {
Color::from_rgb(0.85, 0.85, 0.88)
} else {
Color::from_rgb(0.50, 0.50, 0.55)
}),
]
.spacing(5)
.width(Length::Fill),
button(text(t(locale, "common.open")))
.on_press(Message::ImportEditor(action))
.padding([7, 12])
.style(inputs::secondary_button),
]
.spacing(12)
.align_y(Alignment::Center)
.into()
}
fn progress_view<'a>(state: &ImportEditorState, locale: UiLocale) -> Element<'a, Message> {
let (phase, current, total, detail, eta) = state.progress.as_ref().map_or_else(
|| {
(
if state.is_analyzing {
t(locale, "import.phase.analysis")
} else {
t(locale, "import.phase.starting")
},
0,
1,
String::new(),
None,
)
},
|progress| {
(
phase_label(progress.phase, locale),
progress.current,
progress.total.max(1),
progress.detail.clone(),
progress.eta_ms,
)
},
);
let ratio = current as f32 / total as f32;
let eta = eta
.map(|milliseconds| {
tw(
locale,
"import.eta",
&[("seconds", &(milliseconds / 1_000).to_string())],
)
})
.unwrap_or_default();
inputs::card(
column![
row![
text(phase).size(13),
Space::with_width(Length::Fill),
text(format!("{current} / {total}"))
.size(12)
.color(inputs::LABEL_COLOR),
],
progress_bar(0.0..=1.0, ratio.clamp(0.0, 1.0)),
text(format!("{detail} {eta}"))
.size(12)
.color(Color::from_rgb(0.60, 0.60, 0.65)),
]
.spacing(8),
)
.into()
}
fn summary_view<'a>(report: &'a ImportReport, locale: UiLocale) -> Element<'a, Message> {
let source = std::path::Path::new(&report.source_file)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(&report.source_file);
let counts = row![
stat(locale, "import.posts", &report.post_counts),
stat(locale, "import.pages", &report.page_counts),
stat(locale, "import.media", &report.media_counts),
column![
text(t(locale, "import.taxonomy"))
.size(12)
.color(inputs::LABEL_COLOR),
text(report.taxonomies.len().to_string()).size(22),
]
.spacing(4)
.width(Length::Fill),
]
.spacing(14);
let dates = report
.date_distribution
.iter()
.map(|bucket| {
format!(
"{}: {} / {}",
bucket.year, bucket.post_count, bucket.media_count
)
})
.collect::<Vec<_>>()
.join(" · ");
inputs::card(
column![
text(&report.site.title).size(20),
text(format!(
"{} · {} · {}",
report.site.url.as_deref().unwrap_or(""),
report.site.language.as_deref().unwrap_or(""),
source
))
.size(12)
.color(Color::from_rgb(0.60, 0.60, 0.65)),
counts,
text(tw(locale, "import.dateDistribution", &[("dates", &dates)]))
.size(12)
.color(inputs::LABEL_COLOR),
]
.spacing(10),
)
.into()
}
fn stat<'a>(
locale: UiLocale,
label_key: &str,
counts: &bds_core::model::ImportCounts,
) -> Element<'a, Message> {
column![
text(t(locale, label_key))
.size(12)
.color(inputs::LABEL_COLOR),
text(
(counts.new_count
+ counts.update_count
+ counts.conflict_count
+ counts.duplicate_count
+ counts.missing_count)
.to_string()
)
.size(22),
text(format!(
"{} {} · {} {} · {} {} · {} {} · {} {}",
counts.new_count,
t(locale, "import.status.new"),
counts.update_count,
t(locale, "import.status.update"),
counts.conflict_count,
t(locale, "import.status.conflict"),
counts.duplicate_count,
t(locale, "import.status.duplicate"),
counts.missing_count,
t(locale, "import.status.missing"),
))
.size(10)
.color(Color::from_rgb(0.58, 0.58, 0.62)),
]
.spacing(4)
.width(Length::Fill)
.into()
}
fn execute_toolbar<'a>(
report: &ImportReport,
state: &ImportEditorState,
locale: UiLocale,
) -> Element<'a, Message> {
let count = report.importable_count();
inputs::toolbar(
vec![
text(tw(locale, "import.ready", &[("count", &count.to_string())]))
.size(13)
.into(),
],
vec![
button(text(t(locale, "import.autoMap")))
.on_press_maybe(
(!state.is_analyzing && !state.is_executing)
.then_some(Message::ImportEditor(ImportEditorMsg::AutoMapTaxonomy)),
)
.padding([8, 12])
.style(inputs::secondary_button)
.into(),
button(text(tw(
locale,
"import.execute",
&[("count", &count.to_string())],
)))
.on_press_maybe(
(count > 0 && !state.is_analyzing && !state.is_executing)
.then_some(Message::ImportEditor(ImportEditorMsg::Execute)),
)
.padding([8, 12])
.style(inputs::primary_button)
.into(),
],
)
}
fn result_view<'a>(result: &ImportExecutionResult, locale: UiLocale) -> Element<'a, Message> {
let imported = result.taxonomy.imported
+ result.posts.imported
+ result.media.imported
+ result.pages.imported;
let skipped = result.taxonomy.skipped
+ result.posts.skipped
+ result.media.skipped
+ result.pages.skipped;
inputs::card(
text(tw(
locale,
"import.complete",
&[
("imported", &imported.to_string()),
("skipped", &skipped.to_string()),
],
))
.size(13)
.color(Color::from_rgb(0.45, 0.80, 0.50)),
)
.into()
}
fn section<'a>(
state: &ImportEditorState,
locale: UiLocale,
section: ImportSection,
title_key: &str,
body: Element<'a, Message>,
) -> Vec<Element<'a, Message>> {
let expanded = state.expanded.contains(&section);
let header = inputs::card(
button(
row![
text(if expanded { "" } else { "" }).size(12),
text(t(locale, title_key)).size(13),
]
.spacing(8),
)
.on_press(Message::ImportEditor(ImportEditorMsg::ToggleSection(
section,
)))
.padding([6, 8])
.width(Length::Fill)
.style(inputs::disclosure_button),
)
.padding(6)
.into();
if expanded {
vec![header, inputs::card(body).into()]
} else {
vec![header]
}
}
fn conflict_rows<'a>(conflicts: &[&'a ImportCandidate], locale: UiLocale) -> Element<'a, Message> {
let options = vec![
ResolutionOption {
value: ImportResolution::Ignore,
label: t(locale, "import.resolution.ignore"),
},
ResolutionOption {
value: ImportResolution::Overwrite,
label: t(locale, "import.resolution.overwrite"),
},
ResolutionOption {
value: ImportResolution::Import,
label: t(locale, "import.resolution.import"),
},
];
let rows = conflicts
.iter()
.map(|item| {
let identity = item
.slug
.as_deref()
.or(item.filename.as_deref())
.unwrap_or("")
.to_string();
let identity_label = identity.clone();
let selected = options
.iter()
.find(|option| Some(option.value) == item.resolution);
let kind = item.kind;
row![
column![
text(identity_label).size(13),
text(&item.title).size(11).color(inputs::LABEL_COLOR)
]
.spacing(3)
.width(Length::Fill),
container(inputs::labeled_select(
&t(locale, "import.resolution"),
&options,
selected,
move |option| Message::ImportEditor(ImportEditorMsg::SetResolution {
kind,
identity: identity.clone(),
resolution: option.value,
}),
))
.width(Length::Fixed(240.0)),
]
.spacing(12)
.align_y(Alignment::Center)
.into()
})
.collect::<Vec<_>>();
iced::widget::Column::with_children(rows).spacing(10).into()
}
fn candidate_rows<'a>(items: &'a [ImportCandidate], locale: UiLocale) -> Element<'a, Message> {
let rows = items
.iter()
.map(|item| {
let identity = item
.slug
.as_deref()
.or(item.filename.as_deref())
.unwrap_or("");
let detail = match item.kind {
ImportItemKind::Media => item.relative_path.as_deref().unwrap_or("").to_string(),
_ => format!(
"{} · {} · {}",
item.source_status.as_deref().unwrap_or(""),
item.author.as_deref().unwrap_or(""),
item.categories.join(", ")
),
};
row![
column![
text(&item.title).size(13),
text(identity).size(11).color(inputs::LABEL_COLOR)
]
.spacing(3)
.width(Length::FillPortion(2)),
text(detail)
.size(11)
.color(Color::from_rgb(0.62, 0.62, 0.66))
.width(Length::FillPortion(3)),
text(status_label(item.status, locale)).size(11),
]
.spacing(12)
.align_y(Alignment::Center)
.into()
})
.collect::<Vec<_>>();
iced::widget::Column::with_children(rows).spacing(10).into()
}
fn taxonomy_rows<'a>(
state: &'a ImportEditorState,
report: &'a ImportReport,
locale: UiLocale,
) -> Element<'a, Message> {
let rows = report
.taxonomies
.iter()
.map(|item| {
let options = match item.kind {
TaxonomyKind::Category => &state.category_options,
TaxonomyKind::Tag => &state.tag_options,
};
let source = item.name.clone();
let kind = item.kind;
let status = if item.exists_in_project {
t(locale, "import.taxonomyExisting")
} else if item.mapped_to.is_some() {
t(locale, "import.taxonomyMapped")
} else {
t(locale, "import.taxonomyNew")
};
let select: Element<'a, Message> = if item.exists_in_project {
text("").size(12).into()
} else {
let selected = item
.mapped_to
.as_ref()
.and_then(|mapped| options.iter().find(|option| *option == mapped));
row![
container(inputs::labeled_select(
&t(locale, "import.mapTo"),
options,
selected,
move |target| Message::ImportEditor(ImportEditorMsg::SetTaxonomyMapping {
kind,
source: source.clone(),
target: Some(target),
}),
))
.width(Length::Fixed(260.0)),
button(text(t(locale, "common.clear")))
.on_press_maybe(item.mapped_to.is_some().then_some(Message::ImportEditor(
ImportEditorMsg::SetTaxonomyMapping {
kind,
source: item.name.clone(),
target: None,
},
)))
.padding([7, 10])
.style(inputs::secondary_button),
]
.spacing(8)
.align_y(Alignment::End)
.into()
};
row![
column![
text(&item.name).size(13),
text(match item.kind {
TaxonomyKind::Category => t(locale, "import.category"),
TaxonomyKind::Tag => t(locale, "import.tag"),
})
.size(11)
.color(inputs::LABEL_COLOR),
]
.spacing(3)
.width(Length::Fill),
text(status).size(11).width(Length::Fixed(90.0)),
select,
]
.spacing(12)
.align_y(Alignment::Center)
.into()
})
.collect::<Vec<_>>();
iced::widget::Column::with_children(rows).spacing(10).into()
}
fn macro_rows<'a>(report: &'a ImportReport, locale: UiLocale) -> Element<'a, Message> {
let rows = report
.macros
.iter()
.map(|usage| {
let parameters = usage
.parameters
.iter()
.map(|values| {
values
.iter()
.map(|(key, value)| format!("{key}={value}"))
.collect::<Vec<_>>()
.join(", ")
})
.collect::<Vec<_>>()
.join(" · ");
column![
row![
text(format!("[[{}]]", usage.name)).size(13),
Space::with_width(Length::Fill),
text(tw(
locale,
"import.macroUses",
&[("count", &usage.total_count.to_string())],
))
.size(11)
.color(inputs::LABEL_COLOR),
],
text(parameters).size(11).color(inputs::LABEL_COLOR),
text(usage.post_slugs.join(", "))
.size(11)
.color(Color::from_rgb(0.58, 0.58, 0.62)),
]
.spacing(4)
.into()
})
.collect::<Vec<_>>();
iced::widget::Column::with_children(rows).spacing(10).into()
}
fn status_label(status: ImportItemStatus, locale: UiLocale) -> String {
t(
locale,
match status {
ImportItemStatus::New => "import.status.new",
ImportItemStatus::Update => "import.status.update",
ImportItemStatus::Conflict => "import.status.conflict",
ImportItemStatus::ContentDuplicate => "import.status.duplicate",
ImportItemStatus::Missing => "import.status.missing",
},
)
}
fn phase_label(phase: ImportPhase, locale: UiLocale) -> String {
t(
locale,
match phase {
ImportPhase::Taxonomy => "import.phase.taxonomy",
ImportPhase::Posts => "import.phase.posts",
ImportPhase::Media => "import.phase.media",
ImportPhase::Pages => "import.phase.pages",
ImportPhase::Complete => "import.phase.complete",
},
)
}

View File

@@ -1,6 +1,7 @@
pub mod activity_bar;
pub mod dashboard;
pub mod git;
pub mod import_editor;
pub mod media_editor;
pub mod metadata_diff;
pub mod modal;

View File

@@ -123,6 +123,7 @@ pub enum ConfirmAction {
DeleteTemplate(String),
ForceDeleteTemplate(String),
DeleteTag(String),
DeleteImport(String),
MergeTags {
sources: Vec<String>,
target: String,

View File

@@ -5,7 +5,7 @@ use iced::widget::{Space, button, column, container, image, row, scrollable, tex
use iced::{Background, Border, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
use bds_core::model::{Media, Post, Script, Template};
use bds_core::model::{ImportDefinition, Media, Post, Script, Template};
use crate::app::Message;
use crate::components::inputs;
@@ -627,6 +627,7 @@ pub fn view(
media: &[Media],
scripts: &[Script],
templates: &[Template],
imports: &[ImportDefinition],
post_filter: &PostFilter,
media_filter: &MediaFilter,
media_thumbs: &std::collections::HashMap<String, Option<PathBuf>>,
@@ -653,6 +654,7 @@ pub fn view(
SidebarView::Media => Some(Message::CreateMedia),
SidebarView::Scripts => Some(Message::CreateScript),
SidebarView::Templates => Some(Message::CreateTemplate),
SidebarView::Import => Some(Message::CreateImport),
_ => None,
};
@@ -1079,6 +1081,57 @@ pub fn view(
iced::widget::Column::with_children(items).spacing(1).into()
}
}
SidebarView::Import => {
if imports.is_empty() {
text(t(locale, placeholder_key(sidebar_view)))
.size(12)
.shaping(Shaping::Advanced)
.color(muted)
.into()
} else {
let items = imports
.iter()
.map(|definition| {
let definition_name = definition.name.clone();
let style = if active_tab == Some(definition.id.as_str()) {
item_active_style
} else {
item_style
};
let analyzed = if definition.last_analysis_result.is_some() {
t(locale, "import.sidebar.analyzed")
} else {
t(locale, "import.sidebar.pending")
};
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)),
]
.spacing(1),
)
.width(Length::Fill),
)
.on_press(Message::OpenTab(Tab {
id: definition.id.clone(),
tab_type: TabType::Import,
title: definition.name.clone(),
is_transient: true,
is_dirty: false,
}))
.padding([5, 8])
.width(Length::Fill)
.style(style)
.into()
})
.collect::<Vec<Element<'static, Message>>>();
iced::widget::Column::with_children(items).spacing(1).into()
}
}
SidebarView::Settings => {
// Per sidebar_views.allium SettingsNav: 9 fixed-order sections
use crate::views::settings_view::SettingsSection;

View File

@@ -7,7 +7,7 @@ use iced::{Alignment, Background, Color, Element, Length, Padding, Theme};
use bds_core::engine::git::GitCommit;
use bds_core::i18n::UiLocale;
use bds_core::model::{Media, Post, Project, Script, Template};
use bds_core::model::{ImportDefinition, Media, Post, Project, Script, Template};
use crate::app::Message;
use crate::state::navigation::{OutputEntry, PanelTab, SidebarView, TaskSnapshot};
@@ -87,6 +87,7 @@ pub fn view<'a>(
sidebar_media: &'a [Media],
sidebar_scripts: &'a [Script],
sidebar_templates: &'a [Template],
sidebar_imports: &'a [ImportDefinition],
// Sidebar filters
post_filter: &'a PostFilter,
media_filter: &'a MediaFilter,
@@ -119,6 +120,7 @@ pub fn view<'a>(
media_editors: &'a HashMap<String, MediaEditorState>,
template_editors: &'a HashMap<String, TemplateEditorState>,
script_editors: &'a HashMap<String, ScriptEditorState>,
import_editors: &'a HashMap<String, crate::views::import_editor::ImportEditorState>,
tags_view_state: Option<&'a TagsViewState>,
settings_state: Option<&'a SettingsViewState>,
dashboard_state: Option<&'a DashboardState>,
@@ -147,6 +149,7 @@ pub fn view<'a>(
media_editors,
template_editors,
script_editors,
import_editors,
tags_view_state,
settings_state,
dashboard_state,
@@ -200,6 +203,7 @@ pub fn view<'a>(
sidebar_media,
sidebar_scripts,
sidebar_templates,
sidebar_imports,
post_filter,
media_filter,
sidebar_media_thumbs,
@@ -382,6 +386,7 @@ fn route_content_area<'a>(
media_editors: &'a HashMap<String, MediaEditorState>,
template_editors: &'a HashMap<String, TemplateEditorState>,
script_editors: &'a HashMap<String, ScriptEditorState>,
import_editors: &'a HashMap<String, crate::views::import_editor::ImportEditorState>,
tags_view_state: Option<&'a TagsViewState>,
settings_state: Option<&'a SettingsViewState>,
dashboard_state: Option<&'a DashboardState>,
@@ -397,6 +402,7 @@ fn route_content_area<'a>(
media_editors,
template_editors,
script_editors,
import_editors,
tags_view_state,
settings_state,
dashboard_state,
@@ -445,6 +451,13 @@ fn route_content_area<'a>(
loading_view(locale)
}
}
ContentRoute::Import(tab_id) => {
if let Some(state) = import_editors.get(tab_id) {
crate::views::import_editor::view(state, locale)
} else {
loading_view(locale)
}
}
ContentRoute::Tags => {
if let Some(state) = tags_view_state {
tags_view::view(state, locale)
@@ -490,6 +503,7 @@ enum ContentRoute<'a> {
Media(&'a str),
Templates(&'a str),
Scripts(&'a str),
Import(&'a str),
Tags,
Settings,
SiteValidation,
@@ -510,6 +524,7 @@ fn route_kind<'a>(
media_editors: &'a HashMap<String, MediaEditorState>,
template_editors: &'a HashMap<String, TemplateEditorState>,
script_editors: &'a HashMap<String, ScriptEditorState>,
import_editors: &'a HashMap<String, crate::views::import_editor::ImportEditorState>,
tags_view_state: Option<&'a TagsViewState>,
settings_state: Option<&'a SettingsViewState>,
dashboard_state: Option<&'a DashboardState>,
@@ -555,6 +570,13 @@ fn route_kind<'a>(
ContentRoute::Loading
}
}
TabType::Import => {
if import_editors.contains_key(tab_id) {
ContentRoute::Import(tab_id)
} else {
ContentRoute::Loading
}
}
TabType::Tags => {
if tags_view_state.is_some() {
ContentRoute::Tags
@@ -574,7 +596,6 @@ fn route_kind<'a>(
TabType::GitDiff => ContentRoute::GitDiff(tab_id),
TabType::Style
| TabType::Chat
| TabType::Import
| TabType::MenuEditor
| TabType::Documentation
| TabType::ApiDocumentation
@@ -633,11 +654,11 @@ mod tests {
let empty_media = HashMap::new();
let empty_templates = HashMap::new();
let empty_scripts = HashMap::new();
let empty_imports = HashMap::new();
let site_validation_state = SiteValidationState::default();
let unsupported = [
TabType::Style,
TabType::Chat,
TabType::Import,
TabType::MenuEditor,
TabType::Documentation,
TabType::ApiDocumentation,
@@ -653,6 +674,7 @@ mod tests {
&empty_media,
&empty_templates,
&empty_scripts,
&empty_imports,
None,
None,
None,
@@ -671,6 +693,7 @@ mod tests {
let empty_media = HashMap::new();
let empty_templates = HashMap::new();
let empty_scripts = HashMap::new();
let empty_imports = HashMap::new();
let site_validation_state = SiteValidationState::default();
let tabs = vec![tab("git-diff:file.txt", TabType::GitDiff, "file.txt")];
let route = route_kind(
@@ -680,6 +703,7 @@ mod tests {
&empty_media,
&empty_templates,
&empty_scripts,
&empty_imports,
None,
None,
None,
@@ -694,6 +718,7 @@ mod tests {
let empty_media = HashMap::new();
let empty_templates = HashMap::new();
let empty_scripts = HashMap::new();
let empty_imports = HashMap::new();
let site_validation_state = SiteValidationState::default();
for (tab_type, expected) in [
@@ -708,6 +733,7 @@ mod tests {
&empty_media,
&empty_templates,
&empty_scripts,
&empty_imports,
None,
None,
None,
@@ -733,6 +759,7 @@ mod tests {
let empty_media = HashMap::new();
let empty_templates = HashMap::new();
let empty_scripts = HashMap::new();
let empty_imports = HashMap::new();
let site_validation_state = SiteValidationState::default();
let route = route_kind(
&[],
@@ -741,6 +768,7 @@ mod tests {
&empty_media,
&empty_templates,
&empty_scripts,
&empty_imports,
None,
None,
Some(&dashboard),
@@ -758,6 +786,7 @@ mod tests {
let empty_media = HashMap::new();
let empty_templates = HashMap::new();
let empty_scripts = HashMap::new();
let empty_imports = HashMap::new();
let site_validation_state = SiteValidationState::default();
let tabs = vec![tab(
"site_validation",
@@ -772,6 +801,7 @@ mod tests {
&empty_media,
&empty_templates,
&empty_scripts,
&empty_imports,
None,
None,
None,