Skip to content

Lists: schema column form builder (12 field types) - #161

Merged
Adron merged 2 commits into
mainfrom
issue-18-column-builder
Sep 24, 2026
Merged

Adron merged 2 commits into
mainfrom
issue-18-column-builder

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Stacked PR — 1 of 3

issue-18-column-builder → base issue-17-list-schema-service (PR #143, unmerged). It needs that branch's ListSchema / ListField / ListProperty / ListPropertyUpdate / ListSchemaException and 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 by ListColumnEditorViewModel + ListColumnDraftViewModel:

  • add / edit / remove / reorder columns across all twelve DSL types;
  • per column: key, type, label, required, defaultValue, placeholder, helpText, options, visible, displayOrder (by array order);
  • opened two ways — “Define columns” next to the create-list form (the draft rides along as POST /api/lists' schema) and “Columns” on any saved list;
  • saving a saved list goes through the non-destructive properties PUT only;
  • priority shows and falls back to low/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 closing GET /api/lists shows only the account's pre-existing New list.

  1. defaultValue in a properties body must be a JSON string. A raw number (7) or boolean (true) answers 500 Internal server error and discards the whole PUT. "7" / "true" / "hello" all store fine, and GET …/schema hands them back parsed (7, true, "hello"). The DSL is the opposite — it wants the raw typed value. Models/ListFieldDefaults.cs owns that asymmetry, with the evidence in its doc comment.
  2. An omitted field on a properties item is CLEARED, not preserved. A PUT that left out isVisible/isRequired/defaultValue flipped a deliberately hidden column visible and wiped two defaults. validationRules and visibilityCondition do survive (the shape can't express them). So every draft sends its full projection on every save — that is why ToPropertyUpdate always emits all ten fields.
  3. Changing a saved column's propertyType in place is accepted (200) and does nothing to the data — a text→number change left "abc" sitting in a number column; the next write of that row fails 422 Val must be a valid number. The editor allows it and warns.
  4. propertyKey can't change (400 propertyKey cannot change for an existing property; rename propertyName instead) → the key box is read-only on saved columns and ToPropertyUpdate always sends the stored key.
  5. Deleting a data-bearing column → 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's version. No native dialog, nothing blocking the dispatcher.
  6. A schema-less list can gain its first columns through the safe path — properties PUT on a list with fields: [] created them and left its freeform (even nested) row data untouched.
  7. End-to-end with the real view-model: a throwaway net10.0 console 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, visible and placeholder intact, and a typed row accepted. The six-type subset went through the properties PUT — 200, defaults decoded as 2.5 / false, isVisible: false and isRequired: true preserved.

Deliberately not here

  • The destructive DSL rebuild. A draft that can only be expressed as a rebuild (any of the six richer types on a saved list) is refused with an explanation naming the types and the six the safe path accepts. Schema: guard the destructive DSL rebuild vs the non-destructive properties edit #22 adds the labelled rebuild action and its confirmation; shipping it inside “Save columns” is exactly the footgun that issue exists to prevent.
  • Validation rules and conditional-visibility editing (Schema: validation-rule editor + client-side row validation #19 / Schema: conditional-visibility rule editor #20). The drafts carry validation and visibility untouched, so emitting a DSL from a loaded list doesn't silently drop rules the user never saw.
  • No WPF rendering check was possible from macOS — dotnet build passes in Debug and Release, and the new ComboBox/ComboBoxItem templates 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

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>
@Adron

Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member Author

Two notes from me on this stack

1. The "5 GitHub-backed lists were already gone" observation — that was my error in the brief, and nothing was deleted

I told you the test account had "one real list (New list) plus 5 GitHub-backed lists". That was wrong. I'd sampled those 5 via GET /api/users/adron/lists when doing #61, and then described them as belonging to the test account. They belong to adron, and they're all still there:

=== test account's OWN lists (GET /api/lists) ===
  New list                       source=local    total: 1

=== adron's public lists ===
  total 13 lists, 5 github-backed:
    dashingarrivals              Adron/dashingarrivals
    interlinedlist-windows-app   CompositeCode/interlinedlist-windows-app
    interlinedlist-android       CompositeCode/interlinedlist-android
    interlinedlist-macos-native  CompositeCode/interlinedlist-macos-native
    interlinedlist-ios           CompositeCode/interlinedlist-ios

Your baseline of exactly one list was correct. Right call to flag the discrepancy and leave them alone rather than assume — and the leftover tmp-53-freshness-probe has since been cleaned up by whichever agent made it. Thanks for the rigorous cleanup accounting; the account is back to exactly New list.

2. Your version finding corrected one of my claims — I've verified and fixed it

You found that a stale version still returns 200. That contradicted my own PR #146, where I wrote that version was "the hook for optimistic concurrency on row edits". I re-verified:

row at version 2, PUT {"data":{…},"version":1}
  -> 200, version becomes 3, the write lands

You were right; I was asserting an assumption. Corrected on #146 with the evidence, plus your PUT replaces-not-merges finding, which is exactly why #163's echo-every-known-key approach is necessary rather than merely tidy.

The findings in this stack worth flagging to reviewers

Several are the kind that cause silent data loss, so they're worth reading before the code:

  • defaultValue in a properties body must be a JSON string — a raw 7 or true returns 500 and discards the whole PUT, while the DSL wants the opposite (raw typed value). That asymmetry is a landmine.
  • An omitted field on a properties item is CLEARED, not preserved — this contradicts the natural reading of Schema: models + GET/PUT /api/lists/{id}/schema service #17's "non-destructive" framing. A PUT omitting isVisible/isRequired/defaultValue flipped a hidden column visible and wiped two defaults. Always sending the full projection is the right call.
  • An empty properties: [] answers 409 listing data-bearing keys, not "must have at least one field" — so the ≥1-column rule really is client-side.
  • Changing a saved column's propertyType in place is accepted and migrates nothing — "abc" survives in a now-number column and only the next write of that row fails. Allowing it with a warning is the right trade, but reviewers should know.
  • priority values aren't validated ("whenever" → 201) and a number sent as "7" skips its own min/max check — good reason for the form to emit real numbers.

Noted on the unrendered-UI caveat: the templated ComboBox, the DatePicker popup and the amber confirmation panels do need an eyes-on pass on Windows. That's inherent to this repo (WPF can't run on macOS) and is called out honestly rather than glossed.

@Adron
Adron merged commit 0bc6c1e into main Sep 24, 2026
1 check passed
Adron added a commit that referenced this pull request Sep 24, 2026
…erter

fix(build): duplicate InverseBoolToVisibilityConverter after #161 + #173
Adron added a commit that referenced this pull request Sep 24, 2026
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>
Adron added a commit that referenced this pull request Sep 24, 2026
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>
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.

Schema: column form builder UI (12 field types)

1 participant