Lists: typed, schema-driven row editor (replaces the freeform JSON box) - #163
Merged
Merged
Conversation
…en one
Rows on a list with columns are now added and edited through a form generated
from its live schema — one control per type: single-line text, multi-line
textarea, numeric, True/False (plus **Neither** for a nullable optional
boolean), date picker, date + time, validated email/url, phone, single select,
multiselect checkboxes, and priority over its four values. `defaultValue`
pre-fills a new row; `placeholder`, `helpText`, `visible` and `displayOrder` are
honoured, and the conditional-visibility rules are evaluated client-side —
which is the only place they are ever evaluated, since the server stores a
condition and never applies it.
The grid stopped printing JSON: each row renders a labelled cell per visible
column, a checkbox for boolean, chips for multiselect, an em dash where a row
simply has no value. Row data is keyed by each column's key throughout, never
its label.
The raw-JSON escape hatch stays — it is the only editor for a schema-less list
(still fully supported) and remains one checkbox away on a schema'd one.
Live-probed behaviours this is built around (2026-09-16, throwaway lists since
deleted):
* **A row write REPLACES rowData.** A PUT carrying one key left the row holding
only that key — five other values, orphans included, were gone. So the form
re-sends every key it knows about, including keys with no column at all, and a
field the user never touched is echoed back as the exact JSON the server gave
us rather than a re-rendered one (which keeps a naive datetime, a number
stored as a string, or a nested object intact).
* **Types are enforced on write.** `"true"` in a boolean column is 422, a
multiselect must be a real array, a date must be YYYY-MM-DD — so the form
emits real booleans, arrays and ISO strings. A number is the one the server is
loose about: it accepts `"7"` and then silently skips its own min/max check,
which is exactly why this sends a number.
* **422 carries `details:[{field,message}]`** — up to eight at once — where
`field` is the column key and the message uses its label. The shared
EnsureSuccessAsync flattens that to "Validation failed", so the two row writes
now throw `ListRowValidationException` and each message lands on the control
that caused it. Cheap checks (required, email/url shape, number parse,
min/max, minLength/maxLength, pattern) run before the round trip.
* **GET /api/lists/{id}/data omits each row's `listId`**, which the strict
`ListDataRow.ListId` still requires — so reads go through a new tolerant
`GetListRowsAsync` until #144/PR #146 relaxes the model. `version` is
therefore not surfaced, and loses nothing today: the server ignores a
`version` sent on a row write (a deliberately stale one still answered 200),
so there is no optimistic concurrency to hook up yet.
Verified end-to-end by feeding the payloads the real view-model produces to the
API: a twelve-type row was created and stored byte-for-byte as sent, and an edit
carrying an unknown key and a nested object came back with both intact.
Closes #21
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked PR — 2 of 3
issue-21-typed-row-editor→ baseissue-18-column-builder(PR #161) → which bases onissue-17-list-schema-service(PR #143). Merge the stack bottom-up: #143, #161, then this, then #22.What this adds
Rows on a list with columns are added and edited through a form generated from its live schema instead of a raw-JSON box:
text/email/url/teltextareanumbervalidation.stepshown as a hintbooleannull) when the column is optionaldateYYYY-MM-DDdatetimeHH:MM→ ISOselect/prioritymultiselectdefaultValuepre-fills new rows;placeholder,helpText,visibleanddisplayOrderare honoured; hidden columns fold behind a toggle so a required hidden column is still reachable. Conditional-visibility rules are evaluated client-side byListFieldConditionEvaluator— the only place they are ever evaluated, since the server stores a condition and never applies it.The grid no longer prints JSON: one labelled cell per visible column, a checkbox for boolean, chips for multiselect, an em dash where the row has no value. Row data is keyed by each column's
keythroughout.The raw-JSON hatch stays — the only editor for a schema-less list, and one checkbox (“Edit rows as raw JSON”) away on a schema'd one.
Live evidence (probed today, 2026-09-16)
All throwaway lists were titled
ZZ claude-probe …and deleted; a closingGET /api/listsshows only the account's pre-existingNew list.rowData, it does not merge. APUTcarrying{"title":"replaced"}left the row holding onlytitle— five other values, an unknown key included, gone. So the form re-sends every key it knows about, and any field the user didn't touch is echoed back as the exactJsonElementthe server gave us, which keeps values the form can't fully model (a naive datetime, a number stored as a string, a nested object) byte-identical. A blank optional column the row never had is still left out rather than paddingrowDatawith nulls; clearing a stored value sends an explicit null."true"in a boolean column →422 Done must be true or false; a comma string in a multiselect →Tags must be an array;09/16/2026→Due must be a valid date; a select value off-list →Status must be one of: open, closed. The exceptions: a number accepted as"7"— and then its ownmin/maxcheck is silently skipped, which is exactly why the form sends a real number — and priority, which is not validated against its four values at all.details:[{field,message}], up to eight at once,fieldbeing the column key and the message using its label.EnsureSuccessAsyncflattens that to “Validation failed”, so the two row writes now throwListRowValidationExceptionand each message lands on its control. Required, email/url shape, number parse, min/max, minLength/maxLength andpatternare checked before the round trip.GET /api/lists/{id}/dataomits each row'slistId(androwNumber), whichListDataRow.ListId'srequiredturns into aJsonExceptionright throughGetListDataAsync— the crash Lists: viewing any list that has rows throws —ListDataRow.ListIdisrequiredbut the API doesn't send it #144/PR fix(lists): viewing a list with rows threw — ListId was required but absent #146 fixes.ListDataRow.csis that PR's, so this branch doesn't touch it: reads go through a new tolerantGetListRowsAsyncthat maps rows by hand and fillslistIdin from the request. Dependency: once fix(lists): viewing a list with rows threw — ListId was required but absent #146 lands,GetListDataAsyncworks again andListDataRow.Versionbecomes available.versionis not optimistic concurrency. Sending a deliberately staleversion: 1on a rowPUTstill answered200and bumped the row to 4 — the server ignores it. So there is nothing to hook up yet, and fix(lists): viewing a list with rows threw — ListId was required but absent #146'sVersionbuys a display value rather than a conflict check.net10.0console project<Compile Include>-ing the model and row-field files by absolute path produced a twelve-type row payload —201, stored exactly as sent — then an edit payload carrying an unknown key and a nested object —200, both intact. It also confirmed the local pre-flight messages, the tri-state boolean options, thestatus=closed→ “Why closed?” visibility flip, and the grid's cell kinds.Left unverified / deliberately out
dotnet buildpasses in Debug and Release. The new controls use theme brushes and 3–4px corners, but two want eyes on Windows: the templatedComboBoxdrop-down (from Lists: schema column form builder (12 field types) #161) and theDatePicker, whose calendar popup keeps its platform look — only the field itself is painted from the theme. If that reads wrong in dark mode, the fix is a templated date box, not a colour override.2026-09-16T14:30:00, accepted live) and never timezone-shifted, so a value entered is the value stored; a stored value with aZthat the user doesn't touch is re-sent verbatim.#21didn't ask for more.Closes #21
🤖 Generated with Claude Code