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 {
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 { 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}