Skip to content

feat(field): read and write custom fields with per-type value coercion - #44

Merged
timimsms merged 1 commit into
mainfrom
feat/custom-fields
Aug 28, 2026
Merged

timimsms merged 1 commit into
mainfrom
feat/custom-fields

Conversation

@timimsms

Copy link
Copy Markdown
Owner

Summary

Implements custom field read/write and adds a cu field command over it.

GetCustomFields and SetCustomFieldValue have been declared in internal/interfaces/api.go since the interface was written, with no implementation behind them. This adds those (plus RemoveCustomFieldValue) on api.Client and exposes:

cu field list [--list <id>]          # fields available on a list
cu field get <task-id> [field]       # values set on a task
cu field set <task-id> <field> <value>
cu field clear <task-id> <field>

Values are coerced per type rather than passed through as strings, which is the substance of the change — ClickUp expects epoch milliseconds for dates, option UUIDs for dropdowns, and an array of option UUIDs for labels. So names work on the command line:

$ cu field set 86dxbeqyt Machine "Chilastra,Sunrunner"
Set Machine on task 86dxbeqyt
$ cu field get 86dxbeqyt
name          type        id                                    value
Machine       labels      a258ab77-5866-4ee8-a83a-295fc614ab10  Chilastra, Sunrunner
Repo          url         a5013cb5-9244-49be-a6a2-90e1cc8a1d31  https://github.com/timimsms/cu
Last synced   date        43d99b00-1747-483c-824f-a887db7f3761  2026-08-22 04:00

Dates accept today, now, 2006-01-02, RFC3339, or epoch milliseconds.

Design choices worth reviewing:

  • Fields are resolved from the task's own list, so space- and folder-level fields resolve without the caller knowing where they were defined.
  • An ambiguous field name is an error, not a first match.
  • Unknown option names are rejected with the valid set listed.
  • Unsupported types refuse rather than guess. formula, rollup, manual_progress and friends return setting fields of type "manual_progress" is not supported — a silently wrong write is worse than a clear error.
$ cu field set 86dxbeqyt Machine Nonexistent
Error: no option "Nonexistent" on field "Machine" (available: Chilastra, Sunrunner, Aberama Gold, Rinzler)

Addresses PROJECT_SPEC §13 open question 1 ("Support ClickUp Custom Fields in JSON output?") — all four output formats share one row shape, so -o json gives {name, type, id, value}.

Tests

TestFindCustomField, TestCoerceCustomFieldValue, TestFormatCustomFieldValue cover every supported type, both resolution directions (name→id on write, id→name on read), and each rejection path. Set/get/clear round-trips were also exercised live against a real workspace.

Checklist

  • ./scripts/ci.sh passes locally — except errcheck, which reports the same 27 pre-existing findings on main, none in files this PR touches
  • Commit messages use conventional prefixes
  • CLI docs regenerated (cu docs markdown --dir docs/site/commands) and added to the mkdocs nav
  • Docs updated if user-facing behavior changed — README feature list and examples

Implements the GetCustomFields and SetCustomFieldValue methods that
internal/interfaces/api.go has declared since the interface was written, and
adds the `cu field` command over them: list, get, set, clear.

Values are coerced to the shape each type expects rather than passed through as
strings — epoch milliseconds for dates, resolved option ids for dropdowns and
labels — so `cu field set <task> Machine "Chilastra,Sunrunner"` works with
names. Unknown option names are rejected with the valid set; unsupported types
(formula, rollup, progress) refuse rather than guess, since a silently wrong
write is worse than a clear error.

Fields are resolved from the task's own list, so space- and folder-level fields
resolve without the caller knowing where they were defined. An ambiguous field
name is an error rather than a first match.

Addresses PROJECT_SPEC §13 open question 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aqbmccWm1tqttmBUCR5rv
ClickUp: 86dxbeqyt
@timimsms

Copy link
Copy Markdown
Owner Author

Review — no blockers

Good shape overall. CoerceCustomFieldValue refusing unknown types in the default: case rather than guessing is the right call, and FindCustomField treating an ambiguous name as an error instead of a silent first-match is the detail that usually gets skipped. Resolving through the task's own list in resolveTaskField is what makes space- and folder-level fields work without the caller knowing where the field was defined — worth the extra GetTask.

Non-blocking

  1. The go.mod change doesn't belong to this PR. It promotes github.com/spf13/pflag from // indirect to a direct requirement, but nothing on this branch imports pflag:

    $ git grep -n "spf13/pflag" feat/custom-fields -- '*.go'
    (no output)
    

    The actual pflag import is in #43's internal/config/config_test.go. Looks like it leaked across while both branches were open. Harmless in the end state — once both land, pflag genuinely is direct and the file is correct — but go mod tidy on this branch alone would revert it. Not worth a rebase; just noting it so it isn't mistaken for a real dependency addition later.

  2. labels splits on comma unconditionally. strings.Split(raw, ",") means an option label that itself contains a comma can't be selected. Fine as a documented limit, just not currently documented anywhere the user sees.

  3. field get and field set resolve against different sources. get looks in task.CustomFields; set/clear look in client.GetCustomFields(task.List.ID). If ClickUp returns every accessible field on the task payload (with the value omitted when unset) these agree and there's nothing to do — worth a quick confirm against a real task that has a space-level field never set on it, since if the task payload only carries set fields, cu field get <task> Repo errors on a field cu field set accepts.

  4. drop_down rendering assumes the value is the option uuid. FormatCustomFieldValue matches f.Value against o["id"] and falls back to printing the raw value. If ClickUp hands back an orderindex for dropdowns on some responses, the fallback prints a bare integer rather than the label. Non-fatal by construction, just noting the fallback is the interesting path here.

@timimsms
timimsms merged commit 685dbed into main Aug 28, 2026
16 checks passed
@timimsms
timimsms deleted the feat/custom-fields branch August 28, 2026 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant