Skip to content

Add custom field support to plnk-core and plnk - #3

Merged
plattnum merged 7 commits into
mainfrom
feat/custom-fields
Aug 10, 2026
Merged

Add custom field support to plnk-core and plnk#3
plattnum merged 7 commits into
mainfrom
feat/custom-fields

Conversation

@plattnum

Copy link
Copy Markdown
Owner

Adds custom field support across the SDK and CLI: three new resources (field-group, field, card field) with full CRUD, ID-or-name resolution following the house three-tier match, and client-side validation of Planka's 512-character cap.

Name resolution reaches through base groups — a card group adopted from a template has a null name and no fields of its own, so both the group name and its field names resolve via baseCustomFieldGroupId. Every endpoint was verified against a live Planka server, and the wire quirks found along the way (several contradicting Planka's published OpenAPI spec, including a base-group GET route that returns HTML with a 200) are recorded in AGENTS.md.

Add BaseCustomFieldGroup, CustomFieldGroup, CustomField and CustomFieldValue
to plnk-core, with UpdateCustomFieldGroup / UpdateCustomField param structs and
Tabular impls for all four.

Extend BoardSnapshotIncluded, CardSnapshotIncluded and ProjectSnapshotIncluded
with the custom field collections, and add CustomFieldGroupSnapshot plus
ProjectsListSnapshot envelopes.

Models carry no consumer yet, so the new envelope fields are marked dead_code
until the API traits land.

Unit tests deserialize payloads captured verbatim from a live server, including
the adopted-group case where name is null and the display name lives on the
base group.
Add CustomFieldGroupApi, CustomFieldApi and CardCustomFieldApi, implement all
three for PlankaClientV1, and export them from plnk_core::api.

Base custom field groups are a distinct wire resource with their own routes, so
they get their own methods rather than being folded into the group methods.
There is no GET /api/base-custom-field-groups/{id} route at all, so base groups
are read from the projects list; PATCH and DELETE on that prefix do exist and
are used directly.

list_fields and find_fields take a base flag because a base group's fields are
reachable only through the projects list. list_fields_for_card reads the card
snapshot so name resolution can fetch a card's own fields in one call.

clear_field_value maps a 404 to Ok(()) — clearing means 'ensure unset', which an
already-unset field satisfies. Only that route's 404 is swallowed.

12 wire tests pin the composite value path with no dollar sign, the plural
DELETE route, idempotent clear, base-vs-group create routing, and explicit
false surviving a field PATCH.
Add the field-group resource to the clap tree, wired to CustomFieldGroupApi,
with the hidden field-groups plural alias.

--project, --board and --card form one mutually exclusive scope group on list,
find and create. A project scope yields base groups and a board or card scope
yields ordinary groups; these are different types with different columns and are
not forced through one shape.

get, update and delete try the ordinary route first and fall back to the base
route on a not-found, because base groups are a separate wire resource and an ID
alone does not say which kind it is. Falling back on delete is safe: the first
request made no change.

Add baseCustomFieldGroupId to CustomFieldGroup's trimmed columns. An adopted
group's own name is null, so without it the trimmed row is a blank name with
nothing explaining why.
Add the field resource to the clap tree, wired to CustomFieldApi, with the
hidden fields plural alias.

--group and --base-group form one mutually exclusive scope group on list, find
and create. The pair resolves to a group ID plus a base flag, because the kind
cannot be inferred from an opaque ID and the two are reached by different
routes.

--show-on-front is a bare flag on create, defaulting to false to match Planka.
On update it takes an explicit true or false so that leaving it unset stays
distinguishable from setting it false in the PATCH body.
Add the value surface under card, mirroring card label, with ID-or-name
resolution for --group and --field.

Resolution reaches through the base group. A card group adopted from a base
group has name: null and carries no fields of its own, so both the group name
and the field names must be looked up on the base group. Matching a card group
by its own name alone would match nothing for every template-adopted group,
which is the common case.

The base-group lookup is skipped entirely when no group needs it, and costs one
request rather than one per group. Arguments that are already IDs skip
resolution completely.

Values are validated before any request is issued: empty is rejected with a
message naming card field clear, and over-length is rejected against the 512
character cap. Both would otherwise cost a round trip and return an opaque 400.

Add list_all_base_field_groups to CustomFieldGroupApi — resolving a card's
adopted groups needs their names without walking card to board to project.

12 e2e tests against the mock server. Removing the base-group fallback fails
six of them, including both resolution tests; the ID-bypass test still passes,
which is the correct split.
Add docs/cli/custom-fields.md, linked from the README and from the grammar
reference, plus a worked example in docs/cli/examples.md covering the full
template-to-value round trip.

Record the verified wire quirks in AGENTS.md. Several contradict Planka's
published OpenAPI spec, including three that cost a debugging round trip to
find: there is no GET route for base custom field groups (it returns the SPA's
HTML with 200), base group fields are absent from the board snapshot, and an
adopted card group carries neither a name nor any fields of its own.

Add machine-help examples for all three resources and tests asserting that the
help text states what --show-on-front controls, the 512 character cap, and that
clearing is a separate command. Extend the alias tests to cover field-groups
and fields, and the resource sweep to cover field-group and field.

CHANGELOG entry under Unreleased.
The skill is symlinked into ~/.claude/skills, so agents load it directly from
this repo — without these entries an agent has no way to know the commands
exist. Its frontmatter description drives discovery, so the custom field
keywords go there too.

references/commands.md gains field-group, field and card field sections plus
intent-table and alias entries. references/api-quirks.md gains the three traps
an agent will actually hit: an adopted group has no name and no fields of its
own, base groups are only partly routed, and the value rules around the 512
character cap, empty values and idempotent clear.

docs/cli/cards.md gains a card field section next to card label, since card
field is a card subcommand and readers look for it there.

The Jekyll site under docs/ enumerates no resources, so nothing there is stale;
its What's new card is a release-time change and the feature is still
unreleased.
@plattnum
plattnum merged commit 600c2bf into main Aug 10, 2026
8 checks passed
@plattnum
plattnum deleted the feat/custom-fields branch August 10, 2026 11:29
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