Skip to main content

clickup_cli/commands/
list.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 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 { folder, space, archived } => {
88            let path = match (&folder, &space) {
89                (Some(f), _) => format!("/v2/folder/{}/list?archived={}", f, archived),
90                (_, Some(s)) => format!("/v2/space/{}/list?archived={}", s, archived),
91                _ => {
92                    return Err(CliError::ClientError {
93                        message: "Provide --folder or --space".into(),
94                        status: 0,
95                    });
96                }
97            };
98            let resp = client.get(&path).await?;
99            let lists = resp
100                .get("lists")
101                .and_then(|l| l.as_array())
102                .cloned()
103                .unwrap_or_default();
104            output.print_items(&lists, default_fields, "id");
105            Ok(())
106        }
107        ListCommands::Get { id } => {
108            let resp = client.get(&format!("/v2/list/{}", id)).await?;
109            output.print_single(&resp, default_fields, "id");
110            Ok(())
111        }
112        ListCommands::Create {
113            folder,
114            space,
115            name,
116            content,
117            priority,
118            due_date,
119        } => {
120            let path = match (&folder, &space) {
121                (Some(f), _) => format!("/v2/folder/{}/list", f),
122                (_, Some(s)) => format!("/v2/space/{}/list", s),
123                _ => {
124                    return Err(CliError::ClientError {
125                        message: "Provide --folder or --space".into(),
126                        status: 0,
127                    });
128                }
129            };
130            let mut body = serde_json::json!({ "name": name });
131            if let Some(c) = content {
132                body["content"] = serde_json::Value::String(c);
133            }
134            if let Some(p) = priority {
135                body["priority"] = serde_json::json!(p);
136            }
137            if let Some(d) = due_date {
138                body["due_date"] = serde_json::Value::String(date_to_ms(&d)?);
139            }
140            let resp = client.post(&path, &body).await?;
141            output.print_single(&resp, default_fields, "id");
142            Ok(())
143        }
144        ListCommands::Update { id, name, content } => {
145            let mut body = serde_json::Map::new();
146            if let Some(n) = name {
147                body.insert("name".into(), serde_json::Value::String(n));
148            }
149            if let Some(c) = content {
150                body.insert("content".into(), serde_json::Value::String(c));
151            }
152            let resp = client
153                .put(&format!("/v2/list/{}", id), &serde_json::Value::Object(body))
154                .await?;
155            output.print_single(&resp, default_fields, "id");
156            Ok(())
157        }
158        ListCommands::Delete { id } => {
159            client.delete(&format!("/v2/list/{}", id)).await?;
160            output.print_message(&format!("List {} deleted", id));
161            Ok(())
162        }
163        ListCommands::AddTask { list_id, task_id } => {
164            client
165                .post(
166                    &format!("/v2/list/{}/task/{}", list_id, task_id),
167                    &serde_json::json!({}),
168                )
169                .await?;
170            output.print_message(&format!("Task {} added to list {}", task_id, list_id));
171            Ok(())
172        }
173        ListCommands::RemoveTask { list_id, task_id } => {
174            client
175                .delete(&format!("/v2/list/{}/task/{}", list_id, task_id))
176                .await?;
177            output.print_message(&format!("Task {} removed from list {}", task_id, list_id));
178            Ok(())
179        }
180    }
181}
182
183fn date_to_ms(date_str: &str) -> Result<String, CliError> {
184    let naive = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
185        .map_err(|_| CliError::ClientError {
186            message: format!("Invalid date '{}'. Use YYYY-MM-DD format.", date_str),
187            status: 0,
188        })?;
189    let dt = naive
190        .and_hms_opt(0, 0, 0)
191        .unwrap()
192        .and_utc();
193    Ok((dt.timestamp_millis()).to_string())
194}