Skip to main content

clickup_cli/commands/
view.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 ViewCommands {
11    /// List views (use one scope flag: --workspace-level, --space, --folder, --list)
12    List {
13        /// List workspace-level views
14        #[arg(long = "workspace-level", conflicts_with_all = &["space", "folder", "list"])]
15        workspace_level: bool,
16        /// Space ID
17        #[arg(long, conflicts_with_all = &["workspace", "folder", "list"])]
18        space: Option<String>,
19        /// Folder ID
20        #[arg(long, conflicts_with_all = &["workspace", "space", "list"])]
21        folder: Option<String>,
22        /// List ID
23        #[arg(long, conflicts_with_all = &["workspace", "space", "folder"])]
24        list: Option<String>,
25    },
26    /// Get a view by ID
27    Get {
28        /// View ID
29        id: String,
30    },
31    /// Create a view (use one scope flag: --workspace-level, --space, --folder, --list)
32    Create {
33        /// View name
34        #[arg(long)]
35        name: String,
36        /// View type (list, board, calendar, gantt, activity, map, workload, table, doc, chat, embed)
37        #[arg(long, name = "type")]
38        view_type: String,
39        /// Create workspace-level view
40        #[arg(long = "workspace-level", conflicts_with_all = &["space", "folder", "list"])]
41        workspace_level: bool,
42        /// Space ID
43        #[arg(long, conflicts_with_all = &["workspace", "folder", "list"])]
44        space: Option<String>,
45        /// Folder ID
46        #[arg(long, conflicts_with_all = &["workspace", "space", "list"])]
47        folder: Option<String>,
48        /// List ID
49        #[arg(long, conflicts_with_all = &["workspace", "space", "folder"])]
50        list: Option<String>,
51    },
52    /// Update a view
53    Update {
54        /// View ID
55        id: String,
56        /// New name
57        #[arg(long)]
58        name: Option<String>,
59    },
60    /// Delete a view
61    Delete {
62        /// View ID
63        id: String,
64    },
65    /// List tasks in a view
66    Tasks {
67        /// View ID
68        id: String,
69        /// Page number (0-indexed)
70        #[arg(long, default_value = "0")]
71        page: u32,
72    },
73}
74
75const VIEW_FIELDS: &[&str] = &["id", "name", "type"];
76
77pub async fn execute(command: ViewCommands, cli: &Cli) -> Result<(), CliError> {
78    let token = resolve_token(cli)?;
79    let client = ClickUpClient::new(&token, cli.timeout)?;
80    let output = OutputConfig::from_cli(&cli.output, &cli.fields, cli.no_header, cli.quiet);
81
82    match command {
83        ViewCommands::List {
84            workspace_level: workspace,
85            space,
86            folder,
87            list,
88        } => {
89            let url = if workspace {
90                let ws_id = resolve_workspace(cli)?;
91                format!("/v2/team/{}/view", ws_id)
92            } else if let Some(id) = space {
93                format!("/v2/space/{}/view", id)
94            } else if let Some(id) = folder {
95                format!("/v2/folder/{}/view", id)
96            } else if let Some(id) = list {
97                format!("/v2/list/{}/view", id)
98            } else {
99                return Err(CliError::ClientError {
100                    message: "Specify a scope: --workspace, --space ID, --folder ID, or --list ID"
101                        .into(),
102                    status: 0,
103                });
104            };
105            let resp = client.get(&url).await?;
106            let views = resp
107                .get("views")
108                .and_then(|v| v.as_array())
109                .cloned()
110                .unwrap_or_default();
111            output.print_items(&views, VIEW_FIELDS, "id");
112            Ok(())
113        }
114        ViewCommands::Get { id } => {
115            let resp = client.get(&format!("/v2/view/{}", id)).await?;
116            let view = resp.get("view").cloned().unwrap_or(resp);
117            output.print_single(&view, VIEW_FIELDS, "id");
118            Ok(())
119        }
120        ViewCommands::Create {
121            name,
122            view_type,
123            workspace_level: workspace,
124            space,
125            folder,
126            list,
127        } => {
128            let url = if workspace {
129                let ws_id = resolve_workspace(cli)?;
130                format!("/v2/team/{}/view", ws_id)
131            } else if let Some(id) = space {
132                format!("/v2/space/{}/view", id)
133            } else if let Some(id) = folder {
134                format!("/v2/folder/{}/view", id)
135            } else if let Some(id) = list {
136                format!("/v2/list/{}/view", id)
137            } else {
138                return Err(CliError::ClientError {
139                    message: "Specify a scope: --workspace, --space ID, --folder ID, or --list ID"
140                        .into(),
141                    status: 0,
142                });
143            };
144            let body = default_view_body(&name, &view_type);
145            let resp = client.post(&url, &body).await?;
146            let view = resp.get("view").cloned().unwrap_or(resp);
147            output.print_single(&view, VIEW_FIELDS, "id");
148            Ok(())
149        }
150        ViewCommands::Update { id, name } => {
151            let mut body = serde_json::Map::new();
152            if let Some(n) = name {
153                body.insert("name".into(), serde_json::Value::String(n));
154            }
155            let resp = client
156                .put(
157                    &format!("/v2/view/{}", id),
158                    &serde_json::Value::Object(body),
159                )
160                .await?;
161            let view = resp.get("view").cloned().unwrap_or(resp);
162            output.print_single(&view, VIEW_FIELDS, "id");
163            Ok(())
164        }
165        ViewCommands::Delete { id } => {
166            client.delete(&format!("/v2/view/{}", id)).await?;
167            output.print_message(&format!("View {} deleted", id));
168            Ok(())
169        }
170        ViewCommands::Tasks { id, page } => {
171            let resp = client
172                .get(&format!("/v2/view/{}/task?page={}", id, page))
173                .await?;
174            let tasks = resp
175                .get("tasks")
176                .and_then(|t| t.as_array())
177                .cloned()
178                .unwrap_or_default();
179            output.print_items(&tasks, &["id", "name", "status", "assignees"], "id");
180            Ok(())
181        }
182    }
183}
184
185/// Build the minimal create-view body that ClickUp's v2 spec accepts.
186///
187/// All of `grouping`, `divide`, `sorting`, `filters`, `columns`,
188/// `team_sidebar`, and `settings` are marked required by the OpenAPI spec.
189/// Sending only `{name, type}` 400s. We populate them with the documented
190/// neutral defaults so a basic `view create --name X --type list ...` call
191/// succeeds; the resulting view can be reshaped in the ClickUp UI afterwards.
192pub(crate) fn default_view_body(name: &str, view_type: &str) -> serde_json::Value {
193    serde_json::json!({
194        "name": name,
195        "type": view_type,
196        "grouping": {
197            "field": null,
198            "dir": 1,
199            "collapsed": [],
200            "ignore": false
201        },
202        "divide": {
203            "field": null,
204            "dir": null,
205            "collapsed": []
206        },
207        "sorting": {"fields": []},
208        "filters": {
209            "op": "AND",
210            "fields": [],
211            "search": "",
212            "show_closed": false
213        },
214        "columns": {"fields": []},
215        "team_sidebar": {
216            "assignees": [],
217            "assigned_comments": false,
218            "unassigned_tasks": false
219        },
220        "settings": {
221            "show_task_locations": false,
222            "show_subtasks": 3,
223            "show_subtask_parent_names": false,
224            "show_closed_subtasks": false,
225            "show_assignees": true,
226            "show_images": true,
227            "collapse_empty_columns": null,
228            "me_comments": true,
229            "me_subtasks": true,
230            "me_checklists": true
231        }
232    })
233}