diff --git a/Cargo.toml b/Cargo.toml index f2a4523..36a12c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +all-colors = "0.0" bytes = "1.9" clap = { version = "4.5", features = ["cargo", "derive"] } color-eyre = "0.6" @@ -13,6 +14,7 @@ derive_more = { version = "1.0", features = [ "deref", "from", ] } +inquire = "0.7" # kxio = {path = "../kxio/"} kxio = "4.0" reqwest = { version = "0.12" , features = ["multipart", "stream"]} diff --git a/src/execute.rs b/src/execute.rs index e078c93..335685b 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -13,7 +13,7 @@ impl Execute for crate::Command { match self { Self::Init => Err(eyre!("Config file already exists. Not overwriting it.")), Self::Check => crate::check::run(ctx).await, - Self::Import => todo!(), //crate::import::run(ctx).await, + Self::Import => crate::import::run(ctx).await, Self::Trello(cmd) => cmd.execute(ctx).await, Self::Nextcloud(cmd) => cmd.execute(ctx).await, } diff --git a/src/import/mod.rs b/src/import/mod.rs new file mode 100644 index 0000000..02bd699 --- /dev/null +++ b/src/import/mod.rs @@ -0,0 +1,225 @@ +// +use std::path::PathBuf; +use std::{collections::HashMap, ops::Deref}; + +use all_colors::get_color_hex; +use color_eyre::eyre::eyre; + +use crate::{ + f, + nextcloud::model::{ + Label, NextcloudBoardTitle, NextcloudCardDescription, NextcloudCardTitle, + NextcloudLabelColour, NextcloudLabelTitle, + }, + p, + trello::model::{ + attachment::TrelloAttachmentId, + board::{TrelloBoardId, TrelloBoardName, TrelloBoards}, + list::TrelloListId, + }, + FullCtx, +}; + +pub(crate) async fn run(ctx: &FullCtx) -> color_eyre::Result<()> { + // get list of trello boards + let trello_client = ctx.trello_client(); + let trello_boards = trello_client.boards().await.result?; + // prompt user to select a board + let trello_board_name = inquire::Select::new( + "Select a source Trello board?", + trello_boards + .iter() + .map(|b| b.name.as_ref()) + .collect::>(), + ) + .prompt() + .map(TrelloBoardName::new)?; + let trello_board_id = trello_boards + .find_by_name(&trello_board_name) + .map(|b| b.id.as_ref()) + .map(TrelloBoardId::new) + .expect("find selected board"); + // get list of trello stacks for the selected board + let trello_stacks = trello_client.board(&trello_board_id).await.result?.lists; + // prompt user to select some stacks + let trello_stack_names = trello_stacks + .iter() + .map(|s| s.name.as_ref()) + .collect::>(); + let selected_trello_stack_names = + inquire::MultiSelect::new("Select Trello stacks to import?", trello_stack_names) + .prompt() + .expect("select stacks"); + let selected_trello_stacks = trello_stacks + .iter() + .filter(|s| selected_trello_stack_names.contains(&s.name.as_ref())) + .collect::>(); + if selected_trello_stacks.is_empty() { + return Err(eyre!(f!("no stacks selected"))); + } + + // get list of nextcloud boards + let deck_client = ctx.deck_client(); + let nextcloud_boards = deck_client.get_boards().await.result?; + // prompt user to select a board + let nextcloud_board_name = inquire::Select::new( + "Select a destination Nextcloud board?", + nextcloud_boards + .iter() + .map(|b| b.title.as_ref()) + .collect::>(), + ) + .prompt() + .map(NextcloudBoardTitle::new) + .expect("select board"); + let nextcloud_board_id = nextcloud_boards + .iter() + .find(|b| b.title == nextcloud_board_name) + .map(|b| b.id) + .expect("find selected board"); + // get list of nextcloud stacks in the selected board + let nextcloud_stacks = deck_client.get_stacks(nextcloud_board_id).await.result?; + // identify any stacks by name from those selected in trello that are missing in nextcloud + let missing_stack_names = selected_trello_stack_names + .iter() + .filter(|s| !nextcloud_stacks.iter().any(|ns| ns.title.deref() == **s)) + .cloned() + .collect::>(); + if !missing_stack_names.is_empty() { + crate::p!(ctx.prt, "Missing stacks: {:?}", missing_stack_names); + // create any missing stacks in nextcloud + // for each missing stack + for missing_stack_name in missing_stack_names.into_iter() { + // - create the stack + let stack = deck_client + .create_stack(nextcloud_board_id, &missing_stack_name.clone().into()) + .await + .result?; + p!(ctx.prt, "Created stack: {}", stack.title); + } + p!(ctx.prt, "Stacks created"); + } + // - get the list of nextcloud stacks again (with new stack ids) + let nextcloud_stacks = deck_client.get_stacks(nextcloud_board_id).await.result?; + + let mut nextcloud_labels: HashMap = deck_client + .get_board(nextcloud_board_id) + .await + .result? + .labels + .iter() + .map(|l| (l.title.clone(), l.clone())) + .collect(); + + // for each selected trello stack + for selected_trello_stack in selected_trello_stacks.into_iter() { + p!(ctx.prt, "Importing stack: {}", selected_trello_stack.name); + let nextcloud_stack_id = nextcloud_stacks + .iter() + .find(|ns| ns.title.deref() == selected_trello_stack.name.as_ref()) + .map(|ns| ns.id) + .expect("find nextcloud stack"); + // - get the list of trello cards in the stack + let trello_cards = trello_client + .list_cards(&TrelloListId::new(selected_trello_stack.id.as_ref())) + .await + .result?; + // - for each card in the trello stack + for trello_card in trello_cards.into_iter() { + p!(ctx.prt, "> Importing card: {}", trello_card.name); + // - - create a nextcloud card + let title: NextcloudCardTitle = NextcloudCardTitle::new(trello_card.name); + let desc: Option = match trello_card.desc.len() { + 0 => None, + _ => Some(NextcloudCardDescription::new(trello_card.desc)), + }; + let nextcloud_card = deck_client + .create_card( + nextcloud_board_id, + nextcloud_stack_id, + &title, + desc.as_ref(), + ) + .await + .result?; + // - - for each label on the trello card + for trello_label in trello_card.labels.iter() { + p!( + ctx.prt, + ">> Adding label: {} ({})", + trello_label.name, + trello_label.color + ); + // - - - find the equivalent label in nextcloud + let nextcloud_label: &Label = match nextcloud_labels + .get(&NextcloudLabelTitle::new(trello_label.name.as_ref())) + { + Some(label) => label, + None => { + p!(ctx.prt, ">> Label not found in nextcloud board, creating"); + + let label = deck_client + .create_label( + nextcloud_board_id, + &NextcloudLabelTitle::new(trello_label.name.as_ref()), + &NextcloudLabelColour::new(get_color_hex( + trello_label.color.as_ref(), + )), + ) + .await + .result?; + nextcloud_labels.insert(label.title.clone(), label); + nextcloud_labels + .get(&NextcloudLabelTitle::new(trello_label.name.as_ref())) + .expect("label was just inserted") + } + }; + // - - - add the label to the nextcloud card + deck_client + .add_label_to_card( + nextcloud_board_id, + nextcloud_stack_id, + nextcloud_card.id, + nextcloud_label.id, + ) + .await + .result?; + } + // - - for each attachment on the trello card + let full_card = trello_client.card(&trello_card.id).await.result?; + for trello_attachment in full_card.attachments.iter() { + p!(ctx.prt, ">> Adding attachment: {}", trello_attachment.name); + // - - - download the attachment from trello + let attachment_path = trello_client + .save_attachment( + &trello_card.id, + &TrelloAttachmentId::new(&trello_attachment.id), + Some(&PathBuf::from(&trello_attachment.id)), + ) + .await?; + let attachment_file = ctx.fs.file(&attachment_path); + // - - - upload the attachment to nextcloud card + let attachment = deck_client + .add_attachment_to_card( + nextcloud_board_id, + nextcloud_stack_id, + nextcloud_card.id, + &attachment_file, + ) + .await + .result?; + p!( + ctx.prt, + ">> Attachment added: {}:{}", + attachment.id, + attachment.attachment_type, + // attachment.extended_data.mimetype + ); + // delete local copy of attachment + attachment_file.remove()?; + } + } + } + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 01de6e6..cb347cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ mod api_result; mod check; mod config; mod execute; +mod import; mod init; mod macros; mod nextcloud; diff --git a/src/nextcloud/card.rs b/src/nextcloud/card.rs index 4b0007a..25a1191 100644 --- a/src/nextcloud/card.rs +++ b/src/nextcloud/card.rs @@ -139,7 +139,7 @@ impl Execute for NextcloudCardCommand { (*board_id).into(), (*stack_id).into(), (*card_id).into(), - ctx.fs.file(file), + &ctx.fs.file(file), ) .await; if *dump { @@ -148,12 +148,12 @@ impl Execute for NextcloudCardCommand { let attachment = api_result.result?; p!( ctx.prt, - "{}:{}:{}:{}:{}", + "{}:{}:{}:{}", board_id, stack_id, card_id, attachment.id, - attachment.extended_data.path + // attachment.extended_data.path ); } Ok(()) diff --git a/src/nextcloud/client.rs b/src/nextcloud/client.rs index 688ee62..f442018 100644 --- a/src/nextcloud/client.rs +++ b/src/nextcloud/client.rs @@ -238,7 +238,7 @@ impl<'ctx> DeckClient<'ctx> { board_id: NextcloudBoardId, stack_id: NextcloudStackId, card_id: NextcloudCardId, - file: FileHandle, + file: &FileHandle, ) -> APIResult { let form: multipart::Form = multipart::Form::new(); let form = form.text("type", "file"); diff --git a/src/nextcloud/model.rs b/src/nextcloud/model.rs index bf65e01..17f480f 100644 --- a/src/nextcloud/model.rs +++ b/src/nextcloud/model.rs @@ -60,6 +60,7 @@ newtype!( i64, Copy, Display, + Hash, PartialOrd, Ord, "ID of a Nextcloud Label" @@ -68,6 +69,7 @@ newtype!( NextcloudLabelTitle, String, Display, + Hash, PartialOrd, Ord, "Title of a Nextcloud Label" @@ -76,6 +78,7 @@ newtype!( NextcloudLabelColour, String, Display, + Hash, PartialOrd, Ord, "Colour of a Nextcloud Label" @@ -136,24 +139,11 @@ newtype!( "Description of the Card" ); -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) struct NextcloudBoardOwner { - #[serde(rename = "primaryKey")] - pub(crate) primary_key: String, - pub(crate) uid: String, - #[serde(rename = "displayname")] - pub(crate) display_name: String, -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub(crate) struct Board { pub(crate) id: NextcloudBoardId, pub(crate) title: NextcloudBoardTitle, - pub(crate) owner: NextcloudBoardOwner, - pub(crate) color: NextcloudBoardColour, - pub(crate) archived: bool, pub(crate) labels: Vec