forms(qml): a host slot can draw a nested-object member, and the form encodes it member by member - #822
Merged
Conversation
… encodes it member by member 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: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Sep 25, 2026
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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.
What / why
A struct member (
IgnitionSpecimen specimen,std::optional<PycnometerDetermination> determination1) has no built-in control: DynamicForm flattens it to one text field and reports it unrepresentable, so a form carrying one never becomes ready. #809 gave a host slot a way to draw astd::vector<Row>member. This PR does the same for a nested object, following #809's approach.An object member no slot claims behaves exactly as before.
tst_DynamicFormNestedAggregate.qmlis untouched and green.Contract
isObjectpropertiesthat no typed control claims (aQuantity/Choiceis not one).kindstays"object".objectFieldsobjectFieldsand a collection member itsitemFields. Bounded bymaxObjectDepth(4); a$defstype is never described twice on one path, which makes it cycle-safe.claimedBySlotunrepresentableis""for it and for every object or collection described inside it.objectValue{member: cellText | nestedValue}({}when blank).setObject(value)setValue(JSON.stringify(value)).Encoding rules (
encodeObjectValue):encodeFieldText, the encoder the member would get at top level: same locale normalisation, precision limit and bounds.{}, or every member blank) is also omitted.null, so the form is not ready. The same happens for a value that is not an object and for a cell that does not encode.Prefill:
prefill/prefillFromJsondecode a stored object withdecodeObjectValue, the inverse of the encoder, member by member throughdecodeFieldValue. So prefill → submit round-trips.Tests
src/qt/forms/tests/tst_DynamicFormObjectSlot.qml, 24 cases, with assertions on the submitted body. It covers:byKind("object")BinderIgnitionSectionandMaxDensitySectionde_DEcomma cellsCandde_DEtests/test_nested_forms.cppcases: a required nested member's FieldMeta, and a depth-2 optional member in both the$refand the inlineanyOfform.Mutation checks (actually run):
encodeObjectValuereplaced by plain string quotingforms.hppoptional recursion revertedLocal results: MSVC 14.5x, Qt 6.11.1,
QT_QPA_PLATFORM=offscreen:forms_qml_logic419 passed, 0 failedctest -R forms10/10ctest1596/1596Out of scope
std::vector<Row>is still unrepresentable, because row cells are texts.🤖 Generated with Claude Code