Lists: schema column form builder (12 field types) - #161
Conversation
A column editor in ListsView for creating and editing a list's columns — add / edit / remove / reorder across all twelve DSL types, with `key`, `type`, `label`, `required`, `defaultValue`, `placeholder`, `helpText`, `options`, `visible` and `displayOrder` per column. It is used at list-create time (the draft rides along as POST /api/lists' `schema`) and on a saved list, where it writes through the NON-DESTRUCTIVE `properties` PUT only. Client-side gates, so a bad draft never becomes a request: at least one column, no duplicate keys, a label and key on every column, at least one option for select/multiselect, and a default that parses for its type. The save button is disabled while any of those hold, with the first blocker spelled out next to it. `priority` shows (and falls back to) low/medium/high/urgent. Three API behaviours drive the design, all live-probed today against throwaway lists (created, exercised, deleted — the account is back to its one real list): * The `properties` body accepts only text/number/boolean/date/url/email, so the editor warns the moment a richer type is picked: choosing one costs safe editing on that list permanently. A draft that needs the rebuild is refused here with that explanation rather than silently wiping every column — the destructive path is #22's separate, labelled action. * An omitted field on a `properties` item is CLEARED, not preserved. Verified: a PUT that left out isVisible/isRequired/defaultValue flipped a hidden column visible and wiped two defaults. Every draft therefore sends its full projection on every save. * `defaultValue` in a `properties` body must be a JSON *string*: a raw number or boolean answers 500 and discards the whole request. `ListFieldDefaults` owns that asymmetry (the DSL wants the raw typed value; the stored form is encoded text; GET …/schema hands it back parsed). Plus: deleting a column that still holds data answers 409 with `propertiesWithData`, which becomes an in-place confirmation naming those columns (no native dialog); a saved column's key is locked because the server rejects a key change; and changing a saved column's type is allowed but warns, since existing row values are neither converted nor re-validated. Verified live end-to-end by feeding the bodies the real draft view-model serializes to the API: the twelve-type DSL created all thirteen columns with labels, defaults, options, required/visible and placeholder intact, and the six-type `properties` body round-tripped its defaults decoded (2.5, false). Closes #18 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two notes from me on this stack1. The "5 GitHub-backed lists were already gone" observation — that was my error in the brief, and nothing was deletedI told you the test account had "one real list ( Your baseline of exactly one list was correct. Right call to flag the discrepancy and leave them alone rather than assume — and the leftover 2. Your
|
Conflict in ListsView.xaml: #174's PoweredTemplatePanel and #161's column form builder both insert at the same point in the right pane. Two independent panels, both wanted — kept both, AI first. That order is deliberate rather than incidental: the Powered Template panel is a compact entry point that hides itself when AI is unavailable, while the column builder is a large editor shown only when opened. AI above the form also matches the web, where Powered Templates is a tab over the new-list form. Debug and Release both build clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ListsView.xaml conflict: GitHubListHeader (#74) and the PoweredTemplatePanel + column form builder (#174/#161) all insert at the top of the right pane. All additive — kept all three. Order is deliberate: GitHubListHeader goes FIRST because it identifies the selected list (owner/repo link, private-repo tag, Refresh from GitHub), so it belongs directly under the title rather than below a large editor panel. Debug and Release both build clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stacked PR — 1 of 3
issue-18-column-builder→ baseissue-17-list-schema-service(PR #143, unmerged). It needs that branch'sListSchema/ListField/ListProperty/ListPropertyUpdate/ListSchemaExceptionand the four schema service methods. Review or merge #143 first.The rest of the stack: #21 (typed row editor) branches off this one, #22 (destructive-vs-safe guard) off #21.
What this adds
A column form builder in
Views/ListsView.xaml, driven byListColumnEditorViewModel+ListColumnDraftViewModel:key,type,label,required,defaultValue,placeholder,helpText,options,visible,displayOrder(by array order);POST /api/lists'schema) and “Columns” on any saved list;propertiesPUT only;priorityshows and falls back tolow/medium/high/urgent— the implicit set is never sent, and the server hands it back anyway.Client-side gates (save button is disabled, with the first blocker written next to it): ≥1 column, no duplicate keys, key + label present, ≥1 option for
select/multiselect, parseable default for the type.ListSchema.Validate()runs too, so a bad conditional-visibility rule is caught even though the server accepts it silently.Live evidence (probed today, 2026-09-16)
Every throwaway list was titled
ZZ claude-probe …, exercised, then deleted; a closingGET /api/listsshows only the account's pre-existingNew list.defaultValuein apropertiesbody must be a JSON string. A raw number (7) or boolean (true) answers500 Internal server errorand discards the whole PUT."7"/"true"/"hello"all store fine, andGET …/schemahands them back parsed (7,true,"hello"). The DSL is the opposite — it wants the raw typed value.Models/ListFieldDefaults.csowns that asymmetry, with the evidence in its doc comment.propertiesitem is CLEARED, not preserved. A PUT that left outisVisible/isRequired/defaultValueflipped a deliberately hidden column visible and wiped two defaults.validationRulesandvisibilityConditiondo survive (the shape can't express them). So every draft sends its full projection on every save — that is whyToPropertyUpdatealways emits all ten fields.propertyTypein place is accepted (200) and does nothing to the data — atext→numberchange left"abc"sitting in a number column; the next write of that row fails422 Val must be a valid number. The editor allows it and warns.propertyKeycan't change (400 propertyKey cannot change for an existing property; rename propertyName instead) → the key box is read-only on saved columns andToPropertyUpdatealways sends the stored key.409+propertiesWithData: ["link"]→ in-place confirmation naming the columns (label + key), then a re-send with?force=true, which strips the key from every row and bumps each row'sversion. No native dialog, nothing blocking the dispatcher.propertiesPUT on a list withfields: []created them and left its freeform (even nested) row data untouched.net10.0console project<Compile Include>-ing the model files by absolute path serialized a 13-column, twelve-type draft and POSTed it — 201, all columns back with labels, defaults, options,required,visibleandplaceholderintact, and a typed row accepted. The six-type subset went through thepropertiesPUT — 200, defaults decoded as2.5/false,isVisible: falseandisRequired: truepreserved.Deliberately not here
propertiesedit #22 adds the labelled rebuild action and its confirmation; shipping it inside “Save columns” is exactly the footgun that issue exists to prevent.validationandvisibilityuntouched, so emitting a DSL from a loaded list doesn't silently drop rules the user never saw.dotnet buildpasses in Debug and Release, and the newComboBox/ComboBoxItemtemplates use only theme brushes (SurfaceBrush,BorderBrush,Surface3Brush,NavActiveBgBrush,AmberBrush) with 3–4px corners, but the type picker's drop-down wants a real eyes-on pass on Windows.Closes #18
🤖 Generated with Claude Code