Skip to content
Open
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": "modelsdk/widgets", "date": "2026-10-22", "symptom": "CE0463 \"The definition of this widget has changed\" on every Marketplace Calendar (2.6.1, 11.12.5) written by mxcli, visible to a plain `mx check` but not to `mxcli docker check`, which runs update-widgets on a temporary copy first. The only difference from an update-widgets copy: the toolbarItems property (type=\"object\" isList=\"true\") stored ValueType.ObjectType null (mendixlabs/mxcli#1632).", "cause": "The .mpk parser read an object property's sub-properties only from `<properties><propertyGroup>`; the Calendar lists them directly under `<properties>`, each with a `<category>` element. PropertyDef.Children stayed empty, so GenerateFromMPK (the Calendar has no embedded template) wrote no nested WidgetObjectType.", "file": "modelsdk/widgets/mpk/mpk.go (xmlProperty.NestedDirectProps and Category, nestedGroups, collectNestedProperties takes a direct child's own <category>); tests modelsdk/widgets/mpk/objectdirect_test.go, modelsdk/widgets/objectdirect_test.go; example mdl-examples/bug-tests/1632-calendar-toolbaritems-definition.mdl", "insight": "The second .mpk parser, sdk/widgets/mpk (widget init, catalog, widget docs), already read both shapes and named PopupMenu basicItems and Maps markers as examples of the direct one; modelsdk/widgets/mpk had only the grouped one. PopupMenu and Maps never showed it because they have embedded templates, whose nested ObjectType comes from the template rather than the parser. When two parsers of one format exist, diff their xml struct tags first. Checking a generated widget against `mx update-widgets` on a copy, ids stripped, gives the exact field; `mxcli docker check` cannot, because it runs that same update before checking.", "refs": ["mendixlabs/mxcli#1632", "mendixlabs/mxcli#1578"], "ce": ["CE0463"]}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed

- **A Marketplace Calendar written by mxcli passes a plain `mx check`** (mendixlabs/mxcli#1632) — every Calendar failed with CE0463 "The definition of this widget has changed" until `mxcli fix widgets` or Studio Pro's "Update all widgets"; `mxcli docker check` hid it, since it runs that update on a temporary copy first. The Calendar's `toolbarItems` object list declares its sub-properties directly under `<properties>`, not inside a `<propertyGroup>`, and the `.mpk` parser behind generated widget templates read only the grouped shape, so the list was stored with no definition of its items. Both shapes are now read, a direct sub-property takes its category from its own `<category>` element, and the stored Calendar equals what `mx update-widgets` writes. The other object-list widgets in `doctype-tests/32-pluggable-widget-object-lists-v010.mdl` (Accordion, PopupMenu, Maps, area and line chart, heat map) are stored unchanged.
- **A specialization of a non-persistent entity reads as non-persistent** (mendixlabs/mxcli#1600) — `describe entity Probe.Child` printed `create or modify persistent entity Probe.Child extends Probe.Base (` when `Probe.Base` is non-persistent, so a reader took the child for a table and the output stated a combination Mendix does not have. A specialization stores no persistable flag of its own; it takes its root generalization's, and the reader defaulted it to persistent. The flag is now resolved along the generalization chain, across modules and into the System module (whose `HttpRequest`, `HttpResponse` and `SoapFault` read persistent for the same reason), and the fix reaches everything that reads it: `describe`, the catalog's `EntityType`, the domain-model diagrams, and `alter entities … where persistent`, which used to add its attribute to such a child as well. Re-running the describe output leaves the entity unchanged.
- **`go mod tidy` runs again, and the expression round-trip test runs for the first time** — `mdl/exprcheck/roundtrip_test.go` imported `mdl/backend/mpr` (the legacy engine, deleted) plus `executor.GetHierarchyForMining` and `executor.DescribeMicroflowToString` (both removed). It never broke a build because it sat behind `//go:build roundtrip`, a tag no Makefile target and no CI job has ever passed — the only file in the tree using it. `go mod tidy` considers every build tag, so it was the one command that still saw the file, and it failed with "no matching versions for query latest" on the deleted package, which reads as a dependency problem rather than a dead test. The test is ported to `mdl/executor/exprcheck_roundtrip_test.go` under the `integration` tag, where `setupTestEnv`/`describeMDL` already provide the real-project harness and `make test-integration-executor` actually runs it; the removed hierarchy helper is replaced by building the container-to-module map from `backend.ListModules()`. Its assertion is worth keeping: describing every microflow and re-parsing each expression with the robust parser catches grammar gaps — a missing `parsePrimary` branch, a missing `funcTable` entry, a missing slot constraint — that the unit tests cannot, since those only feed the parser expressions someone thought to write down. It now also fails rather than passing silently when the fixture project yields no microflows or no expression slots.

Expand Down
33 changes: 33 additions & 0 deletions mdl-examples/bug-tests/1632-calendar-toolbaritems-definition.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-- mendixlabs/mxcli#1632: every Marketplace Calendar (content 245304, 2.6.1)
-- written by mxcli passed `check` and `exec`, then a plain `mx check` failed:
-- [CE0463] "The definition of this widget has changed. ..." at Calendar 'cal'
-- `mxcli docker check` reported it clean, because it runs update-widgets on a
-- temporary copy before checking.
-- The Calendar has no embedded template, so its Type is generated from the
-- .mpk. Its toolbarItems object list declares its sub-properties directly
-- under <properties>, with no <propertyGroup>, and the parser read only the
-- grouped shape: toolbarItems was stored with ObjectType null.
-- After the fix, `mx check` reports no error for this page, and the stored
-- page equals a copy that `mx update-widgets` rewrote (ids stripped).
-- Needs: `mxcli marketplace install 245304 -p <app>` first.
mdl 1;

create module BugTest1632;

create persistent entity BugTest1632.Shift (
Title: String(200),
StartsAt: DateTime,
EndsAt: DateTime
);

-- No toolbar item written: the object list is empty, its definition must
-- still be complete.
create or modify page BugTest1632.ShiftCalendar (Title: 'Shifts', Layout: Atlas_Core.Atlas_Default) {
pluggablewidget 'com.mendix.widget.web.calendar.Calendar' cal (
databaseDataSource: database BugTest1632.Shift,
titleType: 'attribute',
titleAttribute: Title,
startAttribute: StartsAt,
endAttribute: EndsAt
)
};
39 changes: 34 additions & 5 deletions modelsdk/widgets/mpk/mpk.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,30 @@ type xmlProperty struct {
ActionVariables []xmlActionVariable `xml:"actionVariables>actionVariable"`
// Nested properties for object type
NestedProps []xmlPropGroup `xml:"properties>propertyGroup"`
// Nested properties an object type lists directly, with no propertyGroup
// around them (the Marketplace Calendar's toolbarItems, PopupMenu
// basicItems, Maps markers) — see nestedGroups. sdk/widgets/mpk already
// reads both shapes.
NestedDirectProps []xmlProperty `xml:"properties>property"`
// Category of such a direct sub-property (<category>Appearance</category>);
// grouped properties take theirs from the group captions instead.
Category string `xml:"category"`
}

// nestedGroups returns an object property's sub-properties as property groups.
// The direct ones come back as one caption-less group, so each takes its own
// <category>. Without them the object had no Children, its generated ObjectType
// stayed nil, and mx check reported CE0463 (mendixlabs/mxcli#1632). Groups come
// first, then the direct ones, the order sdk/widgets/mpk reads them in.
func nestedGroups(p xmlProperty) []xmlPropGroup {
if len(p.NestedDirectProps) == 0 {
return p.NestedProps
}
direct := xmlPropGroup{Properties: p.NestedDirectProps}
for i := range p.NestedDirectProps {
direct.Children = append(direct.Children, xmlPropGroupChild{Property: &p.NestedDirectProps[i]})
}
return append(append([]xmlPropGroup(nil), p.NestedProps...), direct)
}

// xmlActionVariable represents <actionVariable key="…" caption="…" type="…"/>.
Expand Down Expand Up @@ -455,8 +479,8 @@ func walkPropertyGroup(pg xmlPropGroup, parentCategory string, def *WidgetDefini
}

// Parse nested properties for object-type properties
if p.Type == "object" && len(p.NestedProps) > 0 {
for _, npg := range p.NestedProps {
if p.Type == "object" {
for _, npg := range nestedGroups(p) {
collectNestedProperties(npg, &prop, "")
}
}
Expand Down Expand Up @@ -503,6 +527,11 @@ func collectNestedProperties(pg xmlPropGroup, parent *PropertyDef, parentCategor
category = parentCategory
}
for _, p := range pg.Properties {
// A sub-property outside any group names its category itself.
childCategory := category
if childCategory == "" {
childCategory = strings.TrimSpace(p.Category)
}
var allowedTypes []string
for _, at := range p.AttributeTypes {
if at.Name != "" {
Expand All @@ -518,7 +547,7 @@ func collectNestedProperties(pg xmlPropGroup, parent *PropertyDef, parentCategor
Type: p.Type,
Caption: p.Caption,
Description: p.Description,
Category: category,
Category: childCategory,
// Mendix pluggable-widget spec: `required` defaults to true when the
// attribute is absent (mxbuild's update-widgets emits Required=true for
// every property that omits required=, e.g. DataGrid2 showContentAs). Only
Expand All @@ -542,8 +571,8 @@ func collectNestedProperties(pg xmlPropGroup, parent *PropertyDef, parentCategor
ActionVariables: toActionVariables(p.ActionVariables),
}
// Nested object-type properties can themselves contain object lists.
if p.Type == "object" && len(p.NestedProps) > 0 {
for _, npg := range p.NestedProps {
if p.Type == "object" {
for _, npg := range nestedGroups(p) {
collectNestedProperties(npg, &child, "")
}
}
Expand Down
83 changes: 83 additions & 0 deletions modelsdk/widgets/mpk/objectdirect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: Apache-2.0

package mpk

import "testing"

// mendixlabs/mxcli#1632: an object property may list its sub-properties directly
// under <properties>, with no <propertyGroup> around them, and give each one its
// category as a <category> element. The Marketplace Calendar's toolbarItems does.
// Only grouped sub-properties were read, so the object came back with no Children,
// its generated ObjectType was nil, and every Calendar failed `mx check` with
// CE0463 until `update-widgets` rewrote the Type.
func TestParse_ObjectPropertyWithDirectSubProperties(t *testing.T) {
const widgetXML = `<?xml version="1.0" encoding="utf-8"?>
<widget id="com.example.Cal" pluginWidget="true" xmlns="http://www.mendix.com/widget/1.0/">
<name>Cal</name>
<properties>
<propertyGroup caption="General">
<propertyGroup caption="Toolbar">
<property key="toolbarItems" type="object" isList="true" required="false">
<caption>Custom top bar views</caption><description/>
<properties>
<property key="itemType" type="enumeration" defaultValue="month">
<caption>Item</caption>
<category>Appearance</category>
<description/>
<enumerationValues>
<enumerationValue key="day">Day</enumerationValue>
<enumerationValue key="month">Month</enumerationValue>
</enumerationValues>
</property>
<property key="caption" type="textTemplate" required="false">
<caption>Caption</caption><description/>
</property>
</properties>
</property>
<property key="lines" type="object" isList="true">
<caption>Series</caption><description/>
<properties>
<propertyGroup caption="General">
<property key="dataSet" type="enumeration" defaultValue="static">
<caption>Data set</caption><description/>
<enumerationValues><enumerationValue key="static">Single</enumerationValue></enumerationValues>
</property>
</propertyGroup>
</properties>
</property>
</propertyGroup>
</propertyGroup>
</properties>
</widget>`

def := parseWidgetXML(t, widgetXML)
children := map[string][]PropertyDef{}
for _, p := range def.Properties {
children[p.Key] = p.Children
}

type want struct{ key, category string }
check := func(prop string, wants []want) {
t.Helper()
got := children[prop]
if len(got) != len(wants) {
t.Fatalf("%s: %d sub-properties, want %d (%v)", prop, len(got), len(wants), got)
}
for i, w := range wants {
if got[i].Key != w.key || got[i].Category != w.category {
t.Errorf("%s[%d] = {%q, category %q}, want {%q, category %q}",
prop, i, got[i].Key, got[i].Category, w.key, w.category)
}
}
}
// Direct: read in document order, the category from the property's own
// <category> element, none when it declares none.
check("toolbarItems", []want{{"itemType", "Appearance"}, {"caption", ""}})
if got := children["toolbarItems"]; len(got) == 2 {
if len(got[0].EnumValues) != 2 || got[1].Required {
t.Errorf("direct sub-properties lost their attributes: %+v", got)
}
}
// Control: grouped sub-properties keep the group caption as their category.
check("lines", []want{{"dataSet", "General"}})
}
106 changes: 106 additions & 0 deletions modelsdk/widgets/objectdirect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: Apache-2.0

package widgets

import (
"archive/zip"
"os"
"path/filepath"
"testing"

"github.com/mendixlabs/mxcli/modelsdk/widgets/mpk"
)

// mendixlabs/mxcli#1632, end to end from the .mpk: the Marketplace Calendar lists
// toolbarItems' sub-properties directly under <properties>. The generated Type
// wrote that object property with ObjectType nil, where mxbuild writes a
// WidgetObjectType holding the sub-property types — CE0463 on every Calendar.
func TestGenerateFromMPK_ObjectPropertyWithDirectSubProperties(t *testing.T) {
const pkgXML = `<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://www.mendix.com/package/1.0/">
<clientModule name="Cal" version="1.0.0" xmlns="http://www.mendix.com/clientModule/1.0/">
<widgetFiles><widgetFile path="Cal.xml"/></widgetFiles>
</clientModule>
</package>`
const widgetXML = `<?xml version="1.0" encoding="utf-8"?>
<widget id="com.example.Cal" pluginWidget="true" xmlns="http://www.mendix.com/widget/1.0/">
<name>Cal</name>
<properties>
<propertyGroup caption="General">
<property key="toolbarItems" type="object" isList="true" required="false">
<caption>Custom top bar views</caption><description/>
<properties>
<property key="itemType" type="enumeration" defaultValue="month">
<caption>Item</caption>
<category>Appearance</category>
<description/>
<enumerationValues>
<enumerationValue key="day">Day</enumerationValue>
<enumerationValue key="month">Month</enumerationValue>
</enumerationValues>
</property>
<property key="caption" type="textTemplate" required="false">
<caption>Caption</caption><description/>
</property>
</properties>
</property>
</propertyGroup>
</properties>
</widget>`

path := filepath.Join(t.TempDir(), "Cal.mpk")
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
zw := zip.NewWriter(f)
for name, body := range map[string]string{"package.xml": pkgXML, "Cal.xml": widgetXML} {
w, err := zw.Create(name)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write([]byte(body)); err != nil {
t.Fatal(err)
}
}
if err := zw.Close(); err != nil {
t.Fatal(err)
}
f.Close()

def, err := mpk.ParseMPK(path)
if err != nil {
t.Fatal(err)
}
ResetPlaceholderCounter()
tmpl := GenerateFromMPK(def)

var toolbar map[string]any
for _, pt := range tmpl.Type["ObjectType"].(map[string]any)["PropertyTypes"].([]any) {
if m, ok := pt.(map[string]any); ok && m["PropertyKey"] == "toolbarItems" {
toolbar = m
}
}
if toolbar == nil {
t.Fatal("toolbarItems PropertyType missing")
}
objType, _ := toolbar["ValueType"].(map[string]any)["ObjectType"].(map[string]any)
if objType == nil {
t.Fatal("toolbarItems ValueType.ObjectType is nil; mxbuild writes a WidgetObjectType with its sub-property types")
}
var keys, cats []string
for _, pt := range objType["PropertyTypes"].([]any) {
if m, ok := pt.(map[string]any); ok {
keys = append(keys, m["PropertyKey"].(string))
cats = append(cats, m["Category"].(string))
}
}
if len(keys) != 2 || keys[0] != "itemType" || keys[1] != "caption" {
t.Fatalf("sub-property types = %v, want [itemType caption]", keys)
}
// mxbuild writes Category "Appearance" for the Calendar's itemType, from its
// <category> element. (Every sub-property of the real Calendar declares one.)
if cats[0] != "Appearance" {
t.Errorf("itemType Category = %q, want %q", cats[0], "Appearance")
}
}
Loading