Allow task_type to be updated after creation

Amolith created

Change summary

src/cli.rs                         |  4 ++++
src/cmd/mod.rs                     |  2 ++
src/cmd/update.rs                  |  3 +++
src/cmd/webui/project/mutations.rs |  3 +++
src/ops.rs                         |  4 ++++
tests/cli_update.rs                | 19 +++++++++++++++++++
6 files changed, 35 insertions(+)

Detailed changes

src/cli.rs 🔗

@@ -112,6 +112,10 @@ pub enum Command {
         /// Set description
         #[arg(short = 'd', long = "desc")]
         desc: Option<String>,
+
+        /// Set task type (e.g. task, bug, feature)
+        #[arg(long = "type")]
+        task_type: Option<String>,
     },
 
     /// Mark task(s) as closed

src/cmd/mod.rs 🔗

@@ -94,6 +94,7 @@ pub fn dispatch(cli: &Cli) -> Result<()> {
             effort,
             title,
             desc,
+            task_type,
         } => {
             let root = require_root()?;
             let pri = priority.as_deref().map(Priority::parse).transpose()?;
@@ -107,6 +108,7 @@ pub fn dispatch(cli: &Cli) -> Result<()> {
                     effort: eff,
                     title: title.as_deref(),
                     desc: desc.as_deref(),
+                    task_type: task_type.as_deref(),
                     json: cli.json,
                 },
             )

src/cmd/update.rs 🔗

@@ -12,6 +12,7 @@ pub struct Opts<'a> {
     pub effort: Option<Effort>,
     pub title: Option<&'a str>,
     pub desc: Option<&'a str>,
+    pub task_type: Option<&'a str>,
     pub json: bool,
 }
 
@@ -30,6 +31,7 @@ pub fn run(root: &Path, id: &str, opts: Opts) -> Result<()> {
         && opts.effort.is_none()
         && opts.title.is_none()
         && opts.desc.is_none()
+        && opts.task_type.is_none()
     {
         let interactive = std::env::var("TD_FORCE_EDITOR").is_ok()
             || std::io::IsTerminal::is_terminal(&std::io::stdin());
@@ -73,6 +75,7 @@ pub fn run(root: &Path, id: &str, opts: Opts) -> Result<()> {
             effort: opts.effort,
             title: title_override.map(String::from),
             description: desc_override.map(String::from),
+            task_type: opts.task_type.map(String::from),
         },
     )?;
 

src/cmd/webui/project/mutations.rs 🔗

@@ -60,6 +60,8 @@ pub(in crate::cmd::webui) struct UpdateForm {
     #[serde(default)]
     effort: Option<String>,
     #[serde(default)]
+    task_type: Option<String>,
+    #[serde(default)]
     redirect: Option<String>,
 }
 
@@ -197,6 +199,7 @@ pub(in crate::cmd::webui) async fn update_handler(
                     .transpose()?,
                 title: form.title.filter(|s| !s.is_empty()),
                 description: form.description,
+                task_type: form.task_type.filter(|s| !s.is_empty()),
             },
         )?;
 

src/ops.rs 🔗

@@ -83,6 +83,7 @@ pub struct UpdateOpts {
     pub effort: Option<Effort>,
     pub title: Option<String>,
     pub description: Option<String>,
+    pub task_type: Option<String>,
 }
 
 /// Update task fields and return the refreshed task.
@@ -108,6 +109,9 @@ pub fn update_task(store: &Store, task_id: &TaskId, opts: UpdateOpts) -> Result<
         if let Some(ref d) = opts.description {
             task.insert("description", d.as_str())?;
         }
+        if let Some(ref tt) = opts.task_type {
+            task.insert("type", tt.as_str())?;
+        }
         task.insert("updated_at", ts.clone())?;
         Ok(())
     })?;

tests/cli_update.rs 🔗

@@ -129,6 +129,25 @@ fn update_changes_effort() {
     assert_eq!(t["effort"].as_str().unwrap(), "high");
 }
 
+#[test]
+fn update_changes_task_type() {
+    let tmp = init_tmp();
+    let id = create_task(&tmp, "Reclassify me");
+
+    // Default type is "task"
+    let t = get_task_json(&tmp, &id);
+    assert_eq!(t["type"].as_str().unwrap(), "task");
+
+    td(&tmp)
+        .args(["update", &id, "--type", "bug"])
+        .current_dir(&tmp)
+        .assert()
+        .success();
+
+    let t = get_task_json(&tmp, &id);
+    assert_eq!(t["type"].as_str().unwrap(), "bug");
+}
+
 // ── done ─────────────────────────────────────────────────────────────
 
 #[test]