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, or LIST
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_id={}", 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                    parent.insert("type".into(), serde_json::Value::String(pt));
139                }
140                if let Some(pi) = parent_id {
141                    parent.insert("id".into(), serde_json::Value::String(pi));
142                }
143                body["parent"] = serde_json::Value::Object(parent);
144            }
145            let resp = client.post(&base, &body).await?;
146            output.print_single(&resp, DOC_FIELDS, "id");
147            Ok(())
148        }
149        DocCommands::Get { id } => {
150            let resp = client.get(&format!("{}/{}", base, id)).await?;
151            output.print_single(&resp, DOC_FIELDS, "id");
152            Ok(())
153        }
154        DocCommands::Pages {
155            id,
156            content,
157            max_depth,
158        } => {
159            if content || max_depth.is_some() {
160                let mut params = Vec::new();
161                if content {
162                    params.push("content=true".to_string());
163                }
164                if let Some(depth) = max_depth {
165                    params.push(format!("max_page_depth={}", depth));
166                }
167                let query = if params.is_empty() {
168                    String::new()
169                } else {
170                    format!("?{}", params.join("&"))
171                };
172                let resp = client
173                    .get(&format!("{}/{}/pages{}", base, id, query))
174                    .await?;
175                let mut pages = resp
176                    .get("pages")
177                    .and_then(|v| v.as_array())
178                    .cloned()
179                    .unwrap_or_else(|| resp.as_array().cloned().unwrap_or_default());
180                if let Some(limit) = cli.limit {
181                    pages.truncate(limit);
182                }
183                output.print_items(&pages, PAGE_FIELDS, "id");
184            } else {
185                let resp = client.get(&format!("{}/{}/page_listing", base, id)).await?;
186                let mut pages = resp
187                    .get("pages")
188                    .and_then(|v| v.as_array())
189                    .cloned()
190                    .unwrap_or_else(|| resp.as_array().cloned().unwrap_or_default());
191                if let Some(limit) = cli.limit {
192                    pages.truncate(limit);
193                }
194                output.print_items(&pages, PAGE_FIELDS, "id");
195            }
196            Ok(())
197        }
198        DocCommands::AddPage {
199            doc_id,
200            name,
201            parent_page,
202            content,
203        } => {
204            let mut body = serde_json::json!({ "name": name });
205            if let Some(pp) = parent_page {
206                body["parent_page_id"] = serde_json::Value::String(pp);
207            }
208            if let Some(c) = content {
209                body["content"] = serde_json::Value::String(c);
210            }
211            let resp = client
212                .post(&format!("{}/{}/pages", base, doc_id), &body)
213                .await?;
214            output.print_single(&resp, PAGE_FIELDS, "id");
215            Ok(())
216        }
217        DocCommands::Page { doc_id, page_id } => {
218            let resp = client
219                .get(&format!("{}/{}/pages/{}", base, doc_id, page_id))
220                .await?;
221            output.print_single(&resp, PAGE_FIELDS, "id");
222            Ok(())
223        }
224        DocCommands::EditPage {
225            doc_id,
226            page_id,
227            content,
228            mode,
229        } => {
230            let body = serde_json::json!({
231                "content": content,
232                "content_edit_mode": mode,
233            });
234            let resp = client
235                .put(&format!("{}/{}/pages/{}", base, doc_id, page_id), &body)
236                .await?;
237            output.print_single(&resp, PAGE_FIELDS, "id");
238            Ok(())
239        }
240    }
241}