Skip to main content

clickup_cli/commands/
doc.rs

1use clap::Subcommand;
2use crate::client::ClickUpClient;
3use crate::commands::auth::resolve_token;
4use crate::commands::workspace::resolve_workspace;
5use crate::error::CliError;
6use crate::output::OutputConfig;
7use crate::Cli;
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
186                    .get(&format!("{}/{}/page_listing", base, id))
187                    .await?;
188                let mut pages = resp
189                    .get("pages")
190                    .and_then(|v| v.as_array())
191                    .cloned()
192                    .unwrap_or_else(|| resp.as_array().cloned().unwrap_or_default());
193                if let Some(limit) = cli.limit {
194                    pages.truncate(limit);
195                }
196                output.print_items(&pages, PAGE_FIELDS, "id");
197            }
198            Ok(())
199        }
200        DocCommands::AddPage {
201            doc_id,
202            name,
203            parent_page,
204            content,
205        } => {
206            let mut body = serde_json::json!({ "name": name });
207            if let Some(pp) = parent_page {
208                body["parent_page_id"] = serde_json::Value::String(pp);
209            }
210            if let Some(c) = content {
211                body["content"] = serde_json::Value::String(c);
212            }
213            let resp = client
214                .post(&format!("{}/{}/pages", base, doc_id), &body)
215                .await?;
216            output.print_single(&resp, PAGE_FIELDS, "id");
217            Ok(())
218        }
219        DocCommands::Page { doc_id, page_id } => {
220            let resp = client
221                .get(&format!("{}/{}/pages/{}", base, doc_id, page_id))
222                .await?;
223            output.print_single(&resp, PAGE_FIELDS, "id");
224            Ok(())
225        }
226        DocCommands::EditPage {
227            doc_id,
228            page_id,
229            content,
230            mode,
231        } => {
232            let body = serde_json::json!({
233                "content": content,
234                "content_edit_mode": mode,
235            });
236            let resp = client
237                .put(&format!("{}/{}/pages/{}", base, doc_id, page_id), &body)
238                .await?;
239            output.print_single(&resp, PAGE_FIELDS, "id");
240            Ok(())
241        }
242    }
243}