Skip to main content

clickup_cli/commands/
group.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 GroupCommands {
11    /// List groups in the workspace
12    List,
13    /// Create a group
14    Create {
15        /// Group name
16        #[arg(long)]
17        name: String,
18        /// Member user IDs (repeat for multiple)
19        #[arg(long = "member")]
20        members: Vec<String>,
21    },
22    /// Update a group
23    Update {
24        /// Group ID
25        id: String,
26        /// New group name
27        #[arg(long)]
28        name: Option<String>,
29        /// User IDs to add (repeat for multiple)
30        #[arg(long = "add-member")]
31        add_members: Vec<String>,
32        /// User IDs to remove (repeat for multiple)
33        #[arg(long = "rem-member")]
34        rem_members: Vec<String>,
35    },
36    /// Delete a group
37    Delete {
38        /// Group ID
39        id: String,
40    },
41}
42
43const GROUP_FIELDS: &[&str] = &["id", "name", "members"];
44
45pub async fn execute(command: GroupCommands, cli: &Cli) -> Result<(), CliError> {
46    let token = resolve_token(cli)?;
47    let client = ClickUpClient::new(&token, cli.timeout)?;
48    let output = OutputConfig::from_cli(&cli.output, &cli.fields, cli.no_header, cli.quiet);
49
50    match command {
51        GroupCommands::List => {
52            // ClickUp's GET /v2/group requires team_id as a query param.
53            let team_id = resolve_workspace(cli)?;
54            let resp = client
55                .get(&format!("/v2/group?team_id={}", team_id))
56                .await?;
57            let groups = resp
58                .get("groups")
59                .and_then(|g| g.as_array())
60                .cloned()
61                .unwrap_or_default();
62            output.print_items(&groups, GROUP_FIELDS, "id");
63            Ok(())
64        }
65        GroupCommands::Create { name, members } => {
66            let team_id = resolve_workspace(cli)?;
67            // ClickUp's spec: body field is `members` (not `member_ids`) and the
68            // array contains integer user IDs (not strings). Parse and bail on
69            // anything that isn't a positive integer.
70            let member_ids: Result<Vec<i64>, _> = members
71                .iter()
72                .map(|m| m.parse::<i64>().map_err(|_| m.clone()))
73                .collect();
74            let member_ids = member_ids.map_err(|bad| CliError::ClientError {
75                message: format!("--member must be a numeric user id, got '{}'", bad),
76                status: 0,
77            })?;
78            let body = serde_json::json!({
79                "name": name,
80                "members": member_ids,
81            });
82            let resp = client
83                .post(&format!("/v2/team/{}/group", team_id), &body)
84                .await?;
85            let group = resp.get("group").cloned().unwrap_or(resp);
86            output.print_single(&group, GROUP_FIELDS, "id");
87            Ok(())
88        }
89        GroupCommands::Update {
90            id,
91            name,
92            add_members,
93            rem_members,
94        } => {
95            let mut body = serde_json::Map::new();
96            if let Some(n) = name {
97                body.insert("name".into(), serde_json::Value::String(n));
98            }
99            if !add_members.is_empty() || !rem_members.is_empty() {
100                // ClickUp's spec: member IDs in add/rem arrays are integers.
101                let parse = |ids: Vec<String>| -> Result<Vec<serde_json::Value>, CliError> {
102                    ids.into_iter()
103                        .map(|s| {
104                            s.parse::<i64>().map(|n| serde_json::json!(n)).map_err(|_| {
105                                CliError::ClientError {
106                                    message: format!(
107                                        "member id must be a numeric user id, got '{}'",
108                                        s
109                                    ),
110                                    status: 0,
111                                }
112                            })
113                        })
114                        .collect()
115                };
116                let add = parse(add_members)?;
117                let rem = parse(rem_members)?;
118                body.insert(
119                    "members".into(),
120                    serde_json::json!({ "add": add, "rem": rem }),
121                );
122            }
123            let resp = client
124                .put(
125                    &format!("/v2/group/{}", id),
126                    &serde_json::Value::Object(body),
127                )
128                .await?;
129            let group = resp.get("group").cloned().unwrap_or(resp);
130            output.print_single(&group, GROUP_FIELDS, "id");
131            Ok(())
132        }
133        GroupCommands::Delete { id } => {
134            client.delete(&format!("/v2/group/{}", id)).await?;
135            output.print_message(&format!("Group {} deleted", id));
136            Ok(())
137        }
138    }
139}