Skip to main content

clickup_cli/commands/
goal.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 GoalCommands {
11    /// List goals in workspace
12    List {
13        /// Include completed goals
14        #[arg(long)]
15        include_completed: bool,
16    },
17    /// Get a goal by ID
18    Get {
19        /// Goal ID
20        id: String,
21    },
22    /// Create a goal
23    Create {
24        /// Goal name
25        #[arg(long)]
26        name: String,
27        /// Due date (Unix ms)
28        #[arg(long)]
29        due_date: String,
30        /// Description
31        #[arg(long)]
32        description: String,
33        /// Color hex (e.g. #32a852)
34        #[arg(long)]
35        color: Option<String>,
36        /// Owner user ID
37        #[arg(long)]
38        owner: Option<String>,
39    },
40    /// Update a goal
41    Update {
42        /// Goal ID
43        id: String,
44        /// New name
45        #[arg(long)]
46        name: Option<String>,
47        /// New due date (Unix ms)
48        #[arg(long)]
49        due_date: Option<String>,
50        /// New description
51        #[arg(long)]
52        description: Option<String>,
53        /// New color hex
54        #[arg(long)]
55        color: Option<String>,
56        /// Add owner by user ID
57        #[arg(long)]
58        add_owner: Option<String>,
59        /// Remove owner by user ID
60        #[arg(long)]
61        rem_owner: Option<String>,
62    },
63    /// Delete a goal
64    Delete {
65        /// Goal ID
66        id: String,
67    },
68    /// Add a key result to a goal
69    AddKr {
70        /// Goal ID
71        goal_id: String,
72        /// Key result name
73        #[arg(long)]
74        name: String,
75        /// Type: number, currency, boolean, percentage, automatic
76        #[arg(long, name = "type")]
77        kr_type: String,
78        /// Starting step value
79        #[arg(long)]
80        steps_start: i64,
81        /// Target step value
82        #[arg(long)]
83        steps_end: i64,
84        /// Unit label (e.g. "tasks")
85        #[arg(long)]
86        unit: Option<String>,
87        /// Owner user ID
88        #[arg(long)]
89        owner: Option<String>,
90    },
91    /// Update a key result
92    UpdateKr {
93        /// Key result ID
94        kr_id: String,
95        /// Current step value
96        #[arg(long)]
97        steps_current: i64,
98        /// Note
99        #[arg(long)]
100        note: Option<String>,
101    },
102    /// Delete a key result
103    DeleteKr {
104        /// Key result ID
105        kr_id: String,
106    },
107}
108
109const GOAL_FIELDS: &[&str] = &["id", "name", "percent_completed", "due_date"];
110
111pub async fn execute(command: GoalCommands, cli: &Cli) -> Result<(), CliError> {
112    let token = resolve_token(cli)?;
113    let client = ClickUpClient::new(&token, cli.timeout)?;
114    let output = OutputConfig::from_cli(&cli.output, &cli.fields, cli.no_header, cli.quiet);
115
116    match command {
117        GoalCommands::List { include_completed } => {
118            let ws_id = resolve_workspace(cli)?;
119            let resp = client
120                .get(&format!(
121                    "/v2/team/{}/goal?include_completed={}",
122                    ws_id, include_completed
123                ))
124                .await?;
125            let goals = resp
126                .get("goals")
127                .and_then(|g| g.as_array())
128                .cloned()
129                .unwrap_or_default();
130            output.print_items(&goals, GOAL_FIELDS, "id");
131            Ok(())
132        }
133        GoalCommands::Get { id } => {
134            let resp = client.get(&format!("/v2/goal/{}", id)).await?;
135            let goal = resp.get("goal").cloned().unwrap_or(resp);
136            output.print_single(&goal, GOAL_FIELDS, "id");
137            Ok(())
138        }
139        GoalCommands::Create {
140            name,
141            due_date,
142            description,
143            color,
144            owner,
145        } => {
146            let ws_id = resolve_workspace(cli)?;
147            let mut body = serde_json::json!({
148                "name": name,
149                "due_date": due_date,
150                "description": description,
151            });
152            if let Some(c) = color {
153                body["color"] = serde_json::Value::String(c);
154            }
155            if let Some(o) = owner {
156                body["owners"] = serde_json::json!([o]);
157            }
158            let resp = client
159                .post(&format!("/v2/team/{}/goal", ws_id), &body)
160                .await?;
161            let goal = resp.get("goal").cloned().unwrap_or(resp);
162            output.print_single(&goal, GOAL_FIELDS, "id");
163            Ok(())
164        }
165        GoalCommands::Update {
166            id,
167            name,
168            due_date,
169            description,
170            color,
171            add_owner,
172            rem_owner,
173        } => {
174            let mut body = serde_json::Map::new();
175            if let Some(n) = name {
176                body.insert("name".into(), serde_json::Value::String(n));
177            }
178            if let Some(d) = due_date {
179                body.insert("due_date".into(), serde_json::Value::String(d));
180            }
181            if let Some(d) = description {
182                body.insert("description".into(), serde_json::Value::String(d));
183            }
184            if let Some(c) = color {
185                body.insert("color".into(), serde_json::Value::String(c));
186            }
187            if let Some(o) = add_owner {
188                body.insert("add_owners".into(), serde_json::json!([o]));
189            }
190            if let Some(o) = rem_owner {
191                body.insert("rem_owners".into(), serde_json::json!([o]));
192            }
193            let resp = client
194                .put(
195                    &format!("/v2/goal/{}", id),
196                    &serde_json::Value::Object(body),
197                )
198                .await?;
199            let goal = resp.get("goal").cloned().unwrap_or(resp);
200            output.print_single(&goal, GOAL_FIELDS, "id");
201            Ok(())
202        }
203        GoalCommands::Delete { id } => {
204            client.delete(&format!("/v2/goal/{}", id)).await?;
205            output.print_message(&format!("Goal {} deleted", id));
206            Ok(())
207        }
208        GoalCommands::AddKr {
209            goal_id,
210            name,
211            kr_type,
212            steps_start,
213            steps_end,
214            unit,
215            owner,
216        } => {
217            let mut body = serde_json::json!({
218                "name": name,
219                "type": kr_type,
220                "steps_start": steps_start,
221                "steps_end": steps_end,
222            });
223            if let Some(u) = unit {
224                body["unit"] = serde_json::Value::String(u);
225            }
226            if let Some(o) = owner {
227                body["owners"] = serde_json::json!([o]);
228            }
229            let resp = client
230                .post(&format!("/v2/goal/{}/key_result", goal_id), &body)
231                .await?;
232            let kr = resp.get("key_result").cloned().unwrap_or(resp);
233            output.print_single(
234                &kr,
235                &["id", "name", "type", "steps_start", "steps_end"],
236                "id",
237            );
238            Ok(())
239        }
240        GoalCommands::UpdateKr {
241            kr_id,
242            steps_current,
243            note,
244        } => {
245            let mut body = serde_json::json!({ "steps_current": steps_current });
246            if let Some(n) = note {
247                body["note"] = serde_json::Value::String(n);
248            }
249            let resp = client
250                .put(&format!("/v2/key_result/{}", kr_id), &body)
251                .await?;
252            let kr = resp.get("key_result").cloned().unwrap_or(resp);
253            output.print_single(&kr, &["id", "name", "steps_current", "steps_end"], "id");
254            Ok(())
255        }
256        GoalCommands::DeleteKr { kr_id } => {
257            client.delete(&format!("/v2/key_result/{}", kr_id)).await?;
258            output.print_message(&format!("Key result {} deleted", kr_id));
259            Ok(())
260        }
261    }
262}