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. Honours global --all/--page/--limit for pagination.
66    Tasks {
67        /// View ID
68        id: String,
69    },
70}
71
72const VIEW_FIELDS: &[&str] = &["id", "name", "type"];
73
74pub async fn execute(command: ViewCommands, cli: &Cli) -> Result<(), CliError> {
75    let token = resolve_token(cli)?;
76    let client = ClickUpClient::new(&token, cli.timeout)?;
77    let output = OutputConfig::from_cli(&cli.output, &cli.fields, cli.no_header, cli.quiet);
78
79    match command {
80        ViewCommands::List {
81            workspace_level: workspace,
82            space,
83            folder,
84            list,
85        } => {
86            let url = if workspace {
87                let ws_id = resolve_workspace(cli)?;
88                format!("/v2/team/{}/view", ws_id)
89            } else if let Some(id) = space {
90                format!("/v2/space/{}/view", id)
91            } else if let Some(id) = folder {
92                format!("/v2/folder/{}/view", id)
93            } else if let Some(id) = list {
94                format!("/v2/list/{}/view", id)
95            } else {
96                return Err(CliError::ClientError {
97                    message: "Specify a scope: --workspace, --space ID, --folder ID, or --list ID"
98                        .into(),
99                    status: 0,
100                });
101            };
102            let resp = client.get(&url).await?;
103            let views = resp
104                .get("views")
105                .and_then(|v| v.as_array())
106                .cloned()
107                .unwrap_or_default();
108            output.print_items(&views, VIEW_FIELDS, "id");
109            Ok(())
110        }
111        ViewCommands::Get { id } => {
112            let resp = client.get(&format!("/v2/view/{}", id)).await?;
113            let view = resp.get("view").cloned().unwrap_or(resp);
114            output.print_single(&view, VIEW_FIELDS, "id");
115            Ok(())
116        }
117        ViewCommands::Create {
118            name,
119            view_type,
120            workspace_level: workspace,
121            space,
122            folder,
123            list,
124        } => {
125            let url = if workspace {
126                let ws_id = resolve_workspace(cli)?;
127                format!("/v2/team/{}/view", ws_id)
128            } else if let Some(id) = space {
129                format!("/v2/space/{}/view", id)
130            } else if let Some(id) = folder {
131                format!("/v2/folder/{}/view", id)
132            } else if let Some(id) = list {
133                format!("/v2/list/{}/view", id)
134            } else {
135                return Err(CliError::ClientError {
136                    message: "Specify a scope: --workspace, --space ID, --folder ID, or --list ID"
137                        .into(),
138                    status: 0,
139                });
140            };
141            let body = default_view_body(&name, &view_type);
142            let resp = client.post(&url, &body).await?;
143            let view = resp.get("view").cloned().unwrap_or(resp);
144            output.print_single(&view, VIEW_FIELDS, "id");
145            Ok(())
146        }
147        ViewCommands::Update { id, name } => {
148            let mut body = serde_json::Map::new();
149            if let Some(n) = name {
150                body.insert("name".into(), serde_json::Value::String(n));
151            }
152            let resp = client
153                .put(
154                    &format!("/v2/view/{}", id),
155                    &serde_json::Value::Object(body),
156                )
157                .await?;
158            let view = resp.get("view").cloned().unwrap_or(resp);
159            output.print_single(&view, VIEW_FIELDS, "id");
160            Ok(())
161        }
162        ViewCommands::Delete { id } => {
163            client.delete(&format!("/v2/view/{}", id)).await?;
164            output.print_message(&format!("View {} deleted", id));
165            Ok(())
166        }
167        ViewCommands::Tasks { id } => {
168            let tasks = crate::commands::pagination::walk_page(cli, &client, "tasks", |p| {
169                format!("/v2/view/{}/task?page={}", id, p)
170            })
171            .await?;
172            output.print_items(&tasks, &["id", "name", "status", "assignees"], "id");
173            Ok(())
174        }
175    }
176}
177
178/// Build the minimal create-view body that ClickUp's v2 spec accepts.
179///
180/// All of `grouping`, `divide`, `sorting`, `filters`, `columns`,
181/// `team_sidebar`, and `settings` are marked required by the OpenAPI spec.
182/// Sending only `{name, type}` 400s. We populate them with the documented
183/// neutral defaults so a basic `view create --name X --type list ...` call
184/// succeeds; the resulting view can be reshaped in the ClickUp UI afterwards.
185pub(crate) fn default_view_body(name: &str, view_type: &str) -> serde_json::Value {
186    serde_json::json!({
187        "name": name,
188        "type": view_type,
189        "grouping": {
190            "field": null,
191            "dir": 1,
192            "collapsed": [],
193            "ignore": false
194        },
195        "divide": {
196            "field": null,
197            "dir": null,
198            "collapsed": []
199        },
200        "sorting": {"fields": []},
201        "filters": {
202            "op": "AND",
203            "fields": [],
204            "search": "",
205            "show_closed": false
206        },
207        "columns": {"fields": []},
208        "team_sidebar": {
209            "assignees": [],
210            "assigned_comments": false,
211            "unassigned_tasks": false
212        },
213        "settings": {
214            "show_task_locations": false,
215            "show_subtasks": 3,
216            "show_subtask_parent_names": false,
217            "show_closed_subtasks": false,
218            "show_assignees": true,
219            "show_images": true,
220            "collapse_empty_columns": null,
221            "me_comments": true,
222            "me_subtasks": true,
223            "me_checklists": true
224        }
225    })
226}