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 {
12 #[arg(long)]
14 folder: Option<String>,
15 #[arg(long)]
17 space: Option<String>,
18 #[arg(long)]
20 archived: bool,
21 },
22 Get {
24 id: String,
26 },
27 Create {
29 #[arg(long)]
31 folder: Option<String>,
32 #[arg(long)]
34 space: Option<String>,
35 #[arg(long)]
37 name: String,
38 #[arg(long)]
40 content: Option<String>,
41 #[arg(long)]
43 priority: Option<u8>,
44 #[arg(long)]
46 due_date: Option<String>,
47 },
48 Update {
50 id: String,
52 #[arg(long)]
54 name: Option<String>,
55 #[arg(long)]
57 content: Option<String>,
58 },
59 Delete {
61 id: String,
63 },
64 AddTask {
66 list_id: String,
68 task_id: String,
70 },
71 RemoveTask {
73 list_id: String,
75 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}