Skip to main content

clickup_cli/commands/
comment.rs

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