diff --git a/Cargo.lock b/Cargo.lock index e0ce716..4e6a8c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -431,11 +431,22 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + [[package]] name = "heck" version = "0.5.0" @@ -965,6 +976,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.9.4" @@ -1566,6 +1583,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -2002,6 +2030,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 75fe425..4ee2b3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ comfy-table = "7.2" futures = "0.3" base64 = "0.22" mime_guess = "2.0" +uuid = { version = "1.23.3", features = ["v4"] } [profile.release] strip = true diff --git a/src/commands/ticket.rs b/src/commands/ticket.rs index d7c18ce..29ce990 100644 --- a/src/commands/ticket.rs +++ b/src/commands/ticket.rs @@ -94,6 +94,9 @@ pub enum TicketCmd { /// Comma-separated tags to remove #[arg(long = "tags-remove")] tags_remove: Option, + /// Comma-separated Jira ticket keys (e.g. "TIO-817, TIO-818") + #[arg(long = "jira-tickets")] + jira_tickets: Option, }, /// Article subcommands Article { @@ -107,6 +110,18 @@ pub enum TicketCmd { }, /// Get summary of ticket counts by state Overview, + /// Create or update a shared draft (internal ID — see `ticket get` for the numeric ID) + SharedDraft { + id: String, + #[arg(long)] + body: String, + /// Article type (email or note; default: email) + #[arg(long, default_value = "email")] + r#type: String, + /// Mark as internal note (default: public reply) + #[arg(long)] + internal: bool, + }, } #[derive(Subcommand)] @@ -217,6 +232,7 @@ pub async fn run(cmd: TicketCmd, client: &ZammadClient, json: bool) -> Result<() organization, tags_add, tags_remove, + jira_tickets, } => { update( client, @@ -230,9 +246,10 @@ pub async fn run(cmd: TicketCmd, client: &ZammadClient, json: bool) -> Result<() organization, tags_add, tags_remove, + jira_tickets, json, ) - .await + .await } TicketCmd::Article { cmd } => match cmd { ArticleCmd::Add { @@ -254,6 +271,12 @@ pub async fn run(cmd: TicketCmd, client: &ZammadClient, json: bool) -> Result<() } => attachment_download(client, &id, article, attachment, all, &out, json).await, }, TicketCmd::Overview => overview(client, json).await, + TicketCmd::SharedDraft { + id, + body, + r#type, + internal, + } => shared_draft(client, &id, body, r#type, internal, json).await, } } @@ -563,6 +586,7 @@ async fn update( organization: Option, tags_add: Option, tags_remove: Option, + jira_tickets: Option, json: bool, ) -> Result<()> { // Pending states require a `pending_time`; fail early with a clear message @@ -610,6 +634,7 @@ async fn update( insert_opt_str(&mut body, "title", title); insert_opt_str(&mut body, "customer", customer); insert_opt_str(&mut body, "organization", organization); + insert_opt_str(&mut body, "jira_tickets", jira_tickets); let add_list = tags_add.as_deref().map(split_csv).unwrap_or_default(); let remove_list = tags_remove.as_deref().map(split_csv).unwrap_or_default(); @@ -756,6 +781,53 @@ async fn overview(client: &ZammadClient, json: bool) -> Result<()> { Ok(()) } +/// Create or update a shared draft for a ticket. +/// +/// PUT /api/v1/tickets/{ticket_id}/shared_draft +async fn shared_draft( + client: &ZammadClient, + id_str: &str, + body_text: String, + article_type: String, + internal: bool, + json: bool, +) -> Result<()> { + let resolved = resolve_ticket_id(client, id_str).await?; + let form_id = uuid::Uuid::new_v4().to_string(); + + let payload = serde_json::json!({ + "form_id": form_id, + "new_article": { + "body": body_text, + "content_type": "text/html", + "type": article_type, + "internal": internal, + }, + "ticket_attributes": {}, + }); + + let value = client + .put( + &format!("/api/v1/tickets/{resolved}/shared_draft"), + Some(&payload), + ) + .await?; + + if json { + return output::emit_value(&value); + } + + let draft_id = value + .get("shared_draft_id") + .and_then(|v| v.as_i64()) + .unwrap_or_default(); + let kind = if internal { "internal" } else { "public" }; + output::print_message(&format!( + "Shared draft #{draft_id} saved on ticket {resolved} ({kind})" + )); + Ok(()) +} + /// Resolve a user-supplied ticket reference to an internal Zammad ID. /// /// Accepts: diff --git a/src/output.rs b/src/output.rs index 018909d..2d5e830 100644 --- a/src/output.rs +++ b/src/output.rs @@ -101,6 +101,11 @@ pub fn print_ticket_detail(t: &Ticket) { "Organization:".bold(), t.organization.as_deref().unwrap_or("-") ); + if let Some(jt) = &t.jira_tickets { + if !jt.is_empty() { + println!(" {} {}", "Jira Tickets:".bold(), jt.yellow()); + } + } if let Some(c) = &t.created_at { println!(" {} {}", "Created:".bold(), c); } diff --git a/src/types.rs b/src/types.rs index 3b62c47..270694c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -25,6 +25,8 @@ pub struct Ticket { pub updated_at: Option, #[serde(default)] pub article_count: Option, + #[serde(default)] + pub jira_tickets: Option, } #[derive(Debug, Clone, Serialize, Deserialize)]