Skip to main content

clickup_cli/commands/
doc.rs

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 docs in the workspace
12    List {
13        /// Filter by creator user ID
14        #[arg(long)]
15        creator: Option<String>,
16        /// Include archived docs
17        #[arg(long)]
18        archived: bool,
19    },
20    /// Create a doc
21    Create {
22        /// Doc name
23        #[arg(long)]
24        name: String,
25        /// Visibility: PUBLIC, PRIVATE, or PERSONAL
26        #[arg(long)]
27        visibility: Option<String>,
28        /// Parent type: SPACE, FOLDER, LIST, EVERYTHING, or WORKSPACE
29        #[arg(long)]
30        parent_type: Option<String>,
31        /// Parent ID
32        #[arg(long)]
33        parent_id: Option<String>,
34    },
35    /// Get a doc by ID
36    Get {
37        /// Doc ID
38        id: String,
39    },
40    /// List pages in a doc
41    Pages {
42        /// Doc ID
43        id: String,
44        /// Include page content
45        #[arg(long)]
46        content: bool,
47        /// Maximum page depth
48        #[arg(long)]
49        max_depth: Option<u32>,
50    },
51    /// Add a page to a doc
52    #[command(name = "add-page")]
53    AddPage {
54        /// Doc ID
55        doc_id: String,
56        /// Page name
57        #[arg(long)]
58        name: String,
59        /// Parent page ID
60        #[arg(long)]
61        parent_page: Option<String>,
62        /// Page content
63        #[arg(long)]
64        content: Option<String>,
65    },
66    /// Get a specific page from a doc
67    Page {
68        /// Doc ID
69        doc_id: String,
70        /// Page ID
71        page_id: String,
72    },
73    /// Edit a doc page
74    #[command(name = "edit-page")]
75    EditPage {
76        /// Doc ID
77        doc_id: String,
78        /// Page ID
79        page_id: String,
80        /// Page content
81        #[arg(long)]
82        content: String,
83        /// Content edit mode: replace, append, or prepend
84        #[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}