feat: make best-effort to maintain order of stacks and cards
Some checks failed
Release Please / Release-plz (push) Failing after 42s
Test / build (map[name:stable]) (push) Has been cancelled
Test / build (map[name:nightly]) (push) Has been cancelled

This commit is contained in:
Paul Campbell 2024-12-22 22:47:29 +00:00
parent f9762512f4
commit 4f69fc0a4b
8 changed files with 90 additions and 24 deletions

View file

@ -1,6 +1,12 @@
// //
use crate::nextcloud::model::{NextcloudCardDescription, NextcloudCardTitle}; use crate::nextcloud::model::NextcloudStackTitle;
use crate::trello::model::card::{TrelloCardDescription, TrelloCardName}; use crate::{
nextcloud::model::{NextcloudCardDescription, NextcloudCardTitle, NextcloudOrder},
trello::model::{
card::{TrelloCardDescription, TrelloCardName, TrelloCardPosition},
list::{TrelloListName, TrelloListPosition},
},
};
impl From<&TrelloCardName> for NextcloudCardTitle { impl From<&TrelloCardName> for NextcloudCardTitle {
fn from(value: &TrelloCardName) -> Self { fn from(value: &TrelloCardName) -> Self {
@ -13,3 +19,21 @@ impl From<&TrelloCardDescription> for NextcloudCardDescription {
Self::new(value.to_string()) Self::new(value.to_string())
} }
} }
impl From<&TrelloListName> for NextcloudStackTitle {
fn from(value: &TrelloListName) -> Self {
Self::new(value.to_string())
}
}
impl From<&TrelloListPosition> for NextcloudOrder {
fn from(value: &TrelloListPosition) -> Self {
Self::new(value.0)
}
}
impl From<&TrelloCardPosition> for NextcloudOrder {
fn from(value: &TrelloCardPosition) -> Self {
Self::new(value.0)
}
}

View file

@ -40,7 +40,9 @@ pub(crate) async fn run(ctx: &FullCtx) -> color_eyre::Result<()> {
.map(TrelloBoardId::new) .map(TrelloBoardId::new)
.expect("find selected board"); .expect("find selected board");
// get list of trello stacks for the selected board // get list of trello stacks for the selected board
let trello_stacks = trello_client.board(&trello_board_id).await.result?.lists; let mut trello_stacks = trello_client.board(&trello_board_id).await.result?.lists;
// sort stacks by position
trello_stacks.sort_by_key(|s| s.pos);
// prompt user to select some stacks // prompt user to select some stacks
let trello_stack_names = trello_stacks let trello_stack_names = trello_stacks
.iter() .iter()

View file

@ -56,10 +56,12 @@ impl Actor for ImportStackActor {
crate::p!(this.ctx.prt, "Importing stack: {}", this.trello_stack.name); crate::p!(this.ctx.prt, "Importing stack: {}", this.trello_stack.name);
// - get the list of trello cards in the stack // - get the list of trello cards in the stack
let trello_cards = trello_client let mut trello_cards = trello_client
.list_cards(&TrelloListId::new(this.trello_stack.id.as_ref())) .list_cards(&TrelloListId::new(this.trello_stack.id.as_ref()))
.await .await
.result?; .result?;
// sort cards by their position
trello_cards.sort_by_key(|card| card.pos);
// - for each card in the trello stack // - for each card in the trello stack
for trello_card in trello_cards.into_iter() { for trello_card in trello_cards.into_iter() {

View file

@ -21,26 +21,41 @@ use crate::{
async fn create_any_missing_stacks( async fn create_any_missing_stacks(
ctx: &FullCtx, ctx: &FullCtx,
selected_trello_stack_names: &[String], selected_trello_stacks: &[TrelloList],
nextcloud_board_id: NextcloudBoardId, nextcloud_board_id: NextcloudBoardId,
nextcloud_stacks: Vec<Stack>, nextcloud_stacks: Vec<Stack>,
) -> color_eyre::Result<()> { ) -> color_eyre::Result<()> {
let deck_client: DeckClient = ctx.deck_client(); let deck_client: DeckClient = ctx.deck_client();
// identify any stacks by name from those selected in trello that are missing in nextcloud // identify any stacks by name from those selected in trello that are missing in nextcloud
let missing_stack_names = selected_trello_stack_names let missing_stacks = selected_trello_stacks
.iter() .iter()
.filter(|s| !nextcloud_stacks.iter().any(|ns| ns.title.deref() == *s)) .filter(|trello_stack| {
!nextcloud_stacks
.iter()
.any(|nextcloud_stack| nextcloud_stack.title.deref() == trello_stack.name.deref())
})
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if !missing_stack_names.is_empty() { if !missing_stacks.is_empty() {
crate::p!(ctx.prt, "Missing stacks: {:?}", missing_stack_names); crate::p!(
ctx.prt,
"Missing stacks: {:?}",
missing_stacks
.iter()
.map(|s| s.name.as_ref())
.collect::<Vec<_>>()
);
// create any missing stacks in nextcloud // create any missing stacks in nextcloud
// for each missing stack // for each missing stack
for missing_stack_name in missing_stack_names.into_iter() { for missing_stack in missing_stacks.into_iter() {
// - create the stack // - create the stack
let stack = deck_client let stack = deck_client
.create_stack(nextcloud_board_id, &missing_stack_name.clone().into()) .create_stack(
nextcloud_board_id,
(&missing_stack.name).into(),
(&missing_stack.pos).into(),
)
.await .await
.result?; .result?;
p!(ctx.prt, "Created stack: {}", stack.title); p!(ctx.prt, "Created stack: {}", stack.title);
@ -83,7 +98,12 @@ impl Actor for ImportStacksActor {
on_actor_start!(this, actor_ref, { on_actor_start!(this, actor_ref, {
// spawn a new ImportStack actor for each trello_stack named in selected_trello_stack_names // spawn a new ImportStack actor for each trello_stack named in selected_trello_stack_names
let mut trello_stacks = vec![];
std::mem::swap(&mut this.trello_stacks, &mut trello_stacks);
let selected_trello_stacks = trello_stacks
.into_iter()
.filter(|s| this.selected_trello_stack_names.contains(s.name.as_ref()))
.collect::<Vec<_>>();
// get list of nextcloud stacks in the selected board // get list of nextcloud stacks in the selected board
let nextcloud_stacks = this let nextcloud_stacks = this
.ctx .ctx
@ -93,7 +113,7 @@ impl Actor for ImportStacksActor {
.result?; .result?;
create_any_missing_stacks( create_any_missing_stacks(
&this.ctx, &this.ctx,
&this.selected_trello_stack_names, &selected_trello_stacks,
this.nextcloud_board_id, this.nextcloud_board_id,
nextcloud_stacks, nextcloud_stacks,
) )
@ -107,11 +127,7 @@ impl Actor for ImportStacksActor {
.result?; .result?;
// for each selected trello stack // for each selected trello stack
for selected_trello_stack in this for selected_trello_stack in selected_trello_stacks {
.trello_stacks
.iter()
.filter(|s| this.selected_trello_stack_names.contains(s.name.as_ref()))
{
let nextcloud_stack_id = //this. let nextcloud_stack_id = //this.
nextcloud_stacks nextcloud_stacks
.iter() .iter()

View file

@ -7,6 +7,7 @@ use kxio::{
use reqwest::multipart; use reqwest::multipart;
use serde_json::json; use serde_json::json;
use crate::nextcloud::model::NextcloudOrder;
use crate::{ use crate::{
api_result::APIResult, api_result::APIResult,
f, f,
@ -118,13 +119,14 @@ impl<'ctx> DeckClient<'ctx> {
pub(crate) async fn create_stack( pub(crate) async fn create_stack(
&self, &self,
board_id: NextcloudBoardId, board_id: NextcloudBoardId,
stack_title: &NextcloudStackTitle, stack_title: NextcloudStackTitle,
stack_order: NextcloudOrder,
) -> APIResult<Stack> { ) -> APIResult<Stack> {
self.request_with_body( self.request_with_body(
f!("boards/{board_id}/stacks"), f!("boards/{board_id}/stacks"),
json!({ json!({
"title": stack_title, "title": stack_title,
"order": 999, "order": stack_order,
}) })
.to_string(), .to_string(),
|net, url| net.post(url), |net, url| net.post(url),

View file

@ -2,6 +2,7 @@
use clap::Parser; use clap::Parser;
use crate::execute::Execute; use crate::execute::Execute;
use crate::nextcloud::model::NextcloudOrder;
use crate::{p, FullCtx}; use crate::{p, FullCtx};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@ -50,7 +51,11 @@ impl Execute for NextcloudStackCommand {
} => { } => {
let api_result = ctx let api_result = ctx
.deck_client() .deck_client()
.create_stack((*board_id).into(), &stack_title.clone().into()) .create_stack(
(*board_id).into(),
stack_title.clone().into(),
NextcloudOrder::new(999),
)
.await; .await;
if *dump { if *dump {
p!(ctx.prt, "{}", api_result.text); p!(ctx.prt, "{}", api_result.text);

View file

@ -11,8 +11,8 @@ newtype!(TrelloCardName, String, Display, "Card Name");
newtype!(TrelloCardDescription, String, Display, "Card Description"); newtype!(TrelloCardDescription, String, Display, "Card Description");
newtype!(TrelloCardDue, String, Display, "Card Due"); newtype!(TrelloCardDue, String, Display, "Card Due");
#[derive(Debug, PartialEq, Eq, Display)] #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Display)]
pub struct TrelloCardPosition(i64); pub(crate) struct TrelloCardPosition(pub i64);
impl<'de> Deserialize<'de> for TrelloCardPosition { impl<'de> Deserialize<'de> for TrelloCardPosition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>

View file

@ -1,6 +1,6 @@
// //
use derive_more::derive::Display; use derive_more::derive::Display;
use serde::Deserialize; use serde::{Deserialize, Deserializer};
use crate::newtype; use crate::newtype;
@ -14,8 +14,23 @@ newtype!(
"List Name" "List Name"
); );
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Display)]
pub(crate) struct TrelloListPosition(pub i64);
impl<'de> Deserialize<'de> for TrelloListPosition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// First deserialize as f64
let value = f64::deserialize(deserializer)?;
// Convert to i64 by rounding
Ok(TrelloListPosition(value.round() as i64))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub(crate) struct TrelloList { pub(crate) struct TrelloList {
pub(crate) id: TrelloListId, pub(crate) id: TrelloListId,
pub(crate) name: TrelloListName, pub(crate) name: TrelloListName,
pub(crate) pos: TrelloListPosition,
} }