1use crate::client::ClickUpClient;
2use crate::commands::auth::resolve_token;
3use crate::commands::workspace::resolve_workspace;
4use crate::error::CliError;
5use crate::output::OutputConfig;
6use crate::Cli;
7use clap::Subcommand;
8
9#[derive(Subcommand)]
10pub enum DocCommands {
11 List {
13 #[arg(long)]
15 creator: Option<String>,
16 #[arg(long)]
18 archived: bool,
19 },
20 Create {
22 #[arg(long)]
24 name: String,
25 #[arg(long)]
27 visibility: Option<String>,
28 #[arg(long)]
30 parent_type: Option<String>,
31 #[arg(long)]
33 parent_id: Option<String>,
34 },
35 Get {
37 id: String,
39 },
40 Pages {
42 id: String,
44 #[arg(long)]
46 content: bool,
47 #[arg(long)]
49 max_depth: Option<u32>,
50 },
51 #[command(name = "add-page")]
53 AddPage {
54 doc_id: String,
56 #[arg(long)]
58 name: String,
59 #[arg(long)]
61 parent_page: Option<String>,
62 #[arg(long)]
64 content: Option<String>,
65 },
66 Page {
68 doc_id: String,
70 page_id: String,
72 },
73 #[command(name = "edit-page")]
75 EditPage {
76 doc_id: String,
78 page_id: String,
80 #[arg(long)]
82 content: String,
83 #[arg(long, default_value = "replace")]
85 mode: String,
86 },
87}
88
89const DOC_FIELDS: &[&str] = &["id", "name", "visibility", "date_created"];
90const PAGE_FIELDS: &[&str] = &["id", "name", "date_created", "date_updated"];
91
92pub async fn execute(command: DocCommands, cli: &Cli) -> Result<(), CliError> {
93 let token = resolve_token(cli)?;
94 let client = ClickUpClient::new(&token, cli.timeout)?;
95 let ws_id = resolve_workspace(cli)?;
96 let output = OutputConfig::from_cli(&cli.output, &cli.fields, cli.no_header, cli.quiet);
97 let base = format!("/v3/workspaces/{}/docs", ws_id);
98
99 match command {
100 DocCommands::List { creator, archived } => {
101 let mut params = Vec::new();
102 if let Some(c) = &creator {
103 params.push(format!("creator={}", c));
104 }
105 if archived {
106 params.push("archived=true".to_string());
107 }
108 let query = if params.is_empty() {
109 String::new()
110 } else {
111 format!("?{}", params.join("&"))
112 };
113 let resp = client.get(&format!("{}{}", base, query)).await?;
114 let mut docs = resp
115 .get("docs")
116 .and_then(|v| v.as_array())
117 .cloned()
118 .unwrap_or_default();
119 if let Some(limit) = cli.limit {
120 docs.truncate(limit);
121 }
122 output.print_items(&docs, DOC_FIELDS, "id");
123 Ok(())
124 }
125 DocCommands::Create {
126 name,
127 visibility,
128 parent_type,
129 parent_id,
130 } => {
131 let mut body = serde_json::json!({ "name": name });
132 if let Some(v) = visibility {
133 body["visibility"] = serde_json::Value::String(v);
134 }
135 if parent_type.is_some() || parent_id.is_some() {
136 let mut parent = serde_json::Map::new();
137 if let Some(pt) = parent_type {
138 let type_id = match pt.to_uppercase().as_str() {
139 "SPACE" | "4" => 4,
140 "FOLDER" | "5" => 5,
141 "LIST" | "6" => 6,
142 "EVERYTHING" | "7" => 7,
143 "WORKSPACE" | "12" => 12,
144 other => {
145 return Err(CliError::ClientError {
146 message: format!(
147 "Invalid --parent-type '{}'. Valid values: SPACE, FOLDER, LIST, EVERYTHING, WORKSPACE",
148 other
149 ),
150 status: 0,
151 });
152 }
153 };
154 parent.insert("type".into(), serde_json::json!(type_id));
155 }
156 if let Some(pi) = parent_id {
157 parent.insert("id".into(), serde_json::Value::String(pi));
158 }
159 body["parent"] = serde_json::Value::Object(parent);
160 }
161 let resp = client.post(&base, &body).await?;
162 output.print_single(&resp, DOC_FIELDS, "id");
163 Ok(())
164 }
165 DocCommands::Get { id } => {
166 let resp = client.get(&format!("{}/{}", base, id)).await?;
167 output.print_single(&resp, DOC_FIELDS, "id");
168 Ok(())
169 }
170 DocCommands::Pages {
171 id,
172 content,
173 max_depth,
174 } => {
175 if content || max_depth.is_some() {
176 let mut params = Vec::new();
177 if content {
178 params.push("content=true".to_string());
179 }
180 if let Some(depth) = max_depth {
181 params.push(format!("max_page_depth={}", depth));
182 }
183 let query = if params.is_empty() {
184 String::new()
185 } else {
186 format!("?{}", params.join("&"))
187 };
188 let resp = client
189 .get(&format!("{}/{}/pages{}", base, id, query))
190 .await?;
191 let mut pages = resp
192 .get("pages")
193 .and_then(|v| v.as_array())
194 .cloned()
195 .unwrap_or_else(|| resp.as_array().cloned().unwrap_or_default());
196 if let Some(limit) = cli.limit {
197 pages.truncate(limit);
198 }
199 output.print_items(&pages, PAGE_FIELDS, "id");
200 } else {
201 let resp = client.get(&format!("{}/{}/page_listing", base, id)).await?;
202 let mut pages = resp
203 .get("pages")
204 .and_then(|v| v.as_array())
205 .cloned()
206 .unwrap_or_else(|| resp.as_array().cloned().unwrap_or_default());
207 if let Some(limit) = cli.limit {
208 pages.truncate(limit);
209 }
210 output.print_items(&pages, PAGE_FIELDS, "id");
211 }
212 Ok(())
213 }
214 DocCommands::AddPage {
215 doc_id,
216 name,
217 parent_page,
218 content,
219 } => {
220 let mut body = serde_json::json!({ "name": name });
221 if let Some(pp) = parent_page {
222 body["parent_page_id"] = serde_json::Value::String(pp);
223 }
224 if let Some(c) = content {
225 body["content"] = serde_json::Value::String(c);
226 }
227 let resp = client
228 .post(&format!("{}/{}/pages", base, doc_id), &body)
229 .await?;
230 output.print_single(&resp, PAGE_FIELDS, "id");
231 Ok(())
232 }
233 DocCommands::Page { doc_id, page_id } => {
234 let resp = client
235 .get(&format!("{}/{}/pages/{}", base, doc_id, page_id))
236 .await?;
237 output.print_single(&resp, PAGE_FIELDS, "id");
238 Ok(())
239 }
240 DocCommands::EditPage {
241 doc_id,
242 page_id,
243 content,
244 mode,
245 } => {
246 let body = serde_json::json!({
247 "content": content,
248 "content_edit_mode": mode,
249 });
250 let resp = client
251 .put(&format!("{}/{}/pages/{}", base, doc_id, page_id), &body)
252 .await?;
253 output.print_single(&resp, PAGE_FIELDS, "id");
254 Ok(())
255 }
256 }
257}