use super::App;
use kanban_domain::export::{AllBoardsExport, BoardExporter, BoardImporter};
use std::io;
use uuid::Uuid;
impl App {
pub fn export_board_with_filename(&self) -> io::Result<()> {
if let Some(board_id) = self.board_list.get_selected_board_id() {
let export = self.build_boards_export(&[board_id])?;
BoardExporter::export_to_file(&export, self.input.as_str())?;
}
Ok(())
}
pub fn export_all_boards_with_filename(&self) -> io::Result<()> {
let export = self.build_all_boards_export()?;
BoardExporter::export_to_file(&export, self.input.as_str())?;
Ok(())
}
pub fn auto_save(&self) -> io::Result<()> {
if let Some(ref filename) = self.persistence.save_file {
let export = self.build_all_boards_export()?;
BoardExporter::export_to_file(&export, filename)?;
}
Ok(())
}
fn build_all_boards_export(&self) -> io::Result<AllBoardsExport> {
let snapshot = self
.ctx
.snapshot()
.map_err(|e| io::Error::other(e.to_string()))?;
Ok(BoardImporter::convert_snapshot_to_export(snapshot))
}
pub(crate) fn build_boards_export(&self, board_ids: &[Uuid]) -> io::Result<AllBoardsExport> {
let full = self.build_all_boards_export()?;
let selected: Vec<_> = board_ids
.iter()
.filter_map(|id| full.boards.iter().find(|be| be.board.id == *id).cloned())
.collect();
Ok(AllBoardsExport::from_boards(selected))
}
pub fn import_board_from_file(&mut self, filename: &str) -> io::Result<()> {
let content = std::fs::read_to_string(filename)?;
let first_new_index = self.model.live_boards().count();
let default_sprint_prefix = Some(
self.app_config
.effective_default_sprint_prefix()
.to_string(),
);
if let Some(snapshot) = BoardImporter::try_load_snapshot(&content) {
let cmd = kanban_domain::commands::Command::Board(
kanban_domain::commands::BoardCommand::Import(
kanban_domain::commands::ImportEntities {
boards: snapshot.boards,
columns: snapshot.columns,
cards: snapshot.cards,
archived_cards: snapshot.archived_cards,
archived_boards: snapshot.archived_boards,
sprints: snapshot.sprints,
graph: Some(snapshot.graph),
prefixes: snapshot.prefixes,
default_sprint_prefix,
},
),
);
if let Err(e) = self.ctx.execute_command(cmd) {
self.set_error(e.to_string());
tracing::error!("Failed to import V2 board: {}", e);
return Ok(());
}
self.reload_model();
self.prepare_frame();
self.board_list
.inner_mut()
.set_selected_index(Some(first_new_index));
self.switch_view_strategy(kanban_domain::TaskListView::GroupedByColumn);
return Ok(());
}
let import = BoardImporter::import_from_json(&content)?;
let entities = BoardImporter::extract_entities(import);
let cmd =
kanban_domain::commands::Command::Board(kanban_domain::commands::BoardCommand::Import(
kanban_domain::commands::ImportEntities {
boards: entities.boards,
columns: entities.columns,
cards: entities.cards,
archived_cards: entities.archived_cards,
archived_boards: entities.archived_boards,
sprints: entities.sprints,
prefixes: Vec::new(),
graph: None,
default_sprint_prefix,
},
));
if let Err(e) = self.ctx.execute_command(cmd) {
self.set_error(e.to_string());
tracing::error!("Failed to import V1 board: {}", e);
return Ok(());
}
self.reload_model();
self.prepare_frame();
self.board_list
.inner_mut()
.set_selected_index(Some(first_new_index));
self.switch_view_strategy(kanban_domain::TaskListView::GroupedByColumn);
Ok(())
}
}
#[cfg(test)]
mod prefix_default_tests {
use crate::App;
use kanban_domain::KanbanOperations;
#[test]
fn test_tui_import_reconstructs_counters_from_the_configured_defaults() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("export.json");
let mut src = App::test_default();
src.app_config.default_sprint_prefix = Some("iteration".to_string());
src.ctx.set_app_config(src.app_config.clone());
let board = src.ctx.create_board("Src".into(), None).unwrap();
for _ in 0..3 {
let sprint = src.ctx.create_sprint(board.id, None, None).unwrap();
src.ctx
.update_sprint(
sprint.id,
kanban_domain::SprintUpdate {
prefix: kanban_domain::FieldUpdate::Clear,
..Default::default()
},
)
.unwrap();
}
assert_eq!(
src.ctx
.data_store()
.get_prefix("iteration")
.unwrap()
.map(|p| p.sprint_counter),
Some(3),
"precondition: sprints were minted from the configured namespace"
);
src.input.set(path.to_str().unwrap().to_string());
src.export_all_boards_with_filename().unwrap();
let mut dest = App::test_default();
dest.app_config.default_sprint_prefix = Some("iteration".to_string());
dest.ctx.set_app_config(dest.app_config.clone());
dest.import_board_from_file(path.to_str().unwrap()).unwrap();
let store = dest.ctx.data_store();
assert_eq!(
store
.get_prefix("iteration")
.unwrap()
.map_or(0, |p| p.sprint_counter),
3,
"the namespace the imported sprints were minted from was left at zero"
);
assert_eq!(
store
.get_prefix("sprint")
.unwrap()
.map_or(0, |p| p.sprint_counter),
0,
"a namespace nothing in the payload consumed was inflated"
);
}
}
#[cfg(test)]
mod config_sync_tests {
use crate::App;
#[test]
fn test_writing_the_app_config_updates_the_context_copy_too() {
let mut app = App::test_default();
let mut config = app.app_config.clone();
config.default_sprint_prefix = Some("iteration".to_string());
app.set_app_config(config);
assert_eq!(
app.ctx.app_config().effective_default_sprint_prefix(),
"iteration"
);
assert_eq!(
app.app_config.effective_default_sprint_prefix(),
"iteration"
);
}
}