Skip to main content

clickup_cli/commands/
list.rs

1use crate::client::ClickUpClient;
2use crate::commands::auth::resolve_token;
3use crate::error::CliError;
4use crate::output::OutputConfig;
5use crate::Cli;
6use clap::Subcommand;
7
8#[derive(Subcommand)]
9pub enum ListCommands {
10    /// List lists in a folder or space
11    List {
12        /// Folder ID
13        #[arg(long)]
14        folder: Option<String>,
15        /// Space ID (folderless lists)
16        #[arg(long)]
17        space: Option<String>,
18        /// Include archived
19        #[arg(long)]
20        archived: bool,
21    },
22    /// Get list details
23    Get {
24        /// List ID
25        id: String,
26    },
27    /// Create a list
28    Create {
29        /// Folder ID
30        #[arg(long)]
31        folder: Option<String>,
32        /// Space ID (folderless)
33        #[arg(long)]
34        space: Option<String>,
35        /// List name
36        #[arg(long)]
37        name: String,
38        /// List content/description
39        #[arg(long)]
40        content: Option<String>,
41        /// Priority (1-4)
42        #[arg(long)]
43        priority: Option<u8>,
44        /// Due date (YYYY-MM-DD)
45        #[arg(long)]
46        due_date: Option<String>,
47    },
48    /// Update a list
49    Update {
50        /// List ID
51        id: String,
52        /// New name
53        #[arg(long)]
54        name: Option<String>,
55        /// New content
56        #[arg(long)]
57        content: Option<String>,
58    },
59    /// Delete a list
60    Delete {
61        /// List ID
62        id: String,
63    },
64    /// Add a task to this list
65    AddTask {
66        /// List ID
67        list_id: String,
68        /// Task ID
69        task_id: String,
70    },
71    /// Remove a task from this list
72    RemoveTask {
73        /// List ID
74        list_id: String,
75        /// Task ID
76        task_id: String,
77    },
78}
79
80pub async fn execute(command: ListCommands, cli: &Cli) -> Result<(), CliError> {
81    let token = resolve_token(cli)?;
82    let client = ClickUpClient::new(&token, cli.timeout)?;
83    let output = OutputConfig::from_cli(&cli.output, &cli.fields, cli.no_header, cli.quiet);
84    let default_fields = &["id", "name", "task_count", "status", "due_date"];
85
86    match command {
87        ListCommands::List {
88            folder,
89            space,
90            archived,
91        } => {
92            let path = match (&folder, &space) {
93                (Some(f), _) => format!("/v2/folder/{}/list?archived={}", f, archived),
94                (_, Some(s)) => format!("/v2/space/{}/list?archived={}", s, archived),
95                _ => {
96                    return Err(CliError::ClientError {
97                        message: "Provide --folder or --space".into(),
98                        status: 0,
99                    });
100                }
101            };
102            let resp = client.get(&path).await?;
103            let lists = resp
104                .get("lists")
105                .and_then(|l| l.as_array())
106                .cloned()
107                .unwrap_or_default();
108            output.print_items(&lists, default_fields, "id");
109            Ok(())
110        }
111        ListCommands::Get { id } => {
112            let resp = client.get(&format!("/v2/list/{}", id)).await?;
113            output.print_single(&resp, default_fields, "id");
114            Ok(())
115        }
116        ListCommands::Create {
117            folder,
118            space,
119            name,
120            content,
121            priority,
122            due_date,
123        } => {
124            let path = match (&folder, &space) {
125                (Some(f), _) => format!("/v2/folder/{}/list", f),
126                (_, Some(s)) => format!("/v2/space/{}/list", s),
127                _ => {
128                    return Err(CliError::ClientError {
129                        message: "Provide --folder or --space".into(),
130                        status: 0,
131                    });
132                }
133            };
134            let mut body = serde_json::json!({ "name": name });
135            if let Some(c) = content {
136                body["content"] = serde_json::Value::String(c);
137            }
138            if let Some(p) = priority {
139                body["priority"] = serde_json::json!(p);
140            }
141            if let Some(d) = due_date {
142                body["due_date"] = serde_json::Value::String(date_to_ms(&d)?);
143            }
144            let resp = client.post(&path, &body).await?;
145            output.print_single(&resp, default_fields, "id");
146            Ok(())
147        }
148        ListCommands::Update { id, name, content } => {
149            let mut body = serde_json::Map::new();
150            if let Some(n) = name {
151                body.insert("name".into(), serde_json::Value::String(n));
152            }
153            if let Some(c) = content {
154                body.insert("content".into(), serde_json::Value::String(c));
155            }
156            let resp = client
157                .put(
158                    &format!("/v2/list/{}", id),
159                    &serde_json::Value::Object(body),
160                )
161                .await?;
162            output.print_single(&resp, default_fields, "id");
163            Ok(())
164        }
165        ListCommands::Delete { id } => {
166            client.delete(&format!("/v2/list/{}", id)).await?;
167            output.print_message(&format!("List {} deleted", id));
168            Ok(())
169        }
170        ListCommands::AddTask { list_id, task_id } => {
171            client
172                .post(
173                    &format!("/v2/list/{}/task/{}", list_id, task_id),
174                    &serde_json::json!({}),
175                )
176                .await?;
177            output.print_message(&format!("Task {} added to list {}", task_id, list_id));
178            Ok(())
179        }
180        ListCommands::RemoveTask { list_id, task_id } => {
181            client
182                .delete(&format!("/v2/list/{}/task/{}", list_id, task_id))
183                .await?;
184            output.print_message(&format!("Task {} removed from list {}", task_id, list_id));
185            Ok(())
186        }
187    }
188}
189
190fn date_to_ms(date_str: &str) -> Result<String, CliError> {
191    let naive = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d").map_err(|_| {
192        CliError::ClientError {
193            message: format!("Invalid date '{}'. Use YYYY-MM-DD format.", date_str),
194            status: 0,
195        }
196    })?;
197    let dt = naive.and_hms_opt(0, 0, 0).unwrap().and_utc();
198    Ok((dt.timestamp_millis()).to_string())
199}