Skip to content

Commit 9b0d927

Browse files
MendixMauclaude
andcommitted
fix(pages): ALTER PAGE/SNIPPET SET RenderMode on a dynamic text (closes mendixlabs#1228)
`alter page P { set RenderMode = H2 on <dynamictext> }` was refused as "not a property of this built-in widget", although CREATE PAGE and REPLACE accept RenderMode on a dynamictext and the MCP backend's mutator already sets it. setRawWidgetPropertyMut had no case for it, so it fell through to the pluggable-property setter. Add a RenderMode case dispatched on the stored $Type (Forms$DynamicText): the value is validated case-insensitively against Text, Paragraph and H1-H6 and written in its canonical spelling; anything else is refused with the accepted list, leaving the document untouched. Other widgets keep the previous path. `check -p` picks this up too, since it dry-runs the same setter. Docs: alter-page skill, docs-site alter-page.md, MDL_QUICK_REFERENCE.md, `mxcli syntax page.alter`, and a fix-issue finding. Tested: make test (84 packages ok, 0 FAIL), make lint. The new unit tests fail without the mutator change (8 subtests) and pass with it. Mendix 11.12.2, fresh blank app: the bug-test script exits 1 on main and applies on this branch (describe: H2, Paragraph, H4); mx check 0 errors; H7 refused with nothing written; at runtime the home-page heading set to H5 renders as <h5>. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 9509176 commit 9b0d927

8 files changed

Lines changed: 216 additions & 2 deletions

File tree

‎.claude/skills/fix-issue/findings/mdl-backend.jsonl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,4 @@
134134
{"area":"mdl/backend","date":"2026-09-25","symptom":"A data view with `DataSource: nanoflow Module.NF` (e.g. describe → exec of Feedback v4.0.2's FeedbackModule.ShareFeedback) passes `mxcli check` and exec, then mxbuild 11.13.0 reports CE2633 \"No nanoflow configured for the data source of this data view\". Same result through `alter page … set DataSource = nanoflow X on dv`","cause":"Both writers nested the name in a `Forms$NanoflowSettings` child (ParameterMappings marker 3) by analogy with `Forms$MicroflowSource`, which really does nest `Forms$MicroflowSettings`. Studio Pro's `Forms$NanoflowSource` is FLAT: ForceFullObjects, Nanoflow, ParameterMappings (marker 2) directly on the source — exactly gen's shape. mxbuild found no Nanoflow key. The raw nested builder also never read d.ParameterMappings, so a parameterized source nanoflow lost its arguments. On the read side, the ALTER PAGE flow-context lookup (`flowFromDataSourceDoc`) and describe's argument reader (`flowSourceArgs`) only knew the nested shape, so Studio Pro-authored nanoflow sources yielded no entity context / no arguments","file":"`mdl/backend/modelsdk/widget_write_legacy_gaps.go` (`nanoflowSourceToGen` via gen + `Forms$NanoflowSource` TypeDefaults in `widget_write.go`), `mdl/backend/pagemutator/mutator.go` (`serializeDataSourceBson`, `flowFromDataSourceDoc`), `mdl/executor/cmd_pages_describe_datasource.go` (`flowSourceArgs`)","insight":"**The code comment asserted the wrong shape as a measured fact** (\"Studio Pro nests it in a Forms$NanoflowSettings child … Legacy's shape is the one with a working project behind it\") and a unit test pinned it — both were parity-with-legacy, never measured. `Forms$NanoflowSettings` is not a type in modelsdk/gen or generated/metamodel: **when gen and a hand-rolled builder disagree about a type's shape, grep gen for the type the builder invents before trusting the builder**. What settled it in one step: a 60-line scanner that `bson.Unmarshal`s every mprcontents unit and prints the key-set (with list markers) of each `$Type` instance — 5 of 5 flat nanoflow sources, and 4 of 4 microflow sources nested as gen says, so the microflow path needed nothing. **Enumerate every writer of the type, not just the reported one**: the ALTER PAGE setter had its own copy of the same wrong literal, and the read-side lookups keyed on the wrong shape meant Studio Pro pages were the ones silently mis-read. Readers keep the nested fallback for pages written before the fix. Verified: exec + `mx check` 11.13.0 CE2633 → 0 errors for CREATE PAGE (ShareFeedback round trip, repro script) and ALTER PAGE; ShareFeedback's dataView5 DataSource ndsl now matches Studio Pro exactly. Control: implementation reverted → the 5 new tests fail with the nested key set. Repro `mdl-examples/bug-tests/dataview-nanoflow-source-ce2633.mdl`","refs":[],"ce":["CE2633"]}
135135
{"area": "mdl/backend", "date": "2026-09-25", "symptom": "`returns list of pEntity` for a declared type parameter produced mx check CE1613 \"The selected entity '.pEntity' no longer exists.\" on the action and on a `list of pEntity` parameter; a Studio Pro \"List of <type parameter>\" read back as a bare `List` (DESCRIBE and catalog) and a rewrite of it would serialize a list of an unnamed entity.", "cause": "types.ListType carried only Entity. The reader handled only a ConcreteEntityType list element, the writer always emitted one, and CREATE sent `list of T` down the entity path (Module \"\" + \".\" + T).", "file": "mdl/types/javaaction_types.go (ListType.TypeParameterID), mdl/backend/modelsdk/java_read.go (listTypeFromGen), java_write.go (codeActionListTypeToGen), javascript_read.go, mdl/executor/cmd_javaactions.go (listOfTypeParameter)", "insight": "The Model SDK is the arbiter for which element a slot accepts: `ParameterizedEntityType.createInListTypeUnderParameter` (metamodel 7.21.0+) settles that a list element may be a type parameter, so no version gate. No fixture had a Studio Pro-authored instance, so the evidence is mx check on 11.6.6: previous build CE1613 x2, fixed build 0 errors, clean baseline 0. The JavaScript writer reuses the Java converter, so one write fix covers both; the JS reader is separate raw-map code and needed its own case.", "refs": ["mendixlabs/mxcli#1183"]}
136136
{"area": "mdl/backend", "date": "2026-09-26", "symptom": "`ALTER PAGE … SET ImageUrl = '…' ON img1` (pluggable Image) prints \"Altered page\" and changes nothing: DESCRIBE still shows the old URL. Same silent success for SET on any pluggable widget property of kind Expression, Image, Icon, Action, DataSource, Attribute or Widgets", "cause": "setPluggableWidgetPropertyMut (mdl/backend/pagemutator/mutator.go) wrote every value to Value.PrimitiveValue. imageUrl is a TextTemplate; readers take Value.TextTemplate. The DataGrid 2 column setter had the identical defect fixed on 2026-08-18 (columnValueField) — the widget-level setter sitting next to it was never given the same schema dispatch.", "file": "mdl/backend/pagemutator/mutator.go (setPluggableWidgetPropertyMut, buildPropKindMap), test pluggable_property_kind_test.go, example mdl-examples/bug-tests/1201-alter-page-set-pluggable-texttemplate.mdl", "insight": "When a fix lands on one setter for 'write the field the schema declares', grep the sibling setters in the same file for the same always-PrimitiveValue write — a WidgetValue carries every variant field, so the wrong write never errors and every signal stays green. The live control is cheap without mxbuild: copy testdata/expr-checker, create an image with URL A, ALTER to B, describe (pre-fix binary shows A). A null TextTemplate means the slot is hidden (#574); ALTER does not re-run visibility, so refuse rather than build an envelope there. Not fixed here: a pluggable boolean SET stores \"yes\"/\"no\" — check what CREATE stores before changing it.", "refs": ["mendixlabs/mxcli#1201", "mendixlabs/mxcli#1069", "mendixlabs/mxcli#750", "mendixlabs/mxcli#574"], "rules": []}
137+
{"area": "mdl/backend", "date": "2026-09-22", "symptom": "`alter page P { set RenderMode = H1 on <dynamictext> }` is refused: `failed to set RenderMode on compTitle: property \"RenderMode\" not found (widget has no pluggable Object)` (on current main: `not a property of this built-in widget … use alter styling`), while `create page … dynamictext x (RenderMode: H1)` and `replace x with { dynamictext … }` accept it. `check -p` refuses it too, since it dry-runs the same setter", "cause": "setRawWidgetPropertyMut is a hand-kept switch of first-class built-in properties (caption/content/label/class/…); anything not listed falls through to the pluggable-property setter. RenderMode had no case, although the MCP mutator (mdl/backend/mcp/page_mutator.go) has had one all along — the two backends' SET vocabularies are separate lists", "file": "`mdl/backend/pagemutator/mutator.go` (setDynamicTextRenderModeMut)", "insight": "When a CREATE property is refused by ALTER SET on a built-in widget, compare the CREATE builder's property list with the case list in setRawWidgetPropertyMut (and the MCP mutator's) — every such refusal so far (DynamicClasses, lowercase class, RenderMode) was a missing case, not a storage question. Dispatch RenderMode on the stored `$Type` (Forms$DynamicText): containers and ActionButtons store differently-valued RenderMode/RenderType, and a pluggable widget may own a `renderMode` key, so those keep the old path. Validate against pages.TextRenderMode (Text, Paragraph, H1–H6) case-insensitively and store the canonical spelling — the visitor passes `h2` through as typed. Still missing from SET on dynamictext: ContentParams (CREATE accepts it; SET has no case). Control: HEAD's mutator.go makes the new tests fail with the reported refusal; real 11.14.0 copy: exec → describe shows H2 → mx check 0 errors", "refs": []}

‎.claude/skills/mendix/alter-page/SKILL.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ so a silent write would build cleanly and then fail to open.
153153
| `'<slotKey>'` | Pluggable widgets — any **action-typed** property (File Uploader `createFileAction`, DataGrid 2 `onSelectionChange`, …) | Any `create page` action expression | `set 'createFileAction' = microflow M.ACT_Create on fileUploader1` — refused, naming the widget's action slots, if the key is not action-typed |
154154
| `caption` | ACTIONBUTTON, LINKBUTTON | String | `set caption = 'Submit' on btnSave` |
155155
| `content` | DYNAMICTEXT | String | `set content = 'New Heading' on txtTitle` |
156+
| `RenderMode` | DYNAMICTEXT | Text, Paragraph, H1–H6 (any case; anything else is refused) | `set RenderMode = H2 on txtTitle` |
156157
| `label` | TEXTBOX, TEXTAREA, DATEPICKER, COMBOBOX, CHECKBOX, RADIOBUTTONS | String | `set label = 'full Name' on txtName` |
157158
| `buttonstyle` | ACTIONBUTTON, LINKBUTTON | Primary, Default, Success, Danger, Warning, Info | `set buttonstyle = danger on btnDelete` |
158159
| `class` | Any widget | CSS class string | `set class = 'card mx-2' on container1` |

‎cmd/mxcli/syntax/features_page.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) {
284284
"popup width", "popup height", "popup resizable",
285285
"drop template", "insert template", "list view template",
286286
},
287-
Syntax: "ALTER PAGE Module.Name {\n SET property = value ON widgetName; -- widget property names: any casing\n SET 'Row size' = 'Small' ON lvOrders; -- an Atlas DESIGN property of that widget's\n -- type; quoted and case-sensitive.\n -- `show design properties for <type>` lists\n -- them. ON/OFF for a toggle, where OFF\n -- REMOVES the entry.\n -- A multi-select ('Hide on') or compound\n -- ('Spacing') one needs the inline\n -- DesignProperties: [...] form, because a\n -- SET assignment carries one value.\n SET Action = MICROFLOW Module.MF ON btnSave; -- any CREATE PAGE action form\n SET 'createFileAction' = MICROFLOW Module.MF ON fileUploader1;\n -- a pluggable widget's NAMED action slot,\n -- by the widget's own key; refused on a\n -- key that is not action-typed\n SET DataSource = $Param ON dvOrder; -- parameter/microflow/nanoflow/selection;\n -- DATABASE and association are REPLACE-only,\n -- and a data view takes no database source\n SET (prop1 = val1, prop2 = val2) ON widgetName;\n SET Title = 'New Title'; -- page-level (case-sensitive)\n SET Documentation = 'What this page is for.';\n SET Class = 'css-class'; -- page-level CSS class / style\n SET Style = 'css: rule';\n SET PopupWidth = 800; -- page-level pop-up dimensions\n SET PopupHeight = 480;\n SET PopupResizable = true;\n INSERT AFTER widgetName { <widgets> };\n INSERT BEFORE widgetName { <widgets> };\n INSERT INTO containerName { <widgets> };\n DROP WIDGET name1, name2;\n DROP TEMPLATE FOR Module.Specialization IN listViewName;\n REPLACE widgetName WITH { <widgets> };\n};\n\n-- The BULK form: one design property on every widget of a TYPE.\nALTER PAGES [IN Module]\n SET 'Compact' = ON, 'Striped' = ON\n WHERE WIDGETTYPE = datagrid -- the MDL keyword, which resolves to\n -- exactly one widget id. A full id in\n -- quotes works too. NOT a name: a widget\n -- name is unique only within its page.\n [DRY RUN]; -- run this FIRST. It reports the matches\n -- against a discardable copy and writes\n -- nothing.",
287+
Syntax: "ALTER PAGE Module.Name {\n SET property = value ON widgetName; -- widget property names: any casing\n SET 'Row size' = 'Small' ON lvOrders; -- an Atlas DESIGN property of that widget's\n -- type; quoted and case-sensitive.\n -- `show design properties for <type>` lists\n -- them. ON/OFF for a toggle, where OFF\n -- REMOVES the entry.\n -- A multi-select ('Hide on') or compound\n -- ('Spacing') one needs the inline\n -- DesignProperties: [...] form, because a\n -- SET assignment carries one value.\n SET Action = MICROFLOW Module.MF ON btnSave; -- any CREATE PAGE action form\n SET 'createFileAction' = MICROFLOW Module.MF ON fileUploader1;\n -- a pluggable widget's NAMED action slot,\n -- by the widget's own key; refused on a\n -- key that is not action-typed\n SET DataSource = $Param ON dvOrder; -- parameter/microflow/nanoflow/selection;\n -- DATABASE and association are REPLACE-only,\n -- and a data view takes no database source\n SET RenderMode = H2 ON txtTitle; -- dynamic text: Text | Paragraph | H1..H6\n SET (prop1 = val1, prop2 = val2) ON widgetName;\n SET Title = 'New Title'; -- page-level (case-sensitive)\n SET Documentation = 'What this page is for.';\n SET Class = 'css-class'; -- page-level CSS class / style\n SET Style = 'css: rule';\n SET PopupWidth = 800; -- page-level pop-up dimensions\n SET PopupHeight = 480;\n SET PopupResizable = true;\n INSERT AFTER widgetName { <widgets> };\n INSERT BEFORE widgetName { <widgets> };\n INSERT INTO containerName { <widgets> };\n DROP WIDGET name1, name2;\n DROP TEMPLATE FOR Module.Specialization IN listViewName;\n REPLACE widgetName WITH { <widgets> };\n};\n\n-- The BULK form: one design property on every widget of a TYPE.\nALTER PAGES [IN Module]\n SET 'Compact' = ON, 'Striped' = ON\n WHERE WIDGETTYPE = datagrid -- the MDL keyword, which resolves to\n -- exactly one widget id. A full id in\n -- quotes works too. NOT a name: a widget\n -- name is unique only within its page.\n [DRY RUN]; -- run this FIRST. It reports the matches\n -- against a discardable copy and writes\n -- nothing.",
288288
Example: "ALTER PAGE Module.EditPage {\n SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave;\n INSERT AFTER txtName {\n TEXTBOX txtMiddleName (Label: 'Middle Name', Attribute: MiddleName)\n };\n DROP WIDGET txtUnused;\n};",
289289
SeeAlso: []string{"page.create", "page.show", "snippet.alter"},
290290
})

‎docs-site/src/language/alter-page.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ ALTER PAGE Module.EditPage {
4040
|----------|-------------|---------|
4141
| `Caption` | Button/link caption | `SET Caption = 'Submit' ON btnSave` |
4242
| `Label` | Input field label | `SET Label = 'Full Name' ON txtName` |
43+
| `RenderMode` | Dynamic text rendering: `Text`, `Paragraph`, `H1`–`H6` | `SET RenderMode = H2 ON txtTitle` |
4344
| `ButtonStyle` | Button visual style | `SET ButtonStyle = Danger ON btnDelete` |
4445
| `Class` | CSS class names | `SET Class = 'card p-3' ON cMain` |
4546
| `Style` | Inline CSS | `SET Style = 'margin: 8px;' ON cBox` |

‎docs/01-project/MDL_QUICK_REFERENCE.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1645,6 +1645,7 @@ Modify an existing page or snippet's widget tree in-place without full `create o
16451645
| Documentation | `set Documentation = 'What this page is for.'` | Page-level. Same property the `/** … */` doc comment on `CREATE PAGE` writes, so an existing page can be documented without restating it. `''` clears it |
16461646
| Pop-up dimensions | `set PopupWidth = 800` / `set PopupHeight = 480` / `set PopupResizable = true` | Page-level; apply when the page opens in a pop-up |
16471647
| Page CSS class / style | `set Class = 'css-class'` / `set Style = 'css: rule'` | Page-level (no ON clause); sets the page's Appearance |
1648+
| Dynamic text heading level | `set RenderMode = H2 on txtTitle` | Dynamic text only: Text, Paragraph, H1–H6 (any casing). Any other widget or value is an error naming the allowed set |
16481649
| Widget dynamic classes | `set DynamicClasses = 'expr' on widgetName` | Runtime-computed classes on a widget — the surgical alternative to a bulk `update widgets` |
16491650
| Insert after | `insert after widgetName { widgets }` | Add widgets after target |
16501651
| Insert before | `insert before widgetName { widgets }` | Add widgets before target |
@@ -1661,7 +1662,7 @@ Modify an existing page or snippet's widget tree in-place without full `create o
16611662
| Set layout | `set layout = Module.LayoutName` | Change page layout, auto-maps placeholders |
16621663
| Set layout + map | `set layout = Module.Layout map (Old as New)` | Explicit placeholder mapping |
16631664

1664-
**Supported SET properties:** Caption, Label, ButtonStyle, Class, Style, DynamicClasses, Editable, Visible, Name, Title (page-level), Documentation (page-level), Layout (page-level), PopupWidth / PopupHeight / PopupResizable (page-level), and quoted pluggable widget properties.
1665+
**Supported SET properties:** Caption, Label, ButtonStyle, Class, Style, DynamicClasses, RenderMode (dynamic text), Editable, Visible, Name, Title (page-level), Documentation (page-level), Layout (page-level), PopupWidth / PopupHeight / PopupResizable (page-level), and quoted pluggable widget properties.
16651666

16661667
**Example:**
16671668
```sql
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
-- ============================================================================
2+
-- ALTER PAGE / ALTER SNIPPET: SET RenderMode on a dynamic text
3+
-- ============================================================================
4+
--
5+
-- Reported:
6+
--
7+
-- alter page MyModule.Comparison_Page { set RenderMode = H2 on compTitle }
8+
-- -> failed to set RenderMode on compTitle: property "RenderMode" not found
9+
-- (widget has no pluggable Object)
10+
--
11+
-- (current main words the same refusal as "not a property of this built-in
12+
-- widget … use alter styling", which is no more true: RenderMode is not a
13+
-- design property either).
14+
--
15+
-- `create page … dynamictext x (RenderMode: H1)` and `replace x with { … }`
16+
-- both accept it, because RenderMode is a first-class Forms$DynamicText
17+
-- property. The ALTER SET switch in setRawWidgetPropertyMut simply had no case
18+
-- for it, so it fell through to the pluggable-property setter.
19+
--
20+
-- Values: Text, Paragraph, H1..H6 (matched case-insensitively, stored in the
21+
-- canonical spelling). Anything else is refused by both `check -p` and `exec`
22+
-- — e.g. `set RenderMode = H7 on title` →
23+
-- invalid RenderMode "H7" for dynamic text "title": expected one of Text,
24+
-- Paragraph, H1, H2, H3, H4, H5, H6
25+
--
26+
-- Measured on a copy of a real Mendix 11.14.0 app: exec → "Altered page",
27+
-- describe shows RenderMode: H2, mx check → 0 errors; exactly one .mxunit
28+
-- changed.
29+
--
30+
-- Usage:
31+
-- mxcli exec mdl-examples/bug-tests/alter-page-set-rendermode-dynamictext.mdl -p app.mpr
32+
-- ============================================================================
33+
34+
create entity MyFirstModule.RmRow ( Name: String );
35+
36+
create or replace page MyFirstModule.P_SetRenderMode
37+
(
38+
Title: 'Set RenderMode',
39+
Layout: Atlas_Core.Atlas_Default,
40+
Params: { $Row: MyFirstModule.RmRow }
41+
)
42+
{
43+
dataview dv (datasource: $Row) {
44+
dynamictext title (content: 'Title {1}', contentparams: [{1} = Name], rendermode: H1)
45+
dynamictext body (content: 'Body')
46+
}
47+
}
48+
49+
alter page MyFirstModule.P_SetRenderMode {
50+
set RenderMode = H2 on title;
51+
set rendermode = paragraph on body;
52+
}
53+
54+
create or replace snippet MyFirstModule.S_SetRenderMode
55+
{
56+
dynamictext snipTitle (content: 'Snippet title', rendermode: H3)
57+
}
58+
59+
alter snippet MyFirstModule.S_SetRenderMode {
60+
set RenderMode = H4 on snipTitle;
61+
}
62+
63+
describe page MyFirstModule.P_SetRenderMode;
64+
describe snippet MyFirstModule.S_SetRenderMode;

0 commit comments

Comments
 (0)