Skip to content

Commit 9f2cbcf

Browse files
committed
fix(microflows): read and describe a show page's title override
show page ... with Title = '...' wrote FormSettings.TitleOverride but the reader never read it, so describe dropped it and a describe->exec round trip lost the per-mode popup title. Read it from raw and print it. Field run: ProcureFlow (Mendix 11.14.0) scratch copy — a NAV microflow opening Procurement.Department_NewEdit with Title 'New department': describe prints with Title, re-executing the describe output keeps it, mx check 0 errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5dJ4raEjbm8bFiKeYtchS
1 parent 9509176 commit 9f2cbcf

5 files changed

Lines changed: 97 additions & 0 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-26", "symptom": "`show page M.P($X = $X) with Title = 'New vendor';` in a microflow: stored TitleOverride is correct and mx check passes, but `describe microflow` prints the show page without `with Title`, and re-executing the describe output drops the override", "cause": "actionFromGen's ShowPageAction case reads FormSettings from raw (Form, ParameterMappings) and never looked at TitleOverride, so OverridePageTitle was always nil on read; the formatter had no clause for it either.", "file": "mdl/backend/modelsdk/microflow_read_actions.go", "insight": "#812 fixed the WRITE half of TitleOverride (null vs empty template) and pinned it with an encode test; the READ half had no test, so the field was write-only. For every field a write-path fix touches, add the encode→decode→actionFromGen round trip (the real describe path) in the same change.", "rules": []}

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1414

1515
### Fixed
1616

17+
- **A microflow `show page … with Title = '…'` lost its title override on the way back** — the title (Studio Pro's "Override page title", how one popup shows "New vendor" or "Edit vendor" per mode) was written, but the reader never read `FormSettings.TitleOverride`, so `describe microflow` printed the statement without it and a describe→exec round trip dropped it. It is read back and printed now.
1718
- **Keyword operators were fused in some stored expressions** — a page action's microflow argument `Flag: $a and $b` was stored as `$aand$b`, `if $x then 'a' else 'b'` as `if$xthen'a'else'b'`; the same in `contentparams` values, `send rest request … with (…)` parameters and a dynamic `execute database query`. Those four places took the expression's text without its whitespace; literals, `+` and `$currentObject` were unaffected, which is why it went unnoticed, and `check --references` passed. Measured by decoding the stored units on a Mendix 11.14.0 project. The expression is now stored as written, with MDL comments removed, as the microflow expression sites already did.
1819
- **An input bound to an attribute outside any data container was written with no binding** — `textbox t (Attribute: FullName)` at the top of a page (or of a snippet, or inside a plain container) has no entity to qualify the name with, and the writer stored it as `AttributeRef: null`. Plain `check` passed, `exec --no-check` and `alter page … insert` at page level said success, and mxbuild 11.13.0 failed the page with CE0544 "This widget can only function inside a data context" + CE7005 (text box, text area, date picker, check box, radio buttons, drop-down), CE0402 (dynamic text `Attribute:`) or CE0642 (combo box). A qualified attribute there is stored and fails the same way (CE0544 / CE2421 / CE1365 / CE7247 "Move this widget into a data container"). `Attribute: $P/Name` and `Attribute: $currentObject/Name` never parsed as an attribute path and were dropped even inside a data view. `check` now reports all three as **MDL-WIDGET34** (no project needed), and the page builder refuses them with the widget named, so nothing is written; `alter page` refuses a bare name it has no entity for. Place the widget in a data view, list view, gallery or data grid and bind the attribute by name.
1920
- **An expression property written in brackets was silently dropped** (mendixlabs/mxcli#750) — `dynamicclasses: [ if $currentObject/Featured then 'a' else 'b' ]`, the spelling #750 proposes, parsed as a list that no writer reads: `check` was clean, `exec` said `Created page`, and the widget was stored with no dynamic class. `alter page … set DynamicClasses = [ … ]` said `Altered page` and changed nothing, and a column's `DynamicCellClass` stored the list's text — tokens fused, `[if$x/Ythen'a'else'b']` — as its expression. Measured on a copy of a Mendix 11.14.0 project with the pre-fix binary. `mxcli check` now reports **MDL-WIDGET32** for `DynamicClasses` and `DynamicCellClass` written as a list (no project needed), and ALTER refuses it, so `check -p` reports that too. Write the expression quoted.

‎mdl/backend/modelsdk/microflow_read_actions.go‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,13 @@ func actionFromGen(el element.Element) microflows.MicroflowAction {
287287
out.PageParameterMappings = append(out.PageParameterMappings, pm)
288288
}
289289
}
290+
// "Override page title": a Microflows$TextTemplate whose Text holds
291+
// the translations. Null when the page keeps its own title. Not
292+
// reading it made DESCRIBE lose `with Title = '…'` and a rewrite
293+
// through the reader drop the override.
294+
if tt, ok := fs.Lookup("TitleOverride").DocumentOK(); ok {
295+
out.OverridePageTitle = rawTextsText(tt.Lookup("Text"))
296+
}
290297
}
291298
return out
292299

@@ -1485,3 +1492,32 @@ func queueSettingsFromRaw(raw bson.Raw) *microflows.QueueSettings {
14851492
}
14861493
return qs
14871494
}
1495+
1496+
// rawTextsText reads a raw Texts$Text value (Items: [marker, Translation…])
1497+
// into a model.Text, or nil when it holds no translation.
1498+
func rawTextsText(v bson.RawValue) *model.Text {
1499+
doc, ok := v.DocumentOK()
1500+
if !ok {
1501+
return nil
1502+
}
1503+
arr, ok := doc.Lookup("Items").ArrayOK()
1504+
if !ok {
1505+
return nil
1506+
}
1507+
vals, _ := arr.Values()
1508+
trans := map[string]string{}
1509+
for _, it := range vals {
1510+
td, ok := it.DocumentOK()
1511+
if !ok {
1512+
continue // the leading list marker
1513+
}
1514+
trans[rawStr(td, "LanguageCode")] = rawStr(td, "Text")
1515+
}
1516+
if len(trans) == 0 {
1517+
return nil
1518+
}
1519+
return &model.Text{
1520+
BaseElement: model.BaseElement{ID: model.ID(rawStr(doc, "$ID")), TypeName: "Texts$Text"},
1521+
Translations: trans,
1522+
}
1523+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package modelsdkbackend
4+
5+
import (
6+
"testing"
7+
8+
"go.mongodb.org/mongo-driver/v2/bson"
9+
10+
"github.com/mendixlabs/mxcli/model"
11+
"github.com/mendixlabs/mxcli/modelsdk/codec"
12+
"github.com/mendixlabs/mxcli/sdk/microflows"
13+
)
14+
15+
// `show page M.P($X = $X) with Title = 'New vendor'` writes FormSettings.
16+
// TitleOverride — Studio Pro's "Override page title", the way one popup gets a
17+
// per-mode title — but the read path never read it back. DESCRIBE therefore
18+
// printed the statement without `with Title`, and any rewrite that went through
19+
// the reader (a describe→exec round trip) silently dropped the override.
20+
func TestActionFromGen_ShowPageKeepsTitleOverride(t *testing.T) {
21+
action := &microflows.ShowPageAction{
22+
PageName: "Sales.Vendor_NewEdit",
23+
FormSettingsID: "fs-1",
24+
OverridePageTitle: &model.Text{
25+
BaseElement: model.BaseElement{ID: "txt-1", TypeName: "Texts$Text"},
26+
Translations: map[string]string{"en_US": "New vendor"},
27+
},
28+
}
29+
raw, err := (&codec.Encoder{}).Encode(microflowActionToGen(action))
30+
if err != nil {
31+
t.Fatalf("encode: %v", err)
32+
}
33+
el, err := codec.NewDecoder(codec.DefaultRegistry).Decode(bson.Raw(raw))
34+
if err != nil {
35+
t.Fatalf("decode: %v", err)
36+
}
37+
got, ok := actionFromGen(el).(*microflows.ShowPageAction)
38+
if !ok {
39+
t.Fatalf("read back %T", actionFromGen(el))
40+
}
41+
if got.OverridePageTitle == nil || got.OverridePageTitle.Translations["en_US"] != "New vendor" {
42+
t.Errorf("OverridePageTitle = %+v, want en_US \"New vendor\"", got.OverridePageTitle)
43+
}
44+
45+
// Control: no override reads back as none.
46+
action.OverridePageTitle = nil
47+
raw, _ = (&codec.Encoder{}).Encode(microflowActionToGen(action))
48+
el, _ = codec.NewDecoder(codec.DefaultRegistry).Decode(bson.Raw(raw))
49+
if got := actionFromGen(el).(*microflows.ShowPageAction); got.OverridePageTitle != nil {
50+
t.Errorf("no override stored, read back %+v", got.OverridePageTitle)
51+
}
52+
}

‎mdl/executor/cmd_microflows_format_action.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,13 @@ func formatAction(
760760
if len(params) > 0 {
761761
paramStr = "(" + strings.Join(params, ", ") + ")"
762762
}
763+
// "Override page title" — the per-mode popup title. Printed so a
764+
// describe→exec round trip keeps it.
765+
if a.OverridePageTitle != nil {
766+
if title := pickTextTranslation(a.OverridePageTitle, describeDefaultLanguage(ctx)); title != "" {
767+
return fmt.Sprintf("show page %s%s with Title = %s;", pageName, paramStr, mdlQuote(title))
768+
}
769+
}
763770
return fmt.Sprintf("show page %s%s;", pageName, paramStr)
764771

765772
case *microflows.ClosePageAction:

0 commit comments

Comments
 (0)