diff --git a/src/api/mod.rs b/src/api/mod.rs index b4c5912..1c01bfa 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -5,9 +5,11 @@ mod task; pub use project::Project; pub use task::Comment; +pub use task::Relation; pub use task::Task; use moka::sync::Cache; +use task::TaskRelation; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Label { @@ -356,6 +358,30 @@ impl VikunjaAPI { serde_json::from_str(&resp).unwrap() } + pub fn remove_relation(&self, task_id: isize, relation: Relation, other_task_id: isize) { + self.delete_request(&format!( + "/tasks/{task_id}/relations/{}/{other_task_id}", + relation.api() + )); + } + + pub fn add_relation( + &self, + task_id: isize, + relation: Relation, + other_task_id: isize, + ) -> TaskRelation { + let resp = self.put_request( + &format!("/tasks/{task_id}/relations"), + &serde_json::json!({ + "task_id": task_id, + "other_task_id": other_task_id, + "relation_kind": relation.api() + }), + ); + serde_json::from_str(&resp).unwrap() + } + pub fn new_comment(&self, task_id: isize, comment: &str) -> Comment { let resp = self.put_request( &format!("/tasks/{task_id}/comments"), diff --git a/src/api/task.rs b/src/api/task.rs index 06a2003..91d636a 100644 --- a/src/api/task.rs +++ b/src/api/task.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; use super::{Label, User}; @@ -23,7 +25,7 @@ pub struct Task { pub percent_done: f64, pub identifier: String, pub index: usize, - // pub related_tasks + pub related_tasks: Option>>, // pub attachments pub cover_image_attachment_id: usize, pub is_favorite: bool, @@ -43,3 +45,83 @@ pub struct Comment { pub id: isize, pub updated: String, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TaskRelation { + pub created: String, + pub created_by: User, + pub other_task_id: isize, + pub task_id: isize, + pub relation_kind: String, +} + +pub enum Relation { + Unknown, + Subtask, + ParentTask, + Related, + DuplicateOf, + Duplicates, + Blocking, + Blocked, + Precedes, + Follows, + CopiedFrom, + CopiedTo, +} + +impl Relation { + pub fn try_parse(val: &str) -> Option { + match val { + "unknown" => Some(Self::Unknown), + "subtask" | "sub" => Some(Self::Subtask), + "parenttask" | "parent" => Some(Self::ParentTask), + "related" => Some(Self::Related), + "duplicateof" => Some(Self::DuplicateOf), + "duplicates" => Some(Self::Duplicates), + "blocking" => Some(Self::Blocking), + "blocked" => Some(Self::Blocked), + "precedes" => Some(Self::Precedes), + "follows" => Some(Self::Follows), + "copiedfrom" => Some(Self::CopiedFrom), + "copiedto" => Some(Self::CopiedTo), + _ => None, + } + } + + pub fn repr(&self) -> String { + match self { + Relation::Unknown => "Unknown", + Relation::Subtask => "Subtask", + Relation::ParentTask => "Parent Task", + Relation::Related => "Related", + Relation::DuplicateOf => "Duplicate of", + Relation::Duplicates => "Duplicates", + Relation::Blocking => "Blocking", + Relation::Blocked => "Blocked by", + Relation::Precedes => "Precedes", + Relation::Follows => "Follows", + Relation::CopiedFrom => "Copied from", + Relation::CopiedTo => "Copied to", + } + .to_string() + } + + pub fn api(&self) -> String { + match self { + Relation::Unknown => "unknown", + Relation::Subtask => "subtask", + Relation::ParentTask => "parenttask", + Relation::Related => "related", + Relation::DuplicateOf => "duplicateof", + Relation::Duplicates => "duplicates", + Relation::Blocking => "blocking", + Relation::Blocked => "blocked", + Relation::Precedes => "precedes", + Relation::Follows => "follows", + Relation::CopiedFrom => "copiedfrom", + Relation::CopiedTo => "copiedto", + } + .to_string() + } +} diff --git a/src/args.rs b/src/args.rs index 8484044..c0694e0 100644 --- a/src/args.rs +++ b/src/args.rs @@ -81,6 +81,15 @@ pub fn get_args() -> clap::ArgMatches { .arg(arg!([task_id] "Task ID").required(true)) .arg(arg!([comment] "Comment").required(true)), ) + .subcommand( + command!() + .name("relation") + .about("Set task relations") + .arg(arg!(-d --delete "Delete the relation").required(false)) + .arg(arg!([task_id] "Task ID").required(true)) + .arg(arg!([relation] "Relation").required(true)) + .arg(arg!([second_task_id] "Other Task ID").required(true)), + ) .subcommand( command!() .name("fav") diff --git a/src/main.rs b/src/main.rs index 7e45301..72555ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,9 @@ mod args; mod config; mod ui; -use api::{ProjectID, VikunjaAPI}; +use api::{ProjectID, Relation, VikunjaAPI}; // todo : error handling -// todo : task relations fn main() { let arg = args::get_args(); @@ -155,6 +154,30 @@ fn main() { api.fav_task(task_id.parse().unwrap(), !undo); ui::task::print_task_info(task_id.parse().unwrap(), &api); } + Some(("relation", rel_args)) => { + let task_id: &String = rel_args.get_one("task_id").unwrap(); + let relation: &String = rel_args.get_one("relation").unwrap(); + let sec_task_id: &String = rel_args.get_one("second_task_id").unwrap(); + let delete = rel_args.get_flag("delete"); + + let relation = Relation::try_parse(&relation).unwrap(); + + if delete { + api.remove_relation( + task_id.parse().unwrap(), + relation, + sec_task_id.parse().unwrap(), + ); + } else { + api.add_relation( + task_id.parse().unwrap(), + relation, + sec_task_id.parse().unwrap(), + ); + } + + ui::task::print_task_info(task_id.parse().unwrap(), &api); + } _ => { let done = arg.get_flag("done"); let fav = arg.get_flag("favorite"); diff --git a/src/ui/task.rs b/src/ui/task.rs index e20cd8d..aa10638 100644 --- a/src/ui/task.rs +++ b/src/ui/task.rs @@ -1,5 +1,5 @@ use crate::{ - api::{Comment, Project, ProjectID, Task, VikunjaAPI}, + api::{Comment, Project, ProjectID, Relation, Task, VikunjaAPI}, ui::{ format_html_to_terminal, hex_to_color, is_in_past, parse_datetime, print_color, print_label, time_relative, @@ -157,10 +157,6 @@ pub fn print_task_info(task_id: isize, api: &VikunjaAPI) { println!(); } - if task.description != "

" && !task.description.is_empty() { - println!("---\n{}", format_html_to_terminal(&task.description)); - } - if let Some(assigned) = task.assignees { print!("Assigned to: "); for assignee in assigned { @@ -169,6 +165,25 @@ pub fn print_task_info(task_id: isize, api: &VikunjaAPI) { println!(); } + if let Some(related) = task.related_tasks { + for relation in related { + print_color( + crossterm::style::Color::Magenta, + &format!("{}: ", Relation::try_parse(&relation.0).unwrap().repr()), + ); + for t in relation.1 { + print_color(crossterm::style::Color::Blue, &t.title); + print_color(crossterm::style::Color::Yellow, &format!(" ({})", t.id)); + print!(" "); + } + println!(); + } + } + + if task.description != "

" && !task.description.is_empty() { + println!("---\n{}", format_html_to_terminal(&task.description)); + } + // pub percent_done: f64, }