fix(pages): stop wrapping layout placeholders in a synthetic container (#760) - #76
Merged
Merged
Conversation
mendixlabs#760) Every mxcli-authored page carried a container nobody asked for. The builder wrapped each non-empty layout placeholder in a Forms$DivContainer named "conditionalVisibilityWidget<N>", so creating a single button produced a button AND a container, visible in the Studio Pro widget tree of every page mxcli wrote. The wrapper was never a BSON requirement. Forms$FormCallArgument carries a `Widgets` array and a Studio Pro page fills it with its top-level widgets directly — both writers already emitted an array containing exactly one element. It existed only because pages.LayoutCallArgument declared a single `Widget` field, so N widgets had to be squeezed through a 1-widget hole. Verified against Mendix's own output rather than inferred: Administration.Account_Overview in a `mx create-project` 11.12.2 app has two top-level widgets in one placeholder (a DivContainer and a LayoutGrid) and zero conditionalVisibilityWidget wrappers anywhere. That also corrects a comment of ours which claimed the wrapper is what "mxcli (and Studio Pro) adds as a layout placeholder container". Studio Pro uses that name when wrapping a widget for conditional visibility, not for placeholders — the same reasoned-by-analogy error that produced mendixlabs#812. Three places already worked around the container: DESCRIBE unwrapped it to avoid "a phantom CONTAINER wrapping all widgets", and the catalog builder skipped it as "transparent". Those readers are deliberately KEPT — projects authored before this fix still contain wrappers, and must keep reading correctly. This changes what we write, it does not retro-clean existing pages. LayoutCallArgument.Widget becomes Widgets []Widget; the builder places the authored widgets directly. Seven call sites across the two writers, the MCP backend, the builder and one example. Verified end-to-end on a real project, identical on both engines: one button -> placeholder widgets [Forms$ActionButton] 0 wrappers three widgets -> [Forms$DynamicText, Forms$ActionButton, Forms$DivContainer] (the DivContainer is the one the author wrote) 0 wrappers mx check -> "The app contains: 0 errors." DESCRIBE -> round-trips to the authored MDL Reverting the writer's widget list fails the new test with the reported symptom. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
…lder-wrapper-760 # Conflicts: # .claude/skills/fix-issue.md
ako
pushed a commit
that referenced
this pull request
Aug 1, 2026
…driver Every bug fix appends a row to the symptom table in .claude/skills/fix-issue.md, so two concurrent fixes always collide on the same line — five resolution rounds in one week. Moving the insertion point from the top of the table to the bottom did not help: both sides still append to the same place, so the collision simply moved with it (PRs #76, #77 and #78 all hit it). git's built-in "union" merge driver keeps BOTH sides of a conflicting hunk instead of raising a conflict. That is exactly right here: the file is only ever appended to, and it is looked up by matching a symptom rather than read in order, so row order carries no meaning. Verified: two branches that each append a row now merge with no conflict and both rows present. Caveat: union applies to the whole file, so two branches editing the same *prose* line would silently keep both instead of conflicting. The failure is a visible duplicated line, not corruption. Splitting the table into its own file — so union covers only append-only content — is the follow-up, deferred because that restructure would conflict with the three open PRs that all touch this table.
ako
pushed a commit
that referenced
this pull request
Aug 1, 2026
The rationale in fix-issue.md and CLAUDE.md claimed that appending at the end of the table avoids the conflict two concurrent fixes cause. It does not — both sides still append to the same line, which is why #76, #77 and #78 each hit it again after that convention was adopted. Merging is handled by the merge=union driver in .gitattributes (added in #77). Appending at the end is kept for readable diffs and rough chronology, which is a human reason, not a git one.
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.
Fixes mendixlabs#760.
The bug
Every mxcli-authored page carried a container nobody asked for. The builder wrapped each non-empty layout placeholder in a
Forms$DivContainernamedconditionalVisibilityWidget<N>, so creating a single button produced a button and a container — visible in the Studio Pro widget tree of every page mxcli wrote.Reproduced exactly as reported:
Root cause — an artificial model constraint, not a BSON requirement
Forms$FormCallArgumentcarries aWidgetsarray, and both writers already emitted an array containing exactly one element. The wrapper existed only becausepages.LayoutCallArgumentdeclared a singleWidgetfield, so N widgets had to be squeezed through a 1-widget hole.Verified against Mendix's own output rather than inferred.
Administration.Account_Overviewin amx create-project11.12.2 app:Two top-level widgets sitting directly in the placeholder, no wrapper anywhere.
That also corrects a comment of ours claiming the wrapper is what "mxcli (and Studio Pro) adds as a layout placeholder container". Studio Pro uses that name when wrapping a widget for conditional visibility, not for placeholders — the same reasoned-by-analogy error that produced mendixlabs#812.
Corroborating signal
Three places already worked around the container:
DESCRIBEunwrapped it to avoid "a phantom CONTAINER wrapping all widgets", and the catalog builder skipped it as "transparent". When three readers are compensating for a construct, it probably shouldn't be written.Those readers are deliberately kept — projects authored before this fix still contain wrappers and must keep reading correctly. This changes what we write; it does not retro-clean existing pages.
The change
LayoutCallArgument.Widget→Widgets []Widget, and the builder places the authored widgets directly. Seven call sites across the two writers, the MCP backend, the builder and one example.Verification
End-to-end on a real project, identical on both engines:
[Forms$ActionButton][Forms$DynamicText, Forms$ActionButton, Forms$DivContainer]The
DivContainerin the second row is the one the author wrote — the fix removes the synthetic wrapper, not real containers.Mutation-checked: truncating the writer's widget list fails the new test with the reported symptom. Full suite green; corpus 237/39 including the new repro fixture.
Note on test placement
I put the regression test at the writer level (
sdk/mpr) rather than the builder. The builder test needs layout resolution through a backend, and the harness plumbing was costing more than the coverage was worth; the writer test needs no backend and still pins the model change and both serializers. The builder behaviour is covered by the end-to-end BSON evidence above.🤖 Generated with Claude Code
https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
Generated by Claude Code