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 docs = crate::commands::pagination::walk_cursor(
102 cli,
103 &client,
104 &["data", "docs"],
105 |cursor| {
106 let mut params: Vec<String> = Vec::new();
107 if let Some(c) = &creator {
108 params.push(format!("creator={}", c));
109 }
110 if archived {
111 params.push("archived=true".to_string());
112 }
113 if let Some(c) = cursor {
114 params.push(format!("cursor={}", c));
115 }
116 if params.is_empty() {
117 base.clone()
118 } else {
119 format!("{}?{}", base, params.join("&"))
120 }
121 },
122 )
123 .await?;
124 output.print_items(&docs, DOC_FIELDS, "id");
125 Ok(())
126 }
127 DocCommands::Create {
128 name,
129 visibility,
130 parent_type,
131 parent_id,
132 } => {
133 let mut body = serde_json::json!({ "name": name });
134 if let Some(v) = visibility {
135 body["visibility"] = serde_json::Value::String(v);
136 }
137 if parent_type.is_some() || parent_id.is_some() {
138 let mut parent = serde_json::Map::new();
139 if let Some(pt) = parent_type {
140 let type_id = match pt.to_uppercase().as_str() {
141 "SPACE" | "4" => 4,
142 "FOLDER" | "5" => 5,
143 "LIST" | "6" => 6,
144 "EVERYTHING" | "7" => 7,
145 "WORKSPACE" | "12" => 12,
146 other => {
147 return Err(CliError::ClientError {
148 message: format!(
149 "Invalid --parent-type '{}'. Valid values: SPACE, FOLDER, LIST, EVERYTHING, WORKSPACE",
150 other
151 ),
152 status: 0,
153 });
154 }
155 };
156 parent.insert("type".into(), serde_json::json!(type_id));
157 }
158 if let Some(pi) = parent_id {
159 parent.insert("id".into(), serde_json::Value::String(pi));
160 }
161 body["parent"] = serde_json::Value::Object(parent);
162 }
163 let resp = client.post(&base, &body).await?;
164 output.print_single(&resp, DOC_FIELDS, "id");
165 Ok(())
166 }
167 DocCommands::Get { id } => {
168 let resp = client.get(&format!("{}/{}", base, id)).await?;
169 output.print_single(&resp, DOC_FIELDS, "id");
170 Ok(())
171 }
172 DocCommands::Pages {
173 id,
174 content,
175 max_depth,
176 } => {
177 if content || max_depth.is_some() {
178 let mut params = Vec::new();
179 if content {
180 params.push("content=true".to_string());
181 }
182 if let Some(depth) = max_depth {
183 params.push(format!("max_page_depth={}", depth));
184 }
185 let query = if params.is_empty() {
186 String::new()
187 } else {
188 format!("?{}", params.join("&"))
189 };
190 let resp = client
191 .get(&format!("{}/{}/pages{}", base, id, query))
192 .await?;
193 let mut pages = resp
194 .get("pages")
195 .and_then(|v| v.as_array())
196 .cloned()
197 .unwrap_or_else(|| resp.as_array().cloned().unwrap_or_default());
198 if let Some(limit) = cli.limit {
199 pages.truncate(limit);
200 }
201 output.print_items(&pages, PAGE_FIELDS, "id");
202 } else {
203 let resp = client.get(&format!("{}/{}/page_listing", base, id)).await?;
204 let mut pages = resp
205 .get("pages")
206 .and_then(|v| v.as_array())
207 .cloned()
208 .unwrap_or_else(|| resp.as_array().cloned().unwrap_or_default());
209 if let Some(limit) = cli.limit {
210 pages.truncate(limit);
211 }
212 output.print_items(&pages, PAGE_FIELDS, "id");
213 }
214 Ok(())
215 }
216 DocCommands::AddPage {
217 doc_id,
218 name,
219 parent_page,
220 content,
221 } => {
222 let mut body = serde_json::json!({ "name": name });
223 if let Some(pp) = parent_page {
224 body["parent_page_id"] = serde_json::Value::String(pp);
225 }
226 if let Some(c) = content {
227 body["content"] = serde_json::Value::String(c);
228 }
229 let resp = client
230 .post(&format!("{}/{}/pages", base, doc_id), &body)
231 .await?;
232 output.print_single(&resp, PAGE_FIELDS, "id");
233 Ok(())
234 }
235 DocCommands::Page { doc_id, page_id } => {
236 let resp = client
237 .get(&format!("{}/{}/pages/{}", base, doc_id, page_id))
238 .await?;
239 output.print_single(&resp, PAGE_FIELDS, "id");
240 Ok(())
241 }
242 DocCommands::EditPage {
243 doc_id,
244 page_id,
245 content,
246 mode,
247 } => {
248 let body = serde_json::json!({
249 "content": content,
250 "content_edit_mode": mode,
251 });
252 let resp = client
253 .put(&format!("{}/{}/pages/{}", base, doc_id, page_id), &body)
254 .await?;
255 output.print_single(&resp, PAGE_FIELDS, "id");
256 Ok(())
257 }
258 }
259}