Skip to content

forms(qml): a host slot can draw a std::vector<Row> member, and the form encodes its rows - #809

Merged
Yaraslaut merged 5 commits into
masterfrom
feature/forms-object-array-slot
Sep 25, 2026
Merged

Yaraslaut merged 5 commits into
masterfrom
feature/forms-object-array-slot

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

A std::vector<Row> member has no built-in control, so DynamicForm reports it unrepresentable and the form never becomes ready. A host grid slot could not fix that: setValue text went through the comma-splitting array encoder, and the slot was never told the row type's members.

When a registered slot claims a top-level collection of objects:

Descriptor key Meaning
isObjectArray array whose items resolve to an object schema with properties
itemFields the row type's member descriptors in x-order order (label, unit, decimals, readOnly, required, kind flags) — the grid's columns; one level only, so a self-referential row cannot loop
claimedBySlot a slot resolves for the member; unrepresentable is then ""
  • The slot writes rows as {member: cellText} (setRows(rows) or setValue(JSON)); each cell is encoded with encodeFieldText (fieldJsonLiteral's body, lifted to take an explicit text/unit index), so a Quantity cell is exact {num,den,dp}, an over-precise one is refused, a blank required cell keeps the form unready. [] is a valid value.
  • Optional slot members, assigned only when declared (existing slots untouched): fieldText (binding to the retained text — prefill, resetFields() and tab rebuilds now reach a slot), rows, setRows, form. With fieldText, CollectionView's row prefill reaches a slot-overridden scalar field (views.md limitation updated).
  • No slot → no change; tst_DynamicFormNestedAggregate.qml untouched and green.

⚠️ Cells are read in the member's canonical unit (no per-row unit selector); a collection nested inside a row stays unrepresentable; row labels resolve through explicit x-i18nKey or the literal only.

Spec: docs/spec/forms/forms.md — new "Collections of objects — a host slot draws them", slot-contract table in "Theming / component-override registry", "What ready claims".

Tests (MSVC 14.51, Qt 6.11.1): new tst_DynamicFormObjectArraySlot.qml (18 cases, asserting submitted bodies); replacing the cell encoder with string quoting reddens 3. forms_qml_logic 335 passed.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Sep 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Yaraslaut added a commit that referenced this pull request Sep 25, 2026
…record into DynamicForm for editing (fixes #814) (#817)

Every prefill path wrote *control text*: CollectionView sets a `field_<name>`
TextField, WizardView calls setFieldValue with text. An editing flow -- open a
saved sample, read its section DTO, prefill the form, edit, submit -- had to
turn each wire value into the text its control holds by hand: a {num,den,dp}
into locale digits at the declared precision, an ISO instant into the display
zone, a closed-set value into its valueJson, and every row of a
std::vector<Row> into cell texts, the inverse of the encoder #809 added.

`decodeFieldValue(field, value)` is that inverse, kind by kind, on exact
digits (a Quantity's num/den are divided as digit strings, an int64 id from
JsonExact.parse stays exact). `prefill(values)` replaces the whole draft from a
payload object, resets unit selectors to the canonical unit, bumps
`prefillRevision` so every drawn control re-seeds (a fetched Choice also
re-selects when its options arrive -- before this it showed "— select —" for a
retained value), re-fetches dependent Choices, and revalidates *inside* the
programmaticEdit window. That last point is measured, not assumed: the first
version reused withoutAutoSubmit, whose final revalidate runs after the
suppression is lifted, and the round-trip case caught the ready prefilled form
submitting itself. `prefillFromJson(text)` parses with JsonExact.

tst_DynamicFormPrefill.qml (11 cases) asserts the round trip against
previewLine for every member kind, the drawn controls and slots holding the
values, de_DE and a UTC+2 display zone, a later edit, replacement semantics, a
fetched Choice with an id past 2^53, and no submission. Removing the re-seed
reddens 5 cases; a wrong Quantity decoder reddens 8. forms_qml_logic: 348
passed on MSVC 14.51 / Qt 6.11.1.

Stacked on #809 (the rows / fieldText slot contract and itemFields it decodes
into).

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Yaraslaut added a commit that referenced this pull request Sep 25, 2026
…record into DynamicForm for editing (fixes #814) (#817)

Every prefill path wrote *control text*: CollectionView sets a `field_<name>`
TextField, WizardView calls setFieldValue with text. An editing flow -- open a
saved sample, read its section DTO, prefill the form, edit, submit -- had to
turn each wire value into the text its control holds by hand: a {num,den,dp}
into locale digits at the declared precision, an ISO instant into the display
zone, a closed-set value into its valueJson, and every row of a
std::vector<Row> into cell texts, the inverse of the encoder #809 added.

`decodeFieldValue(field, value)` is that inverse, kind by kind, on exact
digits (a Quantity's num/den are divided as digit strings, an int64 id from
JsonExact.parse stays exact). `prefill(values)` replaces the whole draft from a
payload object, resets unit selectors to the canonical unit, bumps
`prefillRevision` so every drawn control re-seeds (a fetched Choice also
re-selects when its options arrive -- before this it showed "— select —" for a
retained value), re-fetches dependent Choices, and revalidates *inside* the
programmaticEdit window. That last point is measured, not assumed: the first
version reused withoutAutoSubmit, whose final revalidate runs after the
suppression is lifted, and the round-trip case caught the ready prefilled form
submitting itself. `prefillFromJson(text)` parses with JsonExact.

tst_DynamicFormPrefill.qml (11 cases) asserts the round trip against
previewLine for every member kind, the drawn controls and slots holding the
values, de_DE and a UTC+2 display zone, a later edit, replacement semantics, a
fetched Choice with an id past 2^53, and no submission. Removing the re-seed
reddens 5 cases; a wrong Quantity decoder reddens 8. forms_qml_logic: 348
passed on MSVC 14.51 / Qt 6.11.1.

Stacked on #809 (the rows / fieldText slot contract and itemFields it decodes
into).

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Yaraslaut
Yaraslaut force-pushed the feature/forms-object-array-slot branch from 2b1a564 to 8fcfad4 Compare September 25, 2026 06:56
Yaraslaut added a commit that referenced this pull request Sep 25, 2026
…record into DynamicForm for editing (fixes #814) (#817)

Every prefill path wrote *control text*: CollectionView sets a `field_<name>`
TextField, WizardView calls setFieldValue with text. An editing flow -- open a
saved sample, read its section DTO, prefill the form, edit, submit -- had to
turn each wire value into the text its control holds by hand: a {num,den,dp}
into locale digits at the declared precision, an ISO instant into the display
zone, a closed-set value into its valueJson, and every row of a
std::vector<Row> into cell texts, the inverse of the encoder #809 added.

`decodeFieldValue(field, value)` is that inverse, kind by kind, on exact
digits (a Quantity's num/den are divided as digit strings, an int64 id from
JsonExact.parse stays exact). `prefill(values)` replaces the whole draft from a
payload object, resets unit selectors to the canonical unit, bumps
`prefillRevision` so every drawn control re-seeds (a fetched Choice also
re-selects when its options arrive -- before this it showed "— select —" for a
retained value), re-fetches dependent Choices, and revalidates *inside* the
programmaticEdit window. That last point is measured, not assumed: the first
version reused withoutAutoSubmit, whose final revalidate runs after the
suppression is lifted, and the round-trip case caught the ready prefilled form
submitting itself. `prefillFromJson(text)` parses with JsonExact.

tst_DynamicFormPrefill.qml (11 cases) asserts the round trip against
previewLine for every member kind, the drawn controls and slots holding the
values, de_DE and a UTC+2 display zone, a later edit, replacement semantics, a
fetched Choice with an id past 2^53, and no submission. Removing the re-seed
reddens 5 cases; a wrong Quantity decoder reddens 8. forms_qml_logic: 348
passed on MSVC 14.51 / Qt 6.11.1.

Stacked on #809 (the rows / fieldText slot contract and itemFields it decodes
into).

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Yaraslaut
Yaraslaut force-pushed the feature/forms-object-array-slot branch from 8fcfad4 to 903e512 Compare September 25, 2026 07:54
Yaraslaut and others added 5 commits September 25, 2026 10:21
…orm encodes its rows

A collection of objects has no built-in control: the comma-separated array
control encodes strings, so DynamicForm reports the member unrepresentable and
a form that must carry it never becomes ready. That was right for the built-in
controls, and it left a host with nowhere to go -- registering a grid slot for
the member changed nothing, because the slot could only hand `setValue` a text
the array encoder then split on commas, and the slot was never told what the
row type's members are.

When a registered slot claims a top-level `std::vector<Row>` member:

  - `fields` describes the row type with the same descriptor builder the
    action uses (`describeObject`, depth-limited to one level so a
    self-referential row type cannot loop) and puts it on the field as
    `itemFields`: label, unit, decimals, readOnly, required and the kind
    flags, in x-order order -- the grid's columns. `isObjectArray` and
    `claimedBySlot` say which case the descriptor is.
  - `unrepresentable` is "" for it, because an encoding now exists.
  - The slot writes rows as `{member: cellText}` objects, where a cell holds
    what that member's built-in control would hold. `encodeObjectArray`
    encodes every cell with `encodeFieldText` -- `fieldJsonLiteral`'s body,
    lifted to take an explicit text and unit index -- so a `Quantity` cell is
    an exact {num,den,dp}, an over-precise one is refused rather than rounded,
    a blank optional cell is omitted and a blank required one keeps the form
    unready.

Slots also gain four optional members, each assigned only when the slot
declares it, so every existing slot is untouched: `fieldText` (a binding to the
retained text -- until now a slot never learned a value it had not written
itself, so prefill, `resetFields()` and a tab rebuild all left it stale),
`rows`, `setRows(rows)` and `form`. With `fieldText`, CollectionView's row
prefill reaches a slot-overridden scalar field too, which views.md listed as a
limitation.

Nothing changes without a slot: tst_DynamicFormNestedAggregate.qml is
untouched and green. The new tst_DynamicFormObjectArraySlot.qml (18 cases)
asserts submitted bodies; replacing the cell encoder with plain string quoting
reddens 3 of them. forms_qml_logic: 335 passed on MSVC 14.51 / Qt 6.11.1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…record into DynamicForm for editing (fixes #814) (#817)

Every prefill path wrote *control text*: CollectionView sets a `field_<name>`
TextField, WizardView calls setFieldValue with text. An editing flow -- open a
saved sample, read its section DTO, prefill the form, edit, submit -- had to
turn each wire value into the text its control holds by hand: a {num,den,dp}
into locale digits at the declared precision, an ISO instant into the display
zone, a closed-set value into its valueJson, and every row of a
std::vector<Row> into cell texts, the inverse of the encoder #809 added.

`decodeFieldValue(field, value)` is that inverse, kind by kind, on exact
digits (a Quantity's num/den are divided as digit strings, an int64 id from
JsonExact.parse stays exact). `prefill(values)` replaces the whole draft from a
payload object, resets unit selectors to the canonical unit, bumps
`prefillRevision` so every drawn control re-seeds (a fetched Choice also
re-selects when its options arrive -- before this it showed "— select —" for a
retained value), re-fetches dependent Choices, and revalidates *inside* the
programmaticEdit window. That last point is measured, not assumed: the first
version reused withoutAutoSubmit, whose final revalidate runs after the
suppression is lifted, and the round-trip case caught the ready prefilled form
submitting itself. `prefillFromJson(text)` parses with JsonExact.

tst_DynamicFormPrefill.qml (11 cases) asserts the round trip against
previewLine for every member kind, the drawn controls and slots holding the
values, de_DE and a UTC+2 display zone, a later edit, replacement semantics, a
fetched Choice with an id past 2^53, and no submission. Removing the re-seed
reddens 5 cases; a wrong Quantity decoder reddens 8. forms_qml_logic: 348
passed on MSVC 14.51 / Qt 6.11.1.

Stacked on #809 (the rows / fieldText slot contract and itemFields it decodes
into).

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…on too

slotClaims() is the delegate's own slot resolution, but it passed five
arguments while the delegate passes the kind as well, so a grid registered
with SlotRegistry.byKind("objectArray", ...) was drawn while the form kept
the collection unrepresentable and never encoded its rows. The kind is now
computed once per descriptor and handed to both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4eif7wQNNhSkHUKYqq5Xq
The helper now revalidates inside its suppression window, so the hand-rolled
copy in prefill() and its comment explaining why it avoided the helper no
longer describe anything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4eif7wQNNhSkHUKYqq5Xq
@Yaraslaut
Yaraslaut force-pushed the feature/forms-object-array-slot branch from 903e512 to 9685964 Compare September 25, 2026 08:49
@Yaraslaut
Yaraslaut merged commit 64d339a into master Sep 25, 2026
16 checks passed
Yaraslaut added a commit that referenced this pull request Sep 25, 2026
… encodes it member by member (#822)

A struct member -- `IgnitionSpecimen specimen`, or
`std::optional<PycnometerDetermination> determination1` -- has no built-in
control: DynamicForm flattens it to one text field, reports it
unrepresentable, and a form that must carry it never becomes ready. #809
gave a host slot a way out for `std::vector<Row>`; a nested object had none.

When a registered slot claims a top-level object member:

  - `fields` marks it `isObject` and describes its members as
    `objectFields` with the same descriptor builder (label, unit, decimals,
    readOnly, required, kind flags). The description recurses: a member that
    is itself an object carries its own `objectFields`, a collection its
    `itemFields`. It is bounded by `maxObjectDepth` (4) and stops at the
    first repetition of a `$defs` type on the path, so a self-referential
    type cannot loop.
  - `unrepresentable` is "" for it and for every object or collection
    described inside it.
  - The slot writes `{member: cellText | nestedValue}` objects through the
    new optional `setObject(value)` and reads them back through the
    `objectValue` binding. `encodeObjectValue` encodes each leaf with
    `encodeFieldText` (locale, precision, bounds), a nested object
    recursively and a collection with the row encoder. A blank optional
    leaf or object is omitted; a blank required leaf or object keeps the
    form unready.
  - `prefill` / `prefillFromJson` decode a stored object back into that
    shape (`decodeObjectValue`), so prefill -> submit round-trips.

An object member no slot claims behaves as before;
tst_DynamicFormNestedAggregate.qml is untouched and green.

Schema side: `recurseIntoNestedAggregateIfAny` did not see through
`std::optional<Sub>`, which glaze spells as an `anyOf` over Sub's schema and
null, so an optional sub-record's members had no x-order, title, required
or FieldMeta (unit, x-displayDecimals, label). It now recurses into the
non-null branch. Three new test_nested_forms.cpp cases pin it; without the
fix two of them fail.

The new tst_DynamicFormObjectSlot.qml (24 cases) asserts submitted bodies
for both consumer shapes; replacing the leaf encoder with plain string
quoting reddens 9 of them. forms_qml_logic: 419 passed, full ctest 1596/1596
on MSVC / Qt 6.11.1.

Co-authored-by: Yaraslau Tamashevich <y.tamashevich@lastrada.net>
Co-authored-by: Claude Opus 5.5 (1M context) <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.

1 participant