Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-10-20","symptom":"`column (Attribute: Name, Filterable: true)` on a Data grid 2: the original report was a silent drop; on current main check refuses it, but with a project the message lists the column's properties without the `filter` slot, and without one it suggests `did you mean FilterType?` — a key the column does not have","cause":"The refusal (MDL-WIDGET01 / MDL-WIDGET07) built its hint from property names only: the item's widgets slots (`content`, `filter`) are not properties, so neither the property list nor nearestKey over the builtin union could name the slot the author was reaching for","file":"`mdl/executor/validate_widgets.go` (`widgetSlotHint`), `mdl/executor/validate_widget_embedded_schema.go` (`widgetSlots`)","insight":"Re-run an old silent-drop report on main before fixing it: #177 had already become a refusal, and what was left was the message. A did-you-mean over property names cannot reach a slot, so a key whose prefix is a slot key (`Filterable` → `filter`) needs its own answer naming the body form and the widget types the slot routes (itemSlotAcceptedChildTypes, already per widget). Measured on Mendix 11.13.0: the suggested `column (Attribute: X) { textfilter tf }` builds 0 errors. Control: both new assertions fail on main with `its properties are attribute, header (Caption)` and `did you mean FilterType`","refs":["mendixlabs/mxcli#177","#1230"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-10-20","symptom":"`container ctnMain (RenderMode: 'Main')` in `create or modify layout` passes check and exec, then `describe layout` prints `container ctnMain (Class: 'app-main')` — reported as \"the value isn't stored\"; `alter layout … set (RenderMode: …)` refused with `property \"RenderMode\" is not a property of this built-in widget`","cause":"The write was fine: `bson dump` showed `RenderMode: \"Main\"` on the stored Forms$DivContainer. DESCRIBE's DivContainer branch never read RenderMode (only DynamicText did), the ALTER `rendermode` case routed only dynamic texts and sent containers to the pluggable setter, and buildContainerV3 cast any string straight to the enum, so `'main'` or `'Banner'` was stored verbatim","file":"`mdl/executor/cmd_pages_describe_parse.go`, `cmd_pages_describe_output.go`, `cmd_pages_builder_v3_layout.go` (`buildContainerV3`), `mdl/backend/pagemutator/mutator.go` (`setContainerRenderModeMut`), `sdk/pages/pages_widgets_container.go` (`CanonicalContainerRenderMode`)","insight":"A report that a value \"isn't stored\" is usually read off `describe`; run `bson dump` before touching the write path — here it split a suspected write drop into a read-side gap in one command, and the builder had been right all along. The sdk enum had drifted too (`Div`, `Form`; Mendix's set is Div…Address in modelsdk/gen/pages), so take the member list from the generated enum rather than the hand-written one. Control: the four new tests fail on main with `describe dropped the container's RenderMode`, `RenderMode = \"main\", want Main`, `'Banner' must be refused` and `property \"RenderMode\" not found on this widget`","refs":["mendixlabs/mxcli#1582","#1487"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area":"mdl/executor","date":"2026-10-20","symptom":"Marketplace Calendar 2.6.1 page passes check and exec, then `mx check` fails `[CE0642] \"Property 'Editable' is required.\"` and `\"Property 'Show event date range' is required.\"`; `editable: 'true'` is accepted with no warning and changes nothing, `showEventDate:` warns MDL-WIDGET06 \"will be dropped\" although it is written, and `describe page` prints neither","cause":"Four hops, each dropping the same value: (1) GenerateFromMPK's createDefaultWidgetValue had no Expression case, so `defaultValue=\"true\"` was written as an empty expression; (2) GenerateDefJSON emitted no mapping for a top-level `expression` property, leaving it in KnownProperties (MDL-WIDGET06) and written only by the explicit pass; (3) that pass skips builtin names, and `editable` is also the widget-level editability, which the Calendar's package does not declare — so the widget's own `editable` was unreachable in every spelling; (4) extractExplicitProperties had no Expression branch","file":"`modelsdk/widgets/augment.go` (`createDefaultWidgetValue`), `mdl/executor/widget_defs.go` (`GenerateDefJSON`, generator v19), `mdl/executor/widget_engine.go` (`resolveMapping`, `applyOperation`), `mdl/executor/pluggable_system_props.go` (`editableIsOwnProperty`), `mdl/executor/cmd_pages_describe_pluggable.go`","insight":"A builtin MDL key and a widget's own property of the same name collide silently: the builtin always wins, and when the widget does not declare the matching system property the builtin is a no-op too. Decide the routing in the def generator, which reads the .mpk's system properties, so check and build read one answer. The cheapest probe was the reporter's table — `showEventDate:` made the CE0642 go away while `editable:` did not, which already says the explicit pass writes expressions and something else eats `editable`. Make the template write declared defaults first: that alone makes every Calendar page build, before any MDL names the property. Measured on Mendix 11.13.0 with Calendar 245304 v2.6.1: main → 2× CE0642; this branch → 0 errors with neither, both 'true' and both false/'false', and describe → exec reports Unchanged","refs":["mendixlabs/mxcli#1578","#1483"]}
16 changes: 16 additions & 0 deletions docs-wiki/bug-patterns/silent-property-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,22 @@ model holding a value that the tool reports as absent — the same lossy round t
wearing the right answer. Several findings here are the follow-up half of a fix
that shipped write-only.

**"Not stored" is usually read off `describe` — check the store first.** A
report that a value was dropped is almost always an inference from `describe`
printing nothing, and `describe` is a separate hop that can be the only one
missing. A container's `RenderMode` was on disk all along; only the read side
had never learnt it. One `bson dump` splits a write drop from a read gap before
any code is opened, and points the fix at the right half.

**A builtin key can shadow a widget's own property of the same name.** The
universal names (`Editable`, `Visible`, …) win over a pluggable widget's own
key, and where the package declares no matching system property the builtin is
a no-op as well — so the widget's own property is unreachable in every
spelling, with nothing to warn about because the key is "known". Which meaning
a key has is a fact of the package (its system properties), so decide it where
the package is read — the definition generator — and let check and build both
read that one answer.

**A half-wired feature is worse than a missing one.** When the grammar already
accepts a form, scripts using it look correct and produce models without it.
Before assuming a spelling works, follow the whole chain: AST → model → both
Expand Down
52 changes: 52 additions & 0 deletions mdl-examples/bug-tests/1578-calendar-expression-properties.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-- mendixlabs/mxcli#1578: a page with the Marketplace Calendar (content 245304)
-- passed `check` and `exec`, then `mx check` failed with
-- [CE0642] "Property 'Editable' is required." at Calendar 'CalRepro_Calendar'
-- [CE0642] "Property 'Show event date range' is required."
-- Both are `type="expression" defaultValue="true"` in Calendar.xml. The
-- template wrote them empty, `editable:` only ever reached the widget-level
-- editability (which the Calendar does not declare), `showEventDate:` drew a
-- false MDL-WIDGET06, and `describe page` printed neither.
-- After the fix an unnamed one gets its declared default, `editable:` and
-- `showEventDate:` are written in every spelling, and describe prints both.
-- Needs: `mxcli marketplace install 245304 -p <app>` first.
mdl 1;

create module BugTest1578;

create persistent entity BugTest1578.LeaveRequest (
Reason: String(200),
StartDate: DateTime,
EndDate: DateTime
);

-- Neither property named: both get their default, `mx check` is clean.
create or modify page BugTest1578.CalDefaults (Title: 'Calendar', Layout: Atlas_Core.Atlas_Default) {
pluggablewidget 'com.mendix.widget.web.calendar.Calendar' calDefaults (
databaseDataSource: database BugTest1578.LeaveRequest,
titleType: 'attribute',
titleAttribute: Reason,
startAttribute: StartDate,
endAttribute: EndDate,
view: 'standard',
defaultViewStandard: 'month'
)
};

-- Both named with a non-default value.
create or modify page BugTest1578.CalReadOnly (Title: 'Calendar', Layout: Atlas_Core.Atlas_Default) {
pluggablewidget 'com.mendix.widget.web.calendar.Calendar' calReadOnly (
databaseDataSource: database BugTest1578.LeaveRequest,
titleType: 'attribute',
titleAttribute: Reason,
startAttribute: StartDate,
endAttribute: EndDate,
view: 'standard',
defaultViewStandard: 'month',
editable: false,
showEventDate: 'false'
)
};

-- Expect: editable: 'true', showEventDate: 'true' — and 'false' on the second.
describe page BugTest1578.CalDefaults;
describe page BugTest1578.CalReadOnly;
33 changes: 33 additions & 0 deletions mdl-examples/bug-tests/1582-container-rendermode.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-- mendixlabs/mxcli#1582: `container ctnMain (RenderMode: 'Main')` in a layout or
-- page was stored, but `describe` never printed it — so it read as dropped, and
-- a describe → exec round trip did drop it. `alter layout … set (RenderMode: …)`
-- was refused with `property "RenderMode" is not a property of this built-in
-- widget`, and an unknown value (`'Banner'`, or `'main'` as typed) was written
-- verbatim into the document.
-- After the fix describe prints `RenderMode: Main`, ALTER sets it, the value is
-- stored in its canonical spelling, and an unknown member is refused.
mdl 1;

create module BugTest1582;

create or modify layout BugTest1582.L_Landmarks (
layouttype: 'Responsive'
) {
scrollcontainer layoutContainer {
region center {
container ctnMain (RenderMode: 'Main', Class: 'app-main') {
placeholder Main
}
container ctnNav (RenderMode: nav) {
placeholder Nav
}
}
}
};

alter layout BugTest1582.L_Landmarks {
set (RenderMode: 'Header') on ctnNav;
};

-- Expect: ctnMain (RenderMode: Main, …) and ctnNav (RenderMode: Header).
describe layout BugTest1582.L_Landmarks;
27 changes: 27 additions & 0 deletions mdl-examples/bug-tests/177-datagrid-column-filter-slot.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- mendixlabs/mxcli#177: `column (Attribute: Name, Filterable: true)` was
-- accepted by check and exec and silently dropped — a Data grid 2 column has no
-- Filterable property. Its filter is the column's `filter` widgets slot.
-- check now refuses `Filterable:` (MDL-WIDGET01 with -p, MDL-WIDGET07 without)
-- and the message names the slot and the filter widgets it takes, instead of
-- listing the column's properties or suggesting `FilterType`.
-- This script is the form the message points at; it builds with 0 errors.
mdl 1;

create module BugTest177;

create persistent entity BugTest177.Order (
OrderNumber: String(20),
Amount: Decimal,
PlacedOn: DateTime
);

create or modify page BugTest177.Order_Overview (Title: 'Orders', Layout: Atlas_Core.Atlas_Default) {
datagrid dgOrders (DataSource: database BugTest177.Order) {
column (Attribute: OrderNumber) { textfilter tfNumber }
column (Attribute: Amount) { numberfilter nfAmount }
column (Attribute: PlacedOn) { datefilter dfPlacedOn }
}
};

-- Expect: each column describes back with its filter widget in its braces.
describe page BugTest177.Order_Overview;
46 changes: 46 additions & 0 deletions mdl/backend/pagemutator/container_rendermode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: Apache-2.0

package pagemutator

import (
"testing"

"go.mongodb.org/mongo-driver/bson"

"github.com/mendixlabs/mxcli/mdl/backend/bsonnav"
)

// `alter layout … set (RenderMode: 'Main') on ctnMain` was refused with
// "property \"RenderMode\" is not a property of this built-in widget", although
// CREATE writes Forms$DivContainer.RenderMode (mendixlabs/mxcli#1582).
func TestSetWidgetProperty_ContainerRenderMode(t *testing.T) {
for _, tc := range []struct{ in, want string }{{"Main", "Main"}, {"nav", "Nav"}, {"Div", "Div"}} {
rawData := makeRawPage(bson.D{
{Key: "$Type", Value: "Forms$DivContainer"},
{Key: "Name", Value: "ctnMain"},
{Key: "RenderMode", Value: "Div"},
})
m := &Mutator{rawData: rawData, widgetFinder: findBsonWidget}
if err := m.SetWidgetProperty("ctnMain", "RenderMode", tc.in); err != nil {
t.Fatalf("SetWidgetProperty(RenderMode=%s): %v", tc.in, err)
}
if got := bsonnav.DGetString(findBsonWidget(rawData, "ctnMain").widget, "RenderMode"); got != tc.want {
t.Errorf("RenderMode = %q, want %q", got, tc.want)
}
}
}

func TestSetWidgetProperty_ContainerRenderModeInvalid(t *testing.T) {
rawData := makeRawPage(bson.D{
{Key: "$Type", Value: "Forms$DivContainer"},
{Key: "Name", Value: "ctnMain"},
{Key: "RenderMode", Value: "Main"},
})
m := &Mutator{rawData: rawData, widgetFinder: findBsonWidget}
if err := m.SetWidgetProperty("ctnMain", "RenderMode", "H1"); err == nil {
t.Fatal("RenderMode H1 on a container must be refused")
}
if got := bsonnav.DGetString(findBsonWidget(rawData, "ctnMain").widget, "RenderMode"); got != "Main" {
t.Errorf("a refused value changed the stored RenderMode to %q", got)
}
}
24 changes: 24 additions & 0 deletions mdl/backend/pagemutator/mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3264,6 +3264,9 @@ func setRawWidgetPropertyMut(widget bson.D, propName string, value any) error {
if isDynamicTextWidget(widget) {
return setDynamicTextRenderModeMut(widget, value)
}
if typ := bsonnav.DGetString(widget, "$Type"); typ == "Forms$DivContainer" || typ == "Pages$DivContainer" {
return setContainerRenderModeMut(widget, value)
}
return setPluggableWidgetPropertyMut(widget, propName, value)
default:
// Try as pluggable widget property
Expand Down Expand Up @@ -3320,6 +3323,27 @@ func setDynamicTextRenderModeMut(widget bson.D, value any) error {
fmt.Sprint(value), bsonnav.DGetString(widget, "Name"), strings.Join(names, ", "))
}

// setContainerRenderModeMut writes a container's RenderMode — the HTML element
// it renders as — in place, canonically spelled; an unknown member is refused
// before the document is touched (mendixlabs/mxcli#1582).
func setContainerRenderModeMut(widget bson.D, value any) error {
s, _ := value.(string)
mode, ok := pages.CanonicalContainerRenderMode(s)
if !ok {
names := make([]string, len(pages.ContainerRenderModes))
for i, m := range pages.ContainerRenderModes {
names[i] = string(m)
}
return fmt.Errorf("invalid RenderMode %q for container %q: expected one of %s",
fmt.Sprint(value), bsonnav.DGetString(widget, "Name"), strings.Join(names, ", "))
}
if !bsonnav.DSet(widget, "RenderMode", string(mode)) {
return fmt.Errorf("container %q has no stored RenderMode property to set",
bsonnav.DGetString(widget, "Name"))
}
return nil
}

// setLegacyImageRefMut writes a static image's Image or a dynamic image's
// DefaultImage: the qualified name of an image-collection entry as a plain
// string ("" when unset), the key and shape CREATE writes and DESCRIBE reads
Expand Down
2 changes: 1 addition & 1 deletion mdl/executor/cmd_pages_builder_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ func (pb *pageBuilder) buildPluggable(def *WidgetDefinition, w *ast.WidgetV3) (p
if err := applyWidgetAppearance(widget, w, pb.themeRegistry); err != nil {
return nil, err
}
if err := applyPluggableSystemSettings(widget, w); err != nil {
if err := applyPluggableSystemSettings(widget, w, def); err != nil {
return nil, err
}
if err := pb.applyVisibleWhen(widget, w); err != nil {
Expand Down
14 changes: 12 additions & 2 deletions mdl/executor/cmd_pages_builder_v3_layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,19 @@ func (pb *pageBuilder) buildContainerV3(w *ast.WidgetV3) (*pages.Container, erro
},
}

// Handle RenderMode
// Handle RenderMode. The codec stores it verbatim, so an unknown member is
// refused here rather than written into a document Studio Pro cannot open.
if rm := w.GetRenderMode(); rm != "" {
container.RenderMode = pages.ContainerRenderMode(rm)
mode, ok := pages.CanonicalContainerRenderMode(rm)
if !ok {
names := make([]string, len(pages.ContainerRenderModes))
for i, m := range pages.ContainerRenderModes {
names[i] = string(m)
}
return nil, mdlerrors.NewValidationf("container %q: unknown RenderMode %q — valid modes are %s",
w.Name, rm, strings.Join(names, ", "))
}
container.RenderMode = mode
}

// Handle Action / OnClick property — a Container is clickable in Mendix via
Expand Down
6 changes: 5 additions & 1 deletion mdl/executor/cmd_pages_describe_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,11 @@ func outputWidgetMDLV3(ctx *ExecContext, w rawWidget, indent int) {

case "Forms$DivContainer", "Pages$DivContainer":
header := fmt.Sprintf("container %s", mdlIdent(w.Name))
props := appendAppearanceProps(ctx, nil, w)
var props []string
if w.RenderMode != "" && w.RenderMode != "Div" {
props = append(props, fmt.Sprintf("RenderMode: %s", w.RenderMode))
}
props = appendAppearanceProps(ctx, props, w)
if w.Action != "" {
props = append(props, actionProp("Action", w.Action))
}
Expand Down
4 changes: 4 additions & 0 deletions mdl/executor/cmd_pages_describe_parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ func parseRawWidget(ctx *ExecContext, w map[string]any, parentEntityContext ...s
if onClick := asActionMap(w["OnClickAction"]); onClick != nil {
widget.Action = extractButtonAction(ctx, map[string]any{"Action": onClick})
}
// The HTML element it renders as (mendixlabs/mxcli#1582).
if rm, ok := w["RenderMode"].(string); ok {
widget.RenderMode = rm
}
}
// Extract GroupBox-specific properties
if typeName == "Forms$GroupBox" || typeName == "Pages$GroupBox" {
Expand Down
16 changes: 16 additions & 0 deletions mdl/executor/cmd_pages_describe_pluggable.go
Original file line number Diff line number Diff line change
Expand Up @@ -1313,6 +1313,22 @@ func extractExplicitProperties(ctx *ExecContext, w map[string]any) []rawExplicit
continue
}

// An expression property: its text, quoted. This path had no branch for
// one, so a stored expression was described as absent — the Calendar's
// `editable` and `showEventDate` (mendixlabs/mxcli#1578). Only an
// Expression-typed property is read: a value of another type keeps
// whatever its PrimitiveValue says below.
if valueTypes[typePointerID] == "Expression" {
if expr := extractString(value["Expression"]); expr != "" {
result = append(result, rawExplicitProp{
Key: propKey,
Value: expr,
ValueType: "Expression",
})
}
continue
}

// Check for a PrimitiveValue.
//
// Booleans used to be dropped here as "common defaults". They are not:
Expand Down
Loading
Loading