You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Schema: replace the freeform JSON row editor with a typed, schema-driven one #21
Views/ListsView.xaml edits rows as raw JSON text. That was the correct workaround while the DSL was unverified; with the schema readable it is now the main usability gap between the app and the web product.
Scope
Generate the row form from the list's schema:
Type
Control
text
single-line text box
textarea
multi-line text box
number
numeric input (honors step)
boolean
True/False dropdown — plus Neither (null) when the field is optional
date
date picker (YYYY-MM-DD)
datetime
date + time picker (ISO)
email
text box with email validation
url
text box with URL validation
tel
text box (+ pattern if set)
select
single-select from options
multiselect
multi-select from options
priority
select over low/medium/high/urgent
Row data is keyed by each column's key. Respect defaultValue, placeholder, helpText, visible, displayOrder and the conditional-visibility rules.
Acceptance criteria
Add and edit rows through typed controls driven by the live schema.
defaultValue pre-fills new rows.
The grid renders each column by its type (checkbox for boolean, chips for multiselect, etc.), not as JSON.
A raw-JSON escape hatch remains available for schema-less lists (confirmed still supported).
Rows created by the app are readable in the web UI and vice versa.
propertyType is also mandatory on every item (omitting it gives the same error for 'undefined'/'null').
Consequence: a list using textarea, datetime, tel, select, multiselect or prioritycannot be edited non-destructively at all — every change to it has to go through the destructive DSL rebuild. That is a real, permanent constraint on #22's guard, not an edge case: the moment a user picks one of the six richer types in #18's builder, they lose safe editing on that list forever.
Exposed as ListFieldType.PropertiesEditable and ListSchema.SupportsPropertiesEdit so the UI can tell the user before they choose.
2. "Destructive" means columns, not rows — so #22's confirmation copy must not say "rows will be deleted"
Confirmed live. The DSL rebuild:
drops and recreates every column with a new id, losing any validation / options / visibility the new DSL omits;
leaves rows intact, but values whose keys no longer exist are orphaned in rowData — still stored, not shown, not validated.
And it renames the list: schema.name overwrote the title ('ZZ Throwaway Schema Probe' → 'ZZ Renamed By DSL Rebuild'), and schema.description overwrites the list description too — clearing it when omitted.#22's confirmation needs to say all three things.
3. Deleting a data-bearing column requires ?force=true
409 {…, "propertiesWithData": ["link"]}
?force=true then strips that key from every row. The shared EnsureSuccessAsync discards the propertiesWithData array, so a dedicated ListSchemaException carries Issues / PropertiesWithData / RequiresForce. #18 should name the affected columns in its confirmation rather than showing a bare 409.
4. The server never validates conditional visibility — #20 must
An unknown operator, a missing watched key, and a forward reference all return 200 and then silently never match. So there is no server-side safety net: ListSchema.Validate() does these checks client-side, and #20's editor should surface them rather than letting a rule quietly do nothing.
5. properties PUT definitively preserves row data — #22's guard is sound
A rename + column-add on a list holding two rows returned 200 and both rows came back with identical ids, identical version (still 1), and identical rowData. Column ids were preserved too, and the validationRules / helpText / visibilityCondition that the properties shape cannot even express survived untouched.
Note the envelope differs between the two directions: GET returns {"data":{…}}, the propertiesPUT returns {"properties":[…]} ordered by displayOrder — no data wrapper.
6. Schema-less lists are unaffected
Created without a schema they answer {"data":{"name":"<list title>","description":…,"fields":[]}} and accept rows with arbitrary, even nested, keys. Unknown keys are accepted on a schema'd list too — nothing is stripped. fields: [] cannot be PUT back (DSL must have at least one field), so ListSchema.Validate() catches that locally.
Row-write validation returns 422 with details:[{field,message}], which the current plumbing flattens — so #21 can't currently attach a message to the offending field without unpacking that itself.
Separately, #144/PR #146 fixed a crash on the row-read path (ListDataRow.ListId was required but absent) and modelled version, which is the hook for optimistic concurrency on row edits.
Implemented in #163 — work continues in the PR from here. Rows on a schema'd list are now edited through generated controls (tri-state boolean with Neither, multiselect checkboxes, date and date+time pickers, validated email/url, priority over its four values), keyed by each column's key, with defaults pre-filled and conditional visibility evaluated client-side since the server never applies it. The grid renders typed cells instead of JSON, and the raw-JSON hatch stays for schema-less lists.
Two live findings drove the design: a row write REPLACES rowData (a one-key PUT dropped five other values), so the form re-sends every key it knows and echoes untouched values back verbatim, unknown keys included; and 422 carries details:[{field,message}] keyed by column key, which the shared error plumbing flattened — the row writes now throw ListRowValidationException so each message lands on its control.
One dependency worth noting: GET /api/lists/{id}/data omits each row's listId, so reads go through a tolerant mapper until #146 relaxes ListDataRow. Also: the server ignores a version sent on a row write (a stale one still returned 200), so #146's Version is a display value, not a conflict check.
Activity
Adron commentedon Sep 16, 2026
Live-verified findings from the schema service (#17 / PR #143) — three change the design here
propertiesform accepts only SIX types, not twelvepropertyTypeis also mandatory on every item (omitting it gives the same error for'undefined'/'null').Consequence: a list using
textarea,datetime,tel,select,multiselectorprioritycannot be edited non-destructively at all — every change to it has to go through the destructive DSL rebuild. That is a real, permanent constraint on #22's guard, not an edge case: the moment a user picks one of the six richer types in #18's builder, they lose safe editing on that list forever.Exposed as
ListFieldType.PropertiesEditableandListSchema.SupportsPropertiesEditso the UI can tell the user before they choose.2. "Destructive" means columns, not rows — so #22's confirmation copy must not say "rows will be deleted"
Confirmed live. The DSL rebuild:
validation/options/visibilitythe new DSL omits;rowData— still stored, not shown, not validated.And it renames the list:
schema.nameoverwrote the title ('ZZ Throwaway Schema Probe'→'ZZ Renamed By DSL Rebuild'), andschema.descriptionoverwrites the list description too — clearing it when omitted. #22's confirmation needs to say all three things.3. Deleting a data-bearing column requires
?force=true?force=truethen strips that key from every row. The sharedEnsureSuccessAsyncdiscards thepropertiesWithDataarray, so a dedicatedListSchemaExceptioncarriesIssues/PropertiesWithData/RequiresForce. #18 should name the affected columns in its confirmation rather than showing a bare 409.4. The server never validates conditional visibility — #20 must
An unknown operator, a missing watched key, and a forward reference all return
200and then silently never match. So there is no server-side safety net:ListSchema.Validate()does these checks client-side, and #20's editor should surface them rather than letting a rule quietly do nothing.5.
propertiesPUT definitively preserves row data — #22's guard is soundA rename + column-add on a list holding two rows returned
200and both rows came back with identical ids, identicalversion(still 1), and identicalrowData. Column ids were preserved too, and thevalidationRules/helpText/visibilityConditionthat thepropertiesshape cannot even express survived untouched.Note the envelope differs between the two directions:
GETreturns{"data":{…}}, thepropertiesPUTreturns{"properties":[…]}ordered bydisplayOrder— nodatawrapper.6. Schema-less lists are unaffected
Created without a schema they answer
{"data":{"name":"<list title>","description":…,"fields":[]}}and accept rows with arbitrary, even nested, keys. Unknown keys are accepted on a schema'd list too — nothing is stripped.fields: []cannot be PUT back (DSL must have at least one field), soListSchema.Validate()catches that locally.Also relevant to #21
Row-write validation returns
422withdetails:[{field,message}], which the current plumbing flattens — so #21 can't currently attach a message to the offending field without unpacking that itself.Separately, #144/PR #146 fixed a crash on the row-read path (
ListDataRow.ListIdwasrequiredbut absent) and modelledversion, which is the hook for optimistic concurrency on row edits.Adron commentedon Sep 16, 2026
Implemented in #163 — work continues in the PR from here. Rows on a schema'd list are now edited through generated controls (tri-state boolean with Neither, multiselect checkboxes, date and date+time pickers, validated email/url, priority over its four values), keyed by each column's key, with defaults pre-filled and conditional visibility evaluated client-side since the server never applies it. The grid renders typed cells instead of JSON, and the raw-JSON hatch stays for schema-less lists.
Two live findings drove the design: a row write REPLACES rowData (a one-key PUT dropped five other values), so the form re-sends every key it knows and echoes untouched values back verbatim, unknown keys included; and 422 carries details:[{field,message}] keyed by column key, which the shared error plumbing flattened — the row writes now throw ListRowValidationException so each message lands on its control.
One dependency worth noting: GET /api/lists/{id}/data omits each row's listId, so reads go through a tolerant mapper until #146 relaxes ListDataRow. Also: the server ignores a
versionsent on a row write (a stale one still returned 200), so #146's Version is a display value, not a conflict check.docs: correct my own claim — `version` is NOT optimistic concurrency