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-21", "symptom": "A Data grid 2 control-bar button opening a page with the grid's selection had no spelling that built: `show page M.Detail(OrderParam = $currentObject)` is MDL-BUTTON01 (CE1571, not row-scoped), `OrderParam = $dgOrders` — what MDL-BUTTON01's own hint suggested — was MDL-PAGEARG01 \"a widget's page argument is always the enclosing context object\", and no argument is mx check `[CE1571] \"No argument has been selected for parameter 'OrderParam'\"`", "cause": "mxcli wrote show page arguments only as the inferred empty mapping (#296) or, since #1403, a PageVariable naming a page/snippet parameter; it had no third form, so the guard (correctly, for what could be written) refused `$grid`. The control bar also had no context of its own: the engine builds a widgets slot's children directly (applyChildSlots), never through the `controlbar` node, so a rule hung on that node in buildWidgetV3 is never reached for Data grid 2", "file": "`mdl/executor/cmd_pages_showpage_args.go` (`pageArgContext.selectionOf`, `namesSelection`, `argContextForChildren`, `namedParameterArgument`), `mdl/executor/widget_engine.go` (`applyChildSlots`, `inDataWidget` at the datasource passes), `mdl/backend/modelsdk/widget_write.go` (`pageSettingsWithMappings`, kind \"widget\"), `mdl/executor/cmd_pages_describe_output.go` (`extractPageParameters`)", "insight": "No Studio Pro reference for the shape existed in the corpus (12 PageParameterMappings, none with Variable.Widget — the scanner's control was counting all 12 first, since bson v2 decodes nested docs as bson.D and a first scan silently matched nothing). Three mxbuild 11.12.5 builds of the same script settled the storage: Variable{Widget: grid} with Argument \"\" → 0 errors, Argument \"$grid\" → CE0115, no mapping → CE1571; the microflow path's Expression form does NOT carry over to a page mapping. A runtime check then showed the binding means what it says: the button is disabled until a row is selected and opens the detail with the selected (second) row. Wrong turn to skip: a builder unit test that sets argCtx and calls buildClientActionV3 passed while exec still refused — test the engine's applyChildSlots, where Data grid 2's control bar is actually built", "refs": ["mendixlabs/mxcli#122", "ako/mxcli#1224"], "ce": ["CE1571", "CE0115"], "rules": ["MDL-PAGEARG01", "MDL-BUTTON01"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"area": "mdl/executor", "date": "2026-10-21", "symptom": "Custom chart `eventDataAttribute: EventData` inside a data view over page parameter $Entry (ChartEntry has no EventData) passes `check --references` and `exec`, then mx check reports `[CE1613] \"The selected attribute 'MyFirstModule.ChartEntry.EventData' no longer exists.\"`; written qualified as MyFirstModule.ChartContext.EventData it is stored as such, but `describe page` prints `eventDataAttribute: EventData` and re-running that rebinds it to ChartEntry", "cause": "(1) misboundAttributeError refused a bare name only when ANOTHER entity in scope declared it, and returned nil for a name no entity had (\"a plain typo, which other checks own\") — no check owned it for pluggable properties; (2) extractExplicitProperties shortened every AttributeRef with describeAttr and was given no scope, and dataSourceEntityContext returned a parameter source's NAME (`Entry`) as the context entity, so no consumer could compare it to the attribute's entity", "file": "`mdl/executor/widget_attribute_scope.go` (`misboundAttributeError`), `mdl/executor/cmd_pages_describe_pluggable.go` (`extractExplicitProperties`), `mdl/executor/cmd_pages_describe_datasource.go` (`parseContextSource`, `parameterSourceEntity`), `mdl/executor/cmd_pages_describe_flowcontext.go` (`dataSourceEntityContext`)", "insight": "A guard that stays quiet on the case it cannot place should say which check owns that case — this one named none, and grep found none. The describe half was the same mistake dataSourceEntityContext had already fixed twice (a flow's name, an association's name standing in for the entity): a parameter source stores its entity beside the name (`EntityRef.Entity` on Forms$DataViewSource), so it is read, not looked up from the page's parameters. To force the fault into a build, use a pre-fix binary: exec shares the refusal with check, so `--no-check` no longer writes the broken binding. Remaining: the stored qualified binding without a SourceVariable gets CE7247 from mx check in a fresh app; Studio Pro's page-parameter binding carries a Forms$PageVariable, which MDL still cannot spell for a pluggable attribute property", "refs": ["mendixlabs/mxcli#1591", "ako/mxcli#1502"], "ce": ["CE1613", "CE7247"]}
4 changes: 4 additions & 0 deletions docs-site/src/reference/page/create-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ DATAGRID dgOrders (DataSource: DATABASE FROM Sales.Order, Selection: Single) {
}
```

A `SHOW PAGE` in the control bar takes the selection the same way —
`Action: SHOW PAGE Sales.Order_Detail(Order = $dgOrders)` opens the page with
the selected row, and the button is disabled until a row is selected.

`mxcli check -p <app.mpr>` reports a missing argument before mxbuild does.

### ButtonStyle Values
Expand Down
55 changes: 55 additions & 0 deletions mdl-examples/bug-tests/122-controlbar-show-page-grid-selection.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
mdl 1;
-- ============================================================================
-- Control-bar SHOW PAGE passing the grid's selection (mendixlabs/mxcli#122)
-- ============================================================================
--
-- Symptom (before fix), Mendix 11.12.5:
-- - `Order = $currentObject` in the control bar: MDL-BUTTON01 (CE1571 —
-- a control bar is not row-scoped), whose hint said to pass `$dgOrders`;
-- - `Order = $dgOrders`: MDL-PAGEARG01 "a widget's page argument is always
-- the enclosing context object";
-- - no argument: mx check CE1571 "No argument has been selected for
-- parameter 'OrderParam'".
-- There was no spelling that built.
--
-- Fix: `$dgOrders` in dgOrders' control bar is the grid's selection, stored
-- as an explicit Forms$PageParameterMapping {Argument "", Variable:
-- Forms$PageVariable{Widget "dgOrders"}}. Measured on mxbuild 11.12.5:
-- that shape → 0 errors
-- Argument "$dgOrders" instead → CE0115 "arguments … do not match"
-- no mapping → CE1571
-- In the running app the button is disabled until a row is selected, and
-- opens the detail page with the selected row (checked against the 2nd row).
--
-- Usage:
-- mxcli exec mdl-examples/bug-tests/122-controlbar-show-page-grid-selection.mdl -p app.mpr
-- mxcli docker check -p app.mpr → 0 errors
-- mxcli -p app.mpr -c "describe page BugTest122.Order_Overview"
-- → Action: show page BugTest122.Order_Detail(OrderParam = $dgOrders)
-- ============================================================================

create module BugTest122;

create persistent entity BugTest122.Order (OrderNumber: String(50));

create page BugTest122.Order_Detail (
Title: 'Order',
Layout: Atlas_Core.Atlas_Default,
Params: ( $OrderParam: BugTest122.Order )
) {
dataview dv (DataSource: $OrderParam) {
textbox tbNumber (Label: 'Number', Attribute: OrderNumber)
}
};

create page BugTest122.Order_Overview (
Title: 'Orders',
Layout: Atlas_Core.Atlas_Default
) {
datagrid dgOrders (DataSource: database from BugTest122.Order, Selection: Single) {
controlbar {
actionbutton btnView (Caption: 'View', Action: show page BugTest122.Order_Detail(OrderParam = $dgOrders), ButtonStyle: Primary)
}
column (Attribute: OrderNumber, Caption: 'Order')
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
mdl 1;
-- ============================================================================
-- Pluggable attribute property bound outside the data view entity
-- (mendixlabs/mxcli#1591)
-- ============================================================================
--
-- Symptom (before fix), Mendix 11.12.5:
--
-- 1. `eventDataAttribute: EventData` inside a data view over $Entry
-- (ChartEntry, which has no EventData) passed `check --references` and
-- `exec`; mx check then reported
-- [CE1613] "The selected attribute 'MyFirstModule.ChartEntry.EventData'
-- no longer exists." at Custom chart 'chart1'
-- 2. Written qualified (MyFirstModule.ChartContext.EventData) it was stored
-- as such, but DESCRIBE printed `eventDataAttribute: EventData`, and
-- re-running the description rebound it to ChartEntry — case 1 again.
--
-- Root cause:
-- 1. misboundAttributeError refused a name only when ANOTHER entity in scope
-- had it, and let a name no entity has through as "a typo other checks
-- own" — none did.
-- 2. The generic pluggable describer shortened every attribute, and a data
-- view over a page parameter put the parameter's NAME in scope instead of
-- its entity, so the two could not be compared.
--
-- Usage:
-- mxcli exec mdl-examples/bug-tests/1591-pluggable-attribute-outside-data-view-entity.mdl -p app.mpr
-- mxcli -p app.mpr -c "describe page BugTest1591.ChartPage"
-- → eventDataAttribute: BugTest1591.ChartContext.EventData
-- Changing that line to `eventDataAttribute: EventData` is now refused by
-- check: "`EventData` is not an attribute of BugTest1591.ChartEntry …".
-- ============================================================================

create module BugTest1591;

create non-persistent entity BugTest1591.ChartContext (EventData: String(unlimited));
create non-persistent entity BugTest1591.ChartEntry (JsonData: String(unlimited), JsonLayout: String(unlimited));

create page BugTest1591.ChartPage (
Title: 'Chart',
Layout: Atlas_Core.Atlas_Default,
Params: ( $Ctx: BugTest1591.ChartContext, $Entry: BugTest1591.ChartEntry )
) {
dataview dvEntry (DataSource: $Entry) {
customchart chart1 (
dataAttribute: JsonData,
layoutAttribute: JsonLayout,
eventDataAttribute: BugTest1591.ChartContext.EventData
)
}
};
9 changes: 8 additions & 1 deletion mdl/backend/modelsdk/widget_write.go
Original file line number Diff line number Diff line change
Expand Up @@ -2086,7 +2086,14 @@ func pageSettingsWithMappings(pageName string, mappings []*pages.PageClientParam
assignID(m)
m.SetParameterQualifiedName(pageName + "." + pm.ParameterName)
m.SetArgument("")
m.SetVariable(sourceVariableToGen(strings.TrimPrefix(pm.Variable, "$"), pm.VariableKind))
if pm.VariableKind == "widget" {
// A control-bar button passing its grid's selection names the grid
// in the Widget slot (mendixlabs/mxcli#122). mxbuild 11.12.5: 0
// errors; the empty array is CE1571, Argument "$grid" CE0115.
m.SetVariable(pageVariableToGen(strings.TrimPrefix(pm.Variable, "$"), "", ""))
} else {
m.SetVariable(sourceVariableToGen(strings.TrimPrefix(pm.Variable, "$"), pm.VariableKind))
}
ps.AddParameterMappings(m)
}
return ps
Expand Down
34 changes: 34 additions & 0 deletions mdl/backend/modelsdk/widget_write_showpage_param_mapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,37 @@ func TestClientActionToGen_ShowPageWritesNamedParameterMapping(t *testing.T) {
t.Errorf("inferred argument written as %d explicit mappings, want 0 (#296: CE0115)", len(got))
}
}

// mendixlabs/mxcli#122: a control-bar button passing its grid's selection
// (`$dgOrders`) is the same explicit mapping with the grid in the PageVariable's
// Widget slot. Measured on mxbuild 11.12.5: this shape is 0 errors; the empty
// array is CE1571 "No argument has been selected", and the selection written as
// Argument "$dgOrders" is CE0115 "do not match the expected parameters".
func TestClientActionToGen_ShowPageWritesSelectionMapping(t *testing.T) {
el, err := clientActionToGen(&pages.PageClientAction{
BaseElement: model.BaseElement{ID: "a"},
PageName: "M.Detail",
ParameterMappings: []*pages.PageClientParameterMapping{
{ParameterName: "Order", Variable: "$dgOrders", VariableKind: "widget"},
},
})
if err != nil {
t.Fatalf("clientActionToGen: %v", err)
}
items := el.(*genPg.PageClientAction).PageSettings().(*genPg.PageSettings).ParameterMappingsItems()
if len(items) != 1 {
t.Fatalf("%d mappings written, want 1", len(items))
}
m := items[0].(*genPg.PageParameterMapping)
if m.TypeName() != "Forms$PageParameterMapping" || m.ParameterQualifiedName() != "M.Detail.Order" || m.Argument() != "" {
t.Errorf("mapping $Type=%q Parameter=%q Argument=%q, want Forms$PageParameterMapping, M.Detail.Order, \"\"",
m.TypeName(), m.ParameterQualifiedName(), m.Argument())
}
pv, ok := m.Variable().(*genPg.PageVariable)
if !ok {
t.Fatalf("Variable = %T, want *PageVariable", m.Variable())
}
if pv.WidgetQualifiedName() != "dgOrders" || pv.PageParameterQualifiedName() != "" {
t.Errorf("PageVariable widget=%q page=%q, want dgOrders and \"\"", pv.WidgetQualifiedName(), pv.PageParameterQualifiedName())
}
}
6 changes: 4 additions & 2 deletions mdl/executor/cmd_pages_describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -665,8 +665,10 @@ type rawDataSource struct {
ContextVariable string // association source: context variable name (empty → $currentObject)
// Destination is the entity an association source's last hop arrives at, as
// stored (EntityRef.Steps[last].DestinationEntity). Reference is the
// association there, so this is the entity the source puts in scope. Empty
// when the model does not store it.
// association there, so this is the entity the source puts in scope. A
// parameter source sets it to the parameter's entity (EntityRef.Entity),
// Reference being the parameter's name. Empty when the model does not
// store it.
Destination string
// EntitySteps is set on a database source reached over associations from a
// context object: a Forms$ListViewXPathSource whose EntityRef is an
Expand Down
14 changes: 12 additions & 2 deletions mdl/executor/cmd_pages_describe_datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,16 @@ func entityPathExpr(ds *rawDataSource) string {
return strings.Join(parts, "/")
}

// parameterSourceEntity is the entity a parameter source stores beside the
// parameter's name (a DirectEntityRef), "" when it stores none.
func parameterSourceEntity(ds map[string]any) string {
entityRef, ok := ds["EntityRef"].(map[string]any)
if !ok || extractString(entityRef["$Type"]) != "DomainModels$DirectEntityRef" {
return ""
}
return extractString(entityRef["Entity"])
}

// parseContextSource reads the "data from context" forms: over an association,
// or straight from a page or snippet parameter.
func parseContextSource(ds map[string]any) *rawDataSource {
Expand All @@ -333,13 +343,13 @@ func parseContextSource(ds map[string]any) *rawDataSource {
}
if sv, ok := ds["SourceVariable"].(map[string]any); ok && sv != nil {
if param := extractString(sv["PageParameter"]); param != "" {
return &rawDataSource{Type: "parameter", Reference: param}
return &rawDataSource{Type: "parameter", Reference: param, Destination: parameterSourceEntity(ds)}
}
// Inside a snippet the parameter has its own slot, PageParameter left
// empty; reading PageParameter alone described the data view with no
// DataSource, and re-executing that dropped the binding.
if param := extractString(sv["SnippetParameter"]); param != "" {
return &rawDataSource{Type: "parameter", Reference: param}
return &rawDataSource{Type: "parameter", Reference: param, Destination: parameterSourceEntity(ds)}
}
}
if entityPath := extractString(ds["EntityPath"]); entityPath != "" {
Expand Down
8 changes: 7 additions & 1 deletion mdl/executor/cmd_pages_describe_flowcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ import (
// accounts for the direction travelled (owner to child or back, reference or
// reference set) and for a specialization of the association's end; it is read
// rather than re-derived. A source that stores none keeps the reference.
//
// For a page or snippet parameter the reference is the parameter's name
// (`Entry`), which no consumer can compare with an entity: a pluggable
// property bound to an attribute of another entity described back bare, and
// re-exec bound it to the parameter's entity — CE1613 (mendixlabs/mxcli#1591).
// The parameter's entity, stored beside the name, is used instead.
func dataSourceEntityContext(ctx *ExecContext, ds *rawDataSource) string {
if ds == nil || ds.Reference == "" {
return ""
Expand All @@ -35,7 +41,7 @@ func dataSourceEntityContext(ctx *ExecContext, ds *rawDataSource) string {
if entity := flowReturnEntity(ctx, ds.Type, ds.Reference); entity != "" {
return entity
}
case "association":
case "association", "parameter":
if ds.Destination != "" {
return ds.Destination
}
Expand Down
9 changes: 9 additions & 0 deletions mdl/executor/cmd_pages_describe_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -1813,6 +1813,15 @@ func extractPageParameters(ctx *ExecContext, settings map[string]any) string {
if value == "" {
value = pageVariableArgValue(mappingMap["Variable"])
}
// A PageVariable naming only a widget is a control-bar button passing
// its grid's selection (mendixlabs/mxcli#122), written `$grid`.
if value == "" {
if v, ok := mappingMap["Variable"].(map[string]any); ok {
if w := extractString(v["Widget"]); w != "" {
value = "$" + w
}
}
}

if value != "" {
params = append(params, visitor.ParameterNameSpelling(paramName)+" = "+value)
Expand Down
2 changes: 1 addition & 1 deletion mdl/executor/cmd_pages_describe_parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ func parseRawWidget(ctx *ExecContext, w map[string]any, parentEntityContext ...s
// For generic pluggable widgets (not handled by dedicated extractors above),
// extract all non-default properties as explicit key-value pairs.
if !isKnownCustomWidgetType(widget.RenderMode) {
widget.ExplicitProperties = extractExplicitProperties(ctx, w)
widget.ExplicitProperties = extractExplicitProperties(ctx, w, inheritedCtx)
widget.ObjectLists = extractObjectLists(ctx, w)
widget.ChildSlots = extractChildSlots(ctx, w, widget.EntityContext)
widget.OmittedContainers = unreconstructedContainers(w, widget.ObjectLists, widget.ChildSlots)
Expand Down
19 changes: 17 additions & 2 deletions mdl/executor/cmd_pages_describe_pluggable.go
Original file line number Diff line number Diff line change
Expand Up @@ -1259,7 +1259,12 @@ func isKnownCustomWidgetType(widgetType string) bool {

// extractExplicitProperties extracts non-default property values from a CustomWidget BSON.
// Returns attribute references and primitive values for properties that differ from defaults.
func extractExplicitProperties(ctx *ExecContext, w map[string]any) []rawExplicitProp {
//
// enclosingEntity is the entity of the data container the widget sits in. An
// attribute property linked to no datasource binds to that object, so one of
// another entity keeps its qualified name: shortened, re-exec bound it to the
// container's entity (mendixlabs/mxcli#1591, CE1613).
func extractExplicitProperties(ctx *ExecContext, w map[string]any, enclosingEntity ...string) []rawExplicitProp {
obj, ok := w["Object"].(map[string]any)
if !ok {
return nil
Expand All @@ -1270,6 +1275,11 @@ func extractExplicitProperties(ctx *ExecContext, w map[string]any) []rawExplicit
return nil
}
valueTypes := buildPropertyValueTypeMap(w)
schemas := widgetPropertySchemas(w)
enclosing := ""
if len(enclosingEntity) > 0 {
enclosing = enclosingEntity[0]
}

var result []rawExplicitProp
props := getBsonArrayElements(obj["Properties"])
Expand All @@ -1291,9 +1301,14 @@ func extractExplicitProperties(ctx *ExecContext, w map[string]any) []rawExplicit
// Check for AttributeRef (attribute binding)
if attrRef, ok := value["AttributeRef"].(map[string]any); ok && attrRef != nil {
if attr := extractString(attrRef["Attribute"]); attr != "" {
value := describeAttr(ctx, attr)
if schemas[typePointerID].DataSourceProperty == "" && strings.Contains(enclosing, ".") &&
!strings.EqualFold(attributeEntity(attr), enclosing) {
value = attr
}
result = append(result, rawExplicitProp{
Key: propKey,
Value: describeAttr(ctx, attr),
Value: value,
IsRef: true,
})
continue
Expand Down
Loading
Loading