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. Note: ClickUp's v2 comment API does not render markdown; markdown syntax is stored as literal 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. Note: ClickUp's v2 comment API does not render markdown; markdown syntax is stored as literal 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. Note: ClickUp's v2 comment API does not render markdown; markdown syntax is stored as literal 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 base = if let Some(id) = list {
91                format!("/v2/list/{}/comment", id)
92            } else if let Some(id) = view {
93                format!("/v2/view/{}/comment", id)
94            } else if let Some(resolved) = git::resolve_task(cli, task.as_deref(), true)? {
95                format!("/v2/task/{}/comment", resolved.id)
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 comments = crate::commands::pagination::walk_start_id(
103                cli,
104                &client,
105                "comments",
106                |start, start_id| match (start, start_id) {
107                    (Some(s), Some(sid)) => format!("{}?start={}&start_id={}", base, s, sid),
108                    _ => base.clone(),
109                },
110            )
111            .await?;
112            let truncated: Vec<serde_json::Value> = comments
113                .into_iter()
114                .map(|mut c| {
115                    if let Some(text) = c.get("comment_text").and_then(|v| v.as_str()) {
116                        // Truncate by chars (not bytes) so the 60-byte boundary
117                        // can't land inside a multibyte UTF-8 codepoint.
118                        let truncated = if text.chars().count() > 60 {
119                            let head: String = text.chars().take(60).collect();
120                            format!("{}…", head)
121                        } else {
122                            text.to_string()
123                        };
124                        c["comment_text"] = serde_json::Value::String(truncated);
125                    }
126                    c
127                })
128                .collect();
129            output.print_items(&truncated, COMMENT_FIELDS, "id");
130            Ok(())
131        }
132        CommentCommands::Create {
133            task,
134            list,
135            view,
136            text,
137            assignee,
138            notify_all,
139        } => {
140            let (url, resp) = if let Some(id) = list {
141                let body = serde_json::json!({ "comment_text": text });
142                let r = client
143                    .post(&format!("/v2/list/{}/comment", id), &body)
144                    .await?;
145                (format!("/v2/list/{}/comment", id), r)
146            } else if let Some(id) = view {
147                let body = serde_json::json!({ "comment_text": text });
148                let r = client
149                    .post(&format!("/v2/view/{}/comment", id), &body)
150                    .await?;
151                (format!("/v2/view/{}/comment", id), r)
152            } else if let Some(resolved) = git::resolve_task(cli, task.as_deref(), true)? {
153                let mut body = serde_json::json!({
154                    "comment_text": text,
155                    "notify_all": notify_all,
156                });
157                if let Some(a) = assignee {
158                    body["assignee"] = serde_json::json!(a);
159                }
160                let r = client
161                    .post(&format!("/v2/task/{}/comment", resolved.id), &body)
162                    .await?;
163                (format!("/v2/task/{}/comment", resolved.id), r)
164            } else {
165                return Err(CliError::ClientError {
166                    message: "One of --task, --list, or --view is required".to_string(),
167                    status: 0,
168                });
169            };
170            let _ = url;
171            output.print_single(&resp, COMMENT_FIELDS, "id");
172            Ok(())
173        }
174        CommentCommands::Update {
175            id,
176            text,
177            resolved,
178            assignee,
179        } => {
180            let mut body = serde_json::json!({ "comment_text": text });
181            if resolved {
182                body["resolved"] = serde_json::Value::Bool(true);
183            }
184            if let Some(a) = assignee {
185                body["assignee"] = serde_json::json!(a);
186            }
187            let resp = client.put(&format!("/v2/comment/{}", id), &body).await?;
188            output.print_single(&resp, COMMENT_FIELDS, "id");
189            Ok(())
190        }
191        CommentCommands::Delete { id } => {
192            client.delete(&format!("/v2/comment/{}", id)).await?;
193            output.print_message(&format!("Comment {} deleted", id));
194            Ok(())
195        }
196        CommentCommands::Replies { id } => {
197            let comments = crate::commands::pagination::walk_start_id(
198                cli,
199                &client,
200                "comments",
201                |start, start_id| match (start, start_id) {
202                    (Some(s), Some(sid)) => {
203                        format!("/v2/comment/{}/reply?start={}&start_id={}", id, s, sid)
204                    }
205                    _ => format!("/v2/comment/{}/reply", id),
206                },
207            )
208            .await?;
209            output.print_items(&comments, COMMENT_FIELDS, "id");
210            Ok(())
211        }
212        CommentCommands::Reply { id, text, assignee } => {
213            let mut body = serde_json::json!({ "comment_text": text });
214            if let Some(a) = assignee {
215                body["assignee"] = serde_json::json!(a);
216            }
217            let resp = client
218                .post(&format!("/v2/comment/{}/reply", id), &body)
219                .await?;
220            output.print_single(&resp, COMMENT_FIELDS, "id");
221            Ok(())
222        }
223    }
224}