Skip to main content

clickup_cli/commands/
comment.rs

1use crate::client::ClickUpClient;
2use crate::commands::auth::resolve_token;
3use crate::error::CliError;
4use crate::git;
5use crate::output::OutputConfig;
6use crate::Cli;
7use clap::Subcommand;
8
9#[derive(Subcommand)]
10pub enum CommentCommands {
11    /// List comments on a task, list, or view
12    List {
13        /// Task ID
14        #[arg(long, conflicts_with_all = ["list", "view"])]
15        task: Option<String>,
16        /// List ID
17        #[arg(long, conflicts_with_all = ["task", "view"])]
18        list: Option<String>,
19        /// View ID
20        #[arg(long, conflicts_with_all = ["task", "list"])]
21        view: Option<String>,
22    },
23    /// Create a comment on a task, list, or view
24    Create {
25        /// Task ID
26        #[arg(long, conflicts_with_all = ["list", "view"])]
27        task: Option<String>,
28        /// List ID
29        #[arg(long, conflicts_with_all = ["task", "view"])]
30        list: Option<String>,
31        /// View ID
32        #[arg(long, conflicts_with_all = ["task", "list"])]
33        view: Option<String>,
34        /// Comment text
35        #[arg(long)]
36        text: String,
37        /// Assignee user ID (task comments only)
38        #[arg(long)]
39        assignee: Option<i64>,
40        /// Notify all watchers (task comments only)
41        #[arg(long)]
42        notify_all: bool,
43    },
44    /// Update a comment
45    Update {
46        /// Comment ID
47        id: String,
48        /// New comment text
49        #[arg(long)]
50        text: String,
51        /// Mark as resolved
52        #[arg(long)]
53        resolved: bool,
54        /// Assignee user ID
55        #[arg(long)]
56        assignee: Option<i64>,
57    },
58    /// Delete a comment
59    Delete {
60        /// Comment ID
61        id: String,
62    },
63    /// List threaded replies on a comment
64    Replies {
65        /// Comment ID
66        id: String,
67    },
68    /// Reply to a comment
69    Reply {
70        /// Comment ID
71        id: String,
72        /// Reply text
73        #[arg(long)]
74        text: String,
75        /// Assignee user ID
76        #[arg(long)]
77        assignee: Option<i64>,
78    },
79}
80
81const COMMENT_FIELDS: &[&str] = &["id", "user", "date", "comment_text"];
82
83pub async fn execute(command: CommentCommands, cli: &Cli) -> Result<(), CliError> {
84    let token = resolve_token(cli)?;
85    let client = ClickUpClient::new(&token, cli.timeout)?;
86    let output = OutputConfig::from_cli(&cli.output, &cli.fields, cli.no_header, cli.quiet);
87
88    match command {
89        CommentCommands::List { task, list, view } => {
90            let (url, key) = if let Some(id) = list {
91                (format!("/v2/list/{}/comment", id), "comments")
92            } else if let Some(id) = view {
93                (format!("/v2/view/{}/comment", id), "comments")
94            } else if let Some(resolved) = git::resolve_task(cli, task.as_deref(), true)? {
95                (format!("/v2/task/{}/comment", resolved.id), "comments")
96            } else {
97                return Err(CliError::ClientError {
98                    message: "One of --task, --list, or --view is required".to_string(),
99                    status: 0,
100                });
101            };
102            let resp = client.get(&url).await?;
103            let comments = resp
104                .get(key)
105                .and_then(|c| c.as_array())
106                .cloned()
107                .unwrap_or_default();
108            let truncated: Vec<serde_json::Value> = comments
109                .into_iter()
110                .map(|mut c| {
111                    if let Some(text) = c.get("comment_text").and_then(|v| v.as_str()) {
112                        let truncated = if text.len() > 60 {
113                            format!("{}…", &text[..60])
114                        } else {
115                            text.to_string()
116                        };
117                        c["comment_text"] = serde_json::Value::String(truncated);
118                    }
119                    c
120                })
121                .collect();
122            output.print_items(&truncated, COMMENT_FIELDS, "id");
123            Ok(())
124        }
125        CommentCommands::Create {
126            task,
127            list,
128            view,
129            text,
130            assignee,
131            notify_all,
132        } => {
133            let (url, resp) = if let Some(id) = list {
134                let body = serde_json::json!({ "comment_text": text });
135                let r = client
136                    .post(&format!("/v2/list/{}/comment", id), &body)
137                    .await?;
138                (format!("/v2/list/{}/comment", id), r)
139            } else if let Some(id) = view {
140                let body = serde_json::json!({ "comment_text": text });
141                let r = client
142                    .post(&format!("/v2/view/{}/comment", id), &body)
143                    .await?;
144                (format!("/v2/view/{}/comment", id), r)
145            } else if let Some(resolved) = git::resolve_task(cli, task.as_deref(), true)? {
146                let mut body = serde_json::json!({
147                    "comment_text": text,
148                    "notify_all": notify_all,
149                });
150                if let Some(a) = assignee {
151                    body["assignee"] = serde_json::json!(a);
152                }
153                let r = client
154                    .post(&format!("/v2/task/{}/comment", resolved.id), &body)
155                    .await?;
156                (format!("/v2/task/{}/comment", resolved.id), r)
157            } else {
158                return Err(CliError::ClientError {
159                    message: "One of --task, --list, or --view is required".to_string(),
160                    status: 0,
161                });
162            };
163            let _ = url;
164            output.print_single(&resp, COMMENT_FIELDS, "id");
165            Ok(())
166        }
167        CommentCommands::Update {
168            id,
169            text,
170            resolved,
171            assignee,
172        } => {
173            let mut body = serde_json::json!({ "comment_text": text });
174            if resolved {
175                body["resolved"] = serde_json::Value::Bool(true);
176            }
177            if let Some(a) = assignee {
178                body["assignee"] = serde_json::json!(a);
179            }
180            let resp = client.put(&format!("/v2/comment/{}", id), &body).await?;
181            output.print_single(&resp, COMMENT_FIELDS, "id");
182            Ok(())
183        }
184        CommentCommands::Delete { id } => {
185            client.delete(&format!("/v2/comment/{}", id)).await?;
186            output.print_message(&format!("Comment {} deleted", id));
187            Ok(())
188        }
189        CommentCommands::Replies { id } => {
190            let resp = client.get(&format!("/v2/comment/{}/reply", id)).await?;
191            let comments = resp
192                .get("comments")
193                .and_then(|c| c.as_array())
194                .cloned()
195                .unwrap_or_default();
196            output.print_items(&comments, COMMENT_FIELDS, "id");
197            Ok(())
198        }
199        CommentCommands::Reply { id, text, assignee } => {
200            let mut body = serde_json::json!({ "comment_text": text });
201            if let Some(a) = assignee {
202                body["assignee"] = serde_json::json!(a);
203            }
204            let resp = client
205                .post(&format!("/v2/comment/{}/reply", id), &body)
206                .await?;
207            output.print_single(&resp, COMMENT_FIELDS, "id");
208            Ok(())
209        }
210    }
211}