Implement complete WordPress WXR import workflow.
This commit is contained in:
@@ -17,6 +17,7 @@ open = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
wry = "0.55"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
|
||||
@@ -13,8 +13,8 @@ use bds_core::engine::ai::{self, AiEndpointConfig, AiEndpointKind};
|
||||
use bds_core::engine::task::{TaskId, TaskManager, TaskStatus};
|
||||
use bds_core::i18n::{UiLocale, detect_os_locale};
|
||||
use bds_core::model::{
|
||||
Media, Post, PostStatus, PostTranslation, Project, PublishingPreferences, Script, SshMode,
|
||||
Template,
|
||||
ImportDefinition, ImportReport, Media, Post, PostStatus, PostTranslation, Project,
|
||||
PublishingPreferences, Script, SshMode, Template,
|
||||
};
|
||||
|
||||
use crate::components::webview::{self, WebViewConfig, WebViewController};
|
||||
@@ -32,6 +32,9 @@ use crate::views::{
|
||||
DashboardTimelineMonth,
|
||||
},
|
||||
git::{GitDiffLoad, GitDiffState, GitNetworkCompletion, GitSnapshot, GitUiState},
|
||||
import_editor::{
|
||||
ImportAnalysisEvent, ImportEditorMsg, ImportEditorState, ImportExecutionEvent,
|
||||
},
|
||||
media_editor::{LinkedPostItem, MediaEditorMsg, MediaEditorState},
|
||||
metadata_diff::MetadataDiffState,
|
||||
modal,
|
||||
@@ -118,6 +121,26 @@ pub enum Message {
|
||||
media_id: String,
|
||||
result: Result<Option<Media>, String>,
|
||||
},
|
||||
ImportUploadsPicked {
|
||||
definition_id: String,
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
ImportWxrPicked {
|
||||
definition_id: String,
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
ImportAnalysisEvent {
|
||||
definition_id: String,
|
||||
event: ImportAnalysisEvent,
|
||||
},
|
||||
ImportExecutionEvent {
|
||||
definition_id: String,
|
||||
event: ImportExecutionEvent,
|
||||
},
|
||||
ImportAutoMapFinished {
|
||||
definition_id: String,
|
||||
result: Result<(ImportDefinition, ImportReport, usize), String>,
|
||||
},
|
||||
|
||||
// Tasks
|
||||
TaskTick,
|
||||
@@ -269,6 +292,7 @@ pub enum Message {
|
||||
ScriptEditor(ScriptEditorMsg),
|
||||
Tags(TagsMsg),
|
||||
Settings(SettingsMsg),
|
||||
ImportEditor(ImportEditorMsg),
|
||||
|
||||
// Editor data loading
|
||||
PostLoaded(Result<Post, String>),
|
||||
@@ -295,6 +319,7 @@ pub enum Message {
|
||||
CreateMedia,
|
||||
CreateScript,
|
||||
CreateTemplate,
|
||||
CreateImport,
|
||||
|
||||
Noop,
|
||||
InitMenuBar,
|
||||
@@ -798,6 +823,7 @@ pub struct BdsApp {
|
||||
sidebar_media: Vec<Media>,
|
||||
sidebar_scripts: Vec<Script>,
|
||||
sidebar_templates: Vec<Template>,
|
||||
sidebar_imports: Vec<ImportDefinition>,
|
||||
sidebar_media_thumbs: HashMap<String, Option<std::path::PathBuf>>,
|
||||
sidebar_posts_has_more: bool,
|
||||
sidebar_media_has_more: bool,
|
||||
@@ -866,6 +892,7 @@ pub struct BdsApp {
|
||||
media_editors: HashMap<String, MediaEditorState>,
|
||||
template_editors: HashMap<String, TemplateEditorState>,
|
||||
script_editors: HashMap<String, ScriptEditorState>,
|
||||
import_editors: HashMap<String, ImportEditorState>,
|
||||
tags_view_state: Option<TagsViewState>,
|
||||
settings_state: Option<SettingsViewState>,
|
||||
dashboard_state: Option<DashboardState>,
|
||||
@@ -967,6 +994,7 @@ impl BdsApp {
|
||||
sidebar_media: Vec::new(),
|
||||
sidebar_scripts: Vec::new(),
|
||||
sidebar_templates: Vec::new(),
|
||||
sidebar_imports: Vec::new(),
|
||||
sidebar_media_thumbs: HashMap::new(),
|
||||
sidebar_posts_has_more: false,
|
||||
sidebar_media_has_more: false,
|
||||
@@ -1009,6 +1037,7 @@ impl BdsApp {
|
||||
media_editors: HashMap::new(),
|
||||
template_editors: HashMap::new(),
|
||||
script_editors: HashMap::new(),
|
||||
import_editors: HashMap::new(),
|
||||
tags_view_state: None,
|
||||
settings_state: None,
|
||||
dashboard_state: None,
|
||||
@@ -1038,6 +1067,7 @@ impl BdsApp {
|
||||
sidebar_media: Vec::new(),
|
||||
sidebar_scripts: Vec::new(),
|
||||
sidebar_templates: Vec::new(),
|
||||
sidebar_imports: Vec::new(),
|
||||
sidebar_media_thumbs: HashMap::new(),
|
||||
sidebar_posts_has_more: false,
|
||||
sidebar_media_has_more: false,
|
||||
@@ -1079,6 +1109,7 @@ impl BdsApp {
|
||||
media_editors: HashMap::new(),
|
||||
template_editors: HashMap::new(),
|
||||
script_editors: HashMap::new(),
|
||||
import_editors: HashMap::new(),
|
||||
tags_view_state: None,
|
||||
settings_state: None,
|
||||
dashboard_state: None,
|
||||
@@ -1136,6 +1167,7 @@ impl BdsApp {
|
||||
),
|
||||
Message::CreateScript => self.create_sidebar_script(),
|
||||
Message::CreateTemplate => self.create_sidebar_template(),
|
||||
Message::CreateImport => self.create_sidebar_import(),
|
||||
Message::OpenSettingsSection(section) => {
|
||||
self.sidebar_view = SidebarView::Settings;
|
||||
self.sidebar_visible = true;
|
||||
@@ -1301,7 +1333,7 @@ impl BdsApp {
|
||||
}
|
||||
}
|
||||
self.sync_menu_state();
|
||||
self.refresh_git_if_visible()
|
||||
Task::batch([self.refresh_counts(), self.refresh_git_if_visible()])
|
||||
}
|
||||
Message::ProjectSwitched(result) => {
|
||||
match result {
|
||||
@@ -1692,6 +1724,108 @@ impl BdsApp {
|
||||
}
|
||||
self.refresh_sidebar_media()
|
||||
}
|
||||
Message::ImportUploadsPicked {
|
||||
definition_id,
|
||||
path,
|
||||
} => {
|
||||
if let Some(path) = path {
|
||||
self.update_import_paths(&definition_id, None, Some(path.as_path()));
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::ImportWxrPicked {
|
||||
definition_id,
|
||||
path,
|
||||
} => {
|
||||
if let Some(path) = path {
|
||||
self.update_import_paths(&definition_id, Some(path.as_path()), None);
|
||||
return self.start_import_analysis(&definition_id);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::ImportAnalysisEvent {
|
||||
definition_id,
|
||||
event,
|
||||
} => {
|
||||
if let Some(state) = self.import_editors.get_mut(&definition_id) {
|
||||
match event {
|
||||
ImportAnalysisEvent::Progress(progress) => state.progress = Some(progress),
|
||||
ImportAnalysisEvent::Finished(result) => {
|
||||
state.is_analyzing = false;
|
||||
match *result {
|
||||
Ok((definition, report)) => {
|
||||
state.definition = definition.clone();
|
||||
state.report = Some(report);
|
||||
state.error = None;
|
||||
if let Some(sidebar) = self
|
||||
.sidebar_imports
|
||||
.iter_mut()
|
||||
.find(|item| item.id == definition_id)
|
||||
{
|
||||
*sidebar = definition;
|
||||
}
|
||||
}
|
||||
Err(error) => state.error = Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::ImportExecutionEvent {
|
||||
definition_id,
|
||||
event,
|
||||
} => {
|
||||
if let Some(state) = self.import_editors.get_mut(&definition_id) {
|
||||
match event {
|
||||
ImportExecutionEvent::Progress(progress) => state.progress = Some(progress),
|
||||
ImportExecutionEvent::Finished(result) => {
|
||||
state.is_executing = false;
|
||||
match result {
|
||||
Ok(result) => {
|
||||
state.result = Some(result);
|
||||
state.error = None;
|
||||
self.notify(
|
||||
ToastLevel::Success,
|
||||
&t(self.ui_locale, "import.toast.complete"),
|
||||
);
|
||||
return self.refresh_counts();
|
||||
}
|
||||
Err(error) => state.error = Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::ImportAutoMapFinished {
|
||||
definition_id,
|
||||
result,
|
||||
} => {
|
||||
if let Some(state) = self.import_editors.get_mut(&definition_id) {
|
||||
state.is_analyzing = false;
|
||||
match result {
|
||||
Ok((definition, report, count)) => {
|
||||
state.definition = definition;
|
||||
state.report = Some(report);
|
||||
state.error = None;
|
||||
self.notify(
|
||||
ToastLevel::Success,
|
||||
&tw(
|
||||
self.ui_locale,
|
||||
"import.toast.mapped",
|
||||
&[("count", &count.to_string())],
|
||||
),
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
state.error = Some(error.clone());
|
||||
self.notify(ToastLevel::Error, &error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
message @ (Message::MainWindowLoaded(_) | Message::EmbeddedPreviewReady(_)) => {
|
||||
self.handle_preview_message(message)
|
||||
}
|
||||
@@ -2028,6 +2162,7 @@ impl BdsApp {
|
||||
self.delete_template_editor(&id, true)
|
||||
}
|
||||
modal::ConfirmAction::DeleteTag(id) => self.delete_tag(&id),
|
||||
modal::ConfirmAction::DeleteImport(id) => self.delete_import_definition(&id),
|
||||
modal::ConfirmAction::MergeTags { sources, target } => {
|
||||
self.merge_tags(&sources, &target)
|
||||
}
|
||||
@@ -2109,6 +2244,7 @@ impl BdsApp {
|
||||
Message::ScriptEditor(msg) => self.handle_script_editor_msg(msg),
|
||||
Message::Tags(msg) => self.handle_tags_msg(msg),
|
||||
Message::Settings(msg) => self.handle_settings_msg(msg),
|
||||
Message::ImportEditor(msg) => self.handle_import_editor_msg(msg),
|
||||
|
||||
// ── Editor data loading ──
|
||||
Message::PostLoaded(result) => {
|
||||
@@ -2238,6 +2374,7 @@ impl BdsApp {
|
||||
&self.sidebar_media,
|
||||
&self.sidebar_scripts,
|
||||
&self.sidebar_templates,
|
||||
&self.sidebar_imports,
|
||||
active_post_filter,
|
||||
&self.media_filter,
|
||||
&self.sidebar_media_thumbs,
|
||||
@@ -2261,6 +2398,7 @@ impl BdsApp {
|
||||
&self.media_editors,
|
||||
&self.template_editors,
|
||||
&self.script_editors,
|
||||
&self.import_editors,
|
||||
self.tags_view_state.as_ref(),
|
||||
self.settings_state.as_ref(),
|
||||
self.dashboard_state.as_ref(),
|
||||
@@ -2643,6 +2781,444 @@ impl BdsApp {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_sidebar_import(&mut self) -> Task<Message> {
|
||||
let (Some(db), Some(project)) = (&self.db, &self.active_project) else {
|
||||
return Task::none();
|
||||
};
|
||||
match engine::wordpress_import::create_definition(
|
||||
db.conn(),
|
||||
&project.id,
|
||||
&t(self.ui_locale, "import.untitled"),
|
||||
) {
|
||||
Ok(definition) => {
|
||||
self.sidebar_imports.push(definition.clone());
|
||||
let tab = Tab {
|
||||
id: definition.id.clone(),
|
||||
tab_type: TabType::Import,
|
||||
title: definition.name.clone(),
|
||||
is_transient: true,
|
||||
is_dirty: false,
|
||||
};
|
||||
let index = tabs::open_tab(&mut self.tabs, tab);
|
||||
if let Some(tab) = self.tabs.get(index).cloned() {
|
||||
self.active_tab = Some(tab.id.clone());
|
||||
self.load_editor_for_tab(&tab);
|
||||
}
|
||||
self.sync_menu_state();
|
||||
}
|
||||
Err(error) => self.notify(ToastLevel::Error, &error.to_string()),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_import_editor_msg(&mut self, message: ImportEditorMsg) -> Task<Message> {
|
||||
let Some(definition_id) = self.active_tab.clone().filter(|id| {
|
||||
self.tabs
|
||||
.iter()
|
||||
.any(|tab| tab.id == *id && tab.tab_type == TabType::Import)
|
||||
}) else {
|
||||
return Task::none();
|
||||
};
|
||||
match message {
|
||||
ImportEditorMsg::NameChanged(name) => {
|
||||
let Some(db) = &self.db else {
|
||||
return Task::none();
|
||||
};
|
||||
match engine::wordpress_import::update_definition(
|
||||
db.conn(),
|
||||
&definition_id,
|
||||
Some(&name),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
) {
|
||||
Ok(definition) => {
|
||||
if let Some(state) = self.import_editors.get_mut(&definition_id) {
|
||||
state.definition = definition.clone();
|
||||
}
|
||||
if let Some(item) = self
|
||||
.sidebar_imports
|
||||
.iter_mut()
|
||||
.find(|item| item.id == definition_id)
|
||||
{
|
||||
*item = definition;
|
||||
}
|
||||
if let Some(tab) = self.tabs.iter_mut().find(|tab| tab.id == definition_id)
|
||||
{
|
||||
tab.title = name;
|
||||
}
|
||||
}
|
||||
Err(error) => self.notify(ToastLevel::Error, &error.to_string()),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
ImportEditorMsg::PickUploads => crate::platform::dialog::pick_import_uploads_folder(
|
||||
definition_id,
|
||||
t(self.ui_locale, "import.uploadsFolder"),
|
||||
),
|
||||
ImportEditorMsg::PickWxr => crate::platform::dialog::pick_import_wxr_file(
|
||||
definition_id,
|
||||
t(self.ui_locale, "import.wxrFile"),
|
||||
t(self.ui_locale, "import.wxrFilter"),
|
||||
),
|
||||
ImportEditorMsg::Analyze => self.start_import_analysis(&definition_id),
|
||||
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()
|
||||
}
|
||||
ImportEditorMsg::SetResolution {
|
||||
kind,
|
||||
identity,
|
||||
resolution,
|
||||
} => {
|
||||
let mut report = self
|
||||
.import_editors
|
||||
.get(&definition_id)
|
||||
.and_then(|state| state.report.clone());
|
||||
if let Some(report) = report.as_mut() {
|
||||
match engine::wordpress_import::set_conflict_resolution(
|
||||
report, kind, &identity, resolution,
|
||||
)
|
||||
.and_then(|_| self.persist_import_report(&definition_id, report))
|
||||
{
|
||||
Ok(definition) => {
|
||||
if let Some(state) = self.import_editors.get_mut(&definition_id) {
|
||||
state.definition = definition;
|
||||
state.report = Some(report.clone());
|
||||
}
|
||||
}
|
||||
Err(error) => self.notify(ToastLevel::Error, &error.to_string()),
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
ImportEditorMsg::SetTaxonomyMapping {
|
||||
kind,
|
||||
source,
|
||||
target,
|
||||
} => {
|
||||
let mut report = self
|
||||
.import_editors
|
||||
.get(&definition_id)
|
||||
.and_then(|state| state.report.clone());
|
||||
if let Some(report) = report.as_mut() {
|
||||
match engine::wordpress_import::set_taxonomy_mapping(
|
||||
report,
|
||||
kind,
|
||||
&source,
|
||||
target.as_deref(),
|
||||
)
|
||||
.and_then(|_| self.persist_import_report(&definition_id, report))
|
||||
{
|
||||
Ok(definition) => {
|
||||
if let Some(state) = self.import_editors.get_mut(&definition_id) {
|
||||
state.definition = definition;
|
||||
state.report = Some(report.clone());
|
||||
}
|
||||
}
|
||||
Err(error) => self.notify(ToastLevel::Error, &error.to_string()),
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
ImportEditorMsg::ToggleSection(section) => {
|
||||
if let Some(state) = self.import_editors.get_mut(&definition_id)
|
||||
&& !state.expanded.remove(§ion)
|
||||
{
|
||||
state.expanded.insert(section);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_import_paths(
|
||||
&mut self,
|
||||
definition_id: &str,
|
||||
wxr_path: Option<&Path>,
|
||||
uploads_path: Option<&Path>,
|
||||
) {
|
||||
let Some(db) = &self.db else { return };
|
||||
match engine::wordpress_import::update_definition(
|
||||
db.conn(),
|
||||
definition_id,
|
||||
None,
|
||||
wxr_path.map(Some),
|
||||
uploads_path.map(Some),
|
||||
wxr_path.map(|_| None),
|
||||
) {
|
||||
Ok(definition) => {
|
||||
if let Some(state) = self.import_editors.get_mut(definition_id) {
|
||||
state.definition = definition.clone();
|
||||
if wxr_path.is_some() {
|
||||
state.report = None;
|
||||
state.result = None;
|
||||
}
|
||||
}
|
||||
if let Some(item) = self
|
||||
.sidebar_imports
|
||||
.iter_mut()
|
||||
.find(|item| item.id == definition_id)
|
||||
{
|
||||
*item = definition;
|
||||
}
|
||||
}
|
||||
Err(error) => self.notify(ToastLevel::Error, &error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_import_definition(&mut self, definition_id: &str) -> Task<Message> {
|
||||
self.active_modal = None;
|
||||
let Some(db) = &self.db else {
|
||||
return Task::none();
|
||||
};
|
||||
match engine::wordpress_import::delete_definition(db.conn(), definition_id) {
|
||||
Ok(()) => {
|
||||
self.import_editors.remove(definition_id);
|
||||
self.sidebar_imports
|
||||
.retain(|definition| definition.id != definition_id);
|
||||
self.close_entity_tab(definition_id);
|
||||
self.notify(
|
||||
ToastLevel::Success,
|
||||
&t(self.ui_locale, "import.toast.deleted"),
|
||||
);
|
||||
}
|
||||
Err(error) => self.notify(ToastLevel::Error, &error.to_string()),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn persist_import_report(
|
||||
&self,
|
||||
definition_id: &str,
|
||||
report: &ImportReport,
|
||||
) -> engine::EngineResult<ImportDefinition> {
|
||||
let db = self
|
||||
.db
|
||||
.as_ref()
|
||||
.ok_or_else(|| engine::EngineError::Validation("database unavailable".to_string()))?;
|
||||
engine::wordpress_import::update_definition(
|
||||
db.conn(),
|
||||
definition_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(Some(report)),
|
||||
)
|
||||
}
|
||||
|
||||
fn start_import_analysis(&mut self, definition_id: &str) -> Task<Message> {
|
||||
let Some(definition) = self
|
||||
.import_editors
|
||||
.get(definition_id)
|
||||
.map(|state| state.definition.clone())
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some(wxr_path) = definition.wxr_file_path.clone() else {
|
||||
if let Some(state) = self.import_editors.get_mut(definition_id) {
|
||||
state.error = Some(t(self.ui_locale, "import.error.wxrRequired"));
|
||||
}
|
||||
return Task::none();
|
||||
};
|
||||
let Some(project) = self
|
||||
.projects
|
||||
.iter()
|
||||
.find(|project| project.id == definition.project_id)
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some(data_dir) = project.data_path.as_deref().map(PathBuf::from) else {
|
||||
return Task::none();
|
||||
};
|
||||
let state = self.import_editors.get_mut(definition_id).unwrap();
|
||||
state.is_analyzing = true;
|
||||
state.progress = None;
|
||||
state.error = None;
|
||||
state.result = None;
|
||||
let uploads = definition.uploads_folder_path.clone();
|
||||
let db_path = self.db_path.clone();
|
||||
let data_dir = data_dir.clone();
|
||||
let project_id = project.id.clone();
|
||||
let definition_id_owned = definition_id.to_string();
|
||||
let (sender, receiver) = futures::channel::mpsc::unbounded();
|
||||
let worker_definition_id = definition_id_owned.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let progress_sender = sender.clone();
|
||||
let result = (|| {
|
||||
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
|
||||
let report = engine::wordpress_import::analyze_wxr(
|
||||
db.conn(),
|
||||
&data_dir,
|
||||
&project_id,
|
||||
Path::new(&wxr_path),
|
||||
uploads.as_deref().map(Path::new),
|
||||
Some(&mut |progress| {
|
||||
let _ =
|
||||
progress_sender.unbounded_send(ImportAnalysisEvent::Progress(progress));
|
||||
}),
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let definition = engine::wordpress_import::update_definition(
|
||||
db.conn(),
|
||||
&worker_definition_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(Some(&report)),
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
Ok((definition, report))
|
||||
})();
|
||||
let _ = sender.unbounded_send(ImportAnalysisEvent::Finished(Box::new(result)));
|
||||
});
|
||||
Task::run(receiver, move |event| Message::ImportAnalysisEvent {
|
||||
definition_id: definition_id_owned.clone(),
|
||||
event,
|
||||
})
|
||||
}
|
||||
|
||||
fn start_import_execution(&mut self, definition_id: &str) -> Task<Message> {
|
||||
let Some(definition) = self
|
||||
.import_editors
|
||||
.get(definition_id)
|
||||
.map(|state| state.definition.clone())
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some(report) = self
|
||||
.import_editors
|
||||
.get(definition_id)
|
||||
.and_then(|state| state.report.clone())
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some(project) = self
|
||||
.projects
|
||||
.iter()
|
||||
.find(|project| project.id == definition.project_id)
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some(data_dir) = project.data_path.as_deref().map(PathBuf::from) else {
|
||||
return Task::none();
|
||||
};
|
||||
let state = self.import_editors.get_mut(definition_id).unwrap();
|
||||
state.is_executing = true;
|
||||
state.progress = None;
|
||||
state.error = None;
|
||||
state.result = None;
|
||||
let db_path = self.db_path.clone();
|
||||
let data_dir = data_dir.clone();
|
||||
let project_id = project.id.clone();
|
||||
let default_author = engine::meta::read_project_json(&data_dir)
|
||||
.ok()
|
||||
.and_then(|metadata| metadata.default_author);
|
||||
let definition_id_owned = definition_id.to_string();
|
||||
let (sender, receiver) = futures::channel::mpsc::unbounded();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let progress_sender = sender.clone();
|
||||
let result = (|| {
|
||||
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
|
||||
engine::wordpress_import::execute_import(
|
||||
db.conn(),
|
||||
&data_dir,
|
||||
&project_id,
|
||||
&report,
|
||||
default_author.as_deref(),
|
||||
Some(&mut |progress| {
|
||||
let _ = progress_sender
|
||||
.unbounded_send(ImportExecutionEvent::Progress(progress));
|
||||
}),
|
||||
)
|
||||
.map_err(|error| error.to_string())
|
||||
})();
|
||||
let _ = sender.unbounded_send(ImportExecutionEvent::Finished(result));
|
||||
});
|
||||
Task::run(receiver, move |event| Message::ImportExecutionEvent {
|
||||
definition_id: definition_id_owned.clone(),
|
||||
event,
|
||||
})
|
||||
}
|
||||
|
||||
fn start_import_auto_mapping(&mut self, definition_id: &str) -> Task<Message> {
|
||||
let Some(definition) = self
|
||||
.import_editors
|
||||
.get(definition_id)
|
||||
.map(|state| state.definition.clone())
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some(mut report) = self
|
||||
.import_editors
|
||||
.get(definition_id)
|
||||
.and_then(|state| state.report.clone())
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some(project) = self
|
||||
.projects
|
||||
.iter()
|
||||
.find(|project| project.id == definition.project_id)
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
let Some(data_dir) = project.data_path.as_deref().map(PathBuf::from) else {
|
||||
return Task::none();
|
||||
};
|
||||
let state = self.import_editors.get_mut(definition_id).unwrap();
|
||||
state.is_analyzing = true;
|
||||
state.error = None;
|
||||
let db_path = self.db_path.clone();
|
||||
let data_dir = data_dir.clone();
|
||||
let project_id = project.id.clone();
|
||||
let offline_mode = self.offline_mode;
|
||||
let definition_id_owned = definition_id.to_string();
|
||||
let result_definition_id = definition_id_owned.clone();
|
||||
Task::perform(
|
||||
async move {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
|
||||
let count = engine::wordpress_import::auto_map_taxonomy(
|
||||
db.conn(),
|
||||
&data_dir,
|
||||
&project_id,
|
||||
offline_mode,
|
||||
&mut report,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let definition = engine::wordpress_import::update_definition(
|
||||
db.conn(),
|
||||
&definition_id_owned,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(Some(&report)),
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
Ok((definition, report, count))
|
||||
})
|
||||
.await
|
||||
.map_err(|error| error.to_string())?
|
||||
},
|
||||
move |result| Message::ImportAutoMapFinished {
|
||||
definition_id: result_definition_id.clone(),
|
||||
result,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn flush_active_post_editor(&mut self) {
|
||||
let Some(active_id) = self.active_tab.clone() else {
|
||||
return;
|
||||
@@ -2812,6 +3388,9 @@ impl BdsApp {
|
||||
self.sidebar_templates =
|
||||
bds_core::db::queries::template::list_templates_by_project(db.conn(), &project.id)
|
||||
.unwrap_or_default();
|
||||
self.sidebar_imports =
|
||||
engine::wordpress_import::list_definitions(db.conn(), &project.id)
|
||||
.unwrap_or_default();
|
||||
|
||||
// Read pico theme from project metadata for status bar badge
|
||||
if let Some(ref data_dir) = self.data_dir
|
||||
@@ -5416,6 +5995,38 @@ impl BdsApp {
|
||||
}
|
||||
}
|
||||
}
|
||||
TabType::Import => {
|
||||
if !self.import_editors.contains_key(&tab.id) {
|
||||
match engine::wordpress_import::get_definition(db.conn(), &tab.id) {
|
||||
Ok(definition) => {
|
||||
let project_data_dir = self
|
||||
.projects
|
||||
.iter()
|
||||
.find(|project| project.id == definition.project_id)
|
||||
.and_then(|project| project.data_path.as_deref())
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| self.data_dir.clone());
|
||||
let categories = project_data_dir
|
||||
.as_deref()
|
||||
.and_then(|path| engine::meta::read_categories_json(path).ok())
|
||||
.unwrap_or_default();
|
||||
let tags = bds_core::db::queries::tag::list_tags_by_project(
|
||||
db.conn(),
|
||||
&definition.project_id,
|
||||
)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|tag| tag.name)
|
||||
.collect();
|
||||
self.import_editors.insert(
|
||||
definition.id.clone(),
|
||||
ImportEditorState::new(definition, categories, tags),
|
||||
);
|
||||
}
|
||||
Err(error) => self.notify_operation_failed("activity.import", error),
|
||||
}
|
||||
}
|
||||
}
|
||||
TabType::Tags => {
|
||||
if self.tags_view_state.is_none() {
|
||||
let project_id = self
|
||||
@@ -6302,6 +6913,7 @@ mod tests {
|
||||
use crate::i18n::t;
|
||||
use crate::state::ToastLevel;
|
||||
use crate::state::sidebar_filter::{MediaFilter, PostFilter};
|
||||
use crate::state::tabs::{Tab, TabType};
|
||||
use crate::views::media_editor::{MediaEditorMsg, MediaEditorState};
|
||||
use crate::views::modal;
|
||||
use crate::views::post_editor::{PostEditorMsg, PostEditorState};
|
||||
@@ -6313,7 +6925,7 @@ mod tests {
|
||||
use bds_core::db::queries::project::insert_project;
|
||||
use bds_core::engine::generation::GenerationReport;
|
||||
use bds_core::engine::task::{TaskStatus, TaskStatus::*};
|
||||
use bds_core::engine::{ai, media, post, script, template};
|
||||
use bds_core::engine::{ai, media, post, script, template, wordpress_import};
|
||||
use bds_core::i18n::UiLocale;
|
||||
use bds_core::model::{Project, ScriptKind, TemplateKind};
|
||||
use chrono::{Datelike, TimeZone};
|
||||
@@ -6391,6 +7003,42 @@ mod tests {
|
||||
format!("http://{}", addr)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_definition_loads_saved_analysis_into_real_editor_state() {
|
||||
let (db, project, tempdir) = setup();
|
||||
let definition =
|
||||
wordpress_import::create_definition(db.conn(), &project.id, "Legacy").unwrap();
|
||||
let mut report = wordpress_import::empty_report();
|
||||
report.site.title = "Legacy Blog".to_string();
|
||||
report.source_file = tempdir.path().join("legacy.xml").display().to_string();
|
||||
wordpress_import::update_definition(
|
||||
db.conn(),
|
||||
&definition.id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(Some(&report)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut app = BdsApp::new_for_tests(db, project, tempdir.path().to_path_buf());
|
||||
let _ = app.refresh_counts();
|
||||
assert_eq!(app.sidebar_imports.len(), 1);
|
||||
let tab = Tab {
|
||||
id: definition.id.clone(),
|
||||
tab_type: TabType::Import,
|
||||
title: "Legacy".to_string(),
|
||||
is_transient: true,
|
||||
is_dirty: false,
|
||||
};
|
||||
app.load_editor_for_tab(&tab);
|
||||
|
||||
let editor = app.import_editors.get(&definition.id).unwrap();
|
||||
assert_eq!(editor.definition.name, "Legacy");
|
||||
assert_eq!(editor.report.as_ref().unwrap().site.title, "Legacy Blog");
|
||||
assert!(editor.category_options.iter().any(|name| name == "article"));
|
||||
}
|
||||
|
||||
fn make_app(db: Database, project: Project, tmp: &TempDir) -> BdsApp {
|
||||
BdsApp::new_for_tests(db, project, tmp.path().to_path_buf())
|
||||
}
|
||||
|
||||
@@ -95,3 +95,44 @@ pub fn pick_media_replacement(
|
||||
|(media_id, path)| Message::MediaReplacementPicked { media_id, path },
|
||||
)
|
||||
}
|
||||
|
||||
/// Pick the WordPress uploads directory for an import definition.
|
||||
pub fn pick_import_uploads_folder(definition_id: String, title: String) -> Task<Message> {
|
||||
Task::perform(
|
||||
async move {
|
||||
let path = rfd::AsyncFileDialog::new()
|
||||
.set_title(&title)
|
||||
.pick_folder()
|
||||
.await
|
||||
.map(|handle| handle.path().to_path_buf());
|
||||
(definition_id, path)
|
||||
},
|
||||
|(definition_id, path)| Message::ImportUploadsPicked {
|
||||
definition_id,
|
||||
path,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Pick a WordPress WXR export for an import definition.
|
||||
pub fn pick_import_wxr_file(
|
||||
definition_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, &["xml", "wxr"])
|
||||
.pick_file()
|
||||
.await
|
||||
.map(|handle| handle.path().to_path_buf());
|
||||
(definition_id, path)
|
||||
},
|
||||
|(definition_id, path)| Message::ImportWxrPicked {
|
||||
definition_id,
|
||||
path,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
777
crates/bds-ui/src/views/import_editor.rs
Normal file
777
crates/bds-ui/src/views/import_editor.rs
Normal 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(§ion);
|
||||
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",
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -123,6 +123,7 @@ pub enum ConfirmAction {
|
||||
DeleteTemplate(String),
|
||||
ForceDeleteTemplate(String),
|
||||
DeleteTag(String),
|
||||
DeleteImport(String),
|
||||
MergeTags {
|
||||
sources: Vec<String>,
|
||||
target: String,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user