Skip to main content

clickup_cli/commands/
field.rs

1use crate::client::ClickUpClient;
2use crate::commands::auth::resolve_token;
3use crate::commands::workspace::resolve_workspace;
4use crate::error::CliError;
5use crate::git;
6use crate::output::OutputConfig;
7use crate::Cli;
8use clap::Subcommand;
9
10#[derive(Subcommand)]
11pub enum FieldCommands {
12    /// List custom fields
13    List {
14        /// List ID
15        #[arg(long)]
16        list: Option<String>,
17        /// Folder ID
18        #[arg(long)]
19        folder: Option<String>,
20        /// Space ID
21        #[arg(long)]
22        space: Option<String>,
23        /// Workspace-level fields
24        #[arg(long = "workspace-level")]
25        workspace_level: bool,
26    },
27    /// Set a custom field value on a task
28    Set {
29        /// Field ID
30        field_id: String,
31        /// Field value (string, number, or JSON)
32        #[arg(long)]
33        value: String,
34        /// Task ID (auto-detected from git branch if omitted)
35        task_id: Option<String>,
36    },
37    /// Unset (clear) a custom field value on a task
38    Unset {
39        /// Field ID
40        field_id: String,
41        /// Task ID (auto-detected from git branch if omitted)
42        task_id: Option<String>,
43    },
44}
45
46const FIELD_FIELDS: &[&str] = &["id", "name", "type", "required"];
47
48pub async fn execute(command: FieldCommands, cli: &Cli) -> Result<(), CliError> {
49    let token = resolve_token(cli)?;
50    let client = ClickUpClient::new(&token, cli.timeout)?;
51    let output = OutputConfig::from_cli(&cli.output, &cli.fields, cli.no_header, cli.quiet);
52
53    match command {
54        FieldCommands::List {
55            list,
56            folder,
57            space,
58            workspace_level,
59        } => {
60            let path = if let Some(list_id) = list {
61                format!("/v2/list/{}/field", list_id)
62            } else if let Some(folder_id) = folder {
63                format!("/v2/folder/{}/field", folder_id)
64            } else if let Some(space_id) = space {
65                format!("/v2/space/{}/field", space_id)
66            } else if workspace_level {
67                let ws_id = resolve_workspace(cli)?;
68                format!("/v2/team/{}/field", ws_id)
69            } else {
70                return Err(CliError::ClientError {
71                    message: "Specify --list, --folder, --space, or --workspace-level".into(),
72                    status: 0,
73                });
74            };
75
76            let resp = client.get(&path).await?;
77            let fields = resp
78                .get("fields")
79                .and_then(|f| f.as_array())
80                .cloned()
81                .unwrap_or_default();
82            output.print_items(&fields, FIELD_FIELDS, "id");
83            Ok(())
84        }
85        FieldCommands::Set {
86            task_id,
87            field_id,
88            value,
89        } => {
90            let task = git::require_task(cli, task_id.as_deref(), true)?;
91            // Try to parse value as JSON first, fallback to string
92            let parsed_value: serde_json::Value =
93                serde_json::from_str(&value).unwrap_or(serde_json::Value::String(value));
94            let body = serde_json::json!({ "value": parsed_value });
95            let resp = client
96                .post(&format!("/v2/task/{}/field/{}", task.id, field_id), &body)
97                .await?;
98            output.print_single(&resp, FIELD_FIELDS, "id");
99            Ok(())
100        }
101        FieldCommands::Unset { task_id, field_id } => {
102            let task = git::require_task(cli, task_id.as_deref(), true)?;
103            client
104                .delete(&format!("/v2/task/{}/field/{}", task.id, field_id))
105                .await?;
106            output.print_message(&format!("Field {} cleared on task {}", field_id, task.raw));
107            Ok(())
108        }
109    }
110}