From 975452f5d5c36e43e84df52835bbdf1f687ac494 Mon Sep 17 00:00:00 2001 From: stagknee <241821830+stagknee@users.noreply.github.com> Date: Thu, 22 Oct 2026 03:02:35 -0500 Subject: [PATCH] fix(widgets): read an object property's sub-properties declared without a propertyGroup (mendixlabs/mxcli#1632) Every Marketplace Calendar written by mxcli failed a plain `mx check` with CE0463 "The definition of this widget has changed". The Calendar has no embedded template, so its Type is generated from the .mpk, and its toolbarItems object list declares its sub-properties directly under , each with a element. modelsdk/widgets/mpk read only , so toolbarItems had no Children and was stored with ValueType.ObjectType null. The parser now reads both shapes, as sdk/widgets/mpk already does; a direct sub-property takes its category from its own . On an 11.12.5 app the stored Calendar equals a copy rewritten by `mx update-widgets` (ids stripped), and the object-list widgets of doctype-tests/32 are stored byte-identical to before. Co-Authored-By: Claude Opus 5.5 --- ...operty-direct-sub-properties-not-read.json | 1 + CHANGELOG.md | 1 + .../1632-calendar-toolbaritems-definition.mdl | 33 ++++++ modelsdk/widgets/mpk/mpk.go | 39 ++++++- modelsdk/widgets/mpk/objectdirect_test.go | 83 ++++++++++++++ modelsdk/widgets/objectdirect_test.go | 106 ++++++++++++++++++ 6 files changed, 258 insertions(+), 5 deletions(-) create mode 100644 .claude/skills/fix-issue/findings/modelsdk/2026-10-22-object-property-direct-sub-properties-not-read.json create mode 100644 mdl-examples/bug-tests/1632-calendar-toolbaritems-definition.mdl create mode 100644 modelsdk/widgets/mpk/objectdirect_test.go create mode 100644 modelsdk/widgets/objectdirect_test.go diff --git a/.claude/skills/fix-issue/findings/modelsdk/2026-10-22-object-property-direct-sub-properties-not-read.json b/.claude/skills/fix-issue/findings/modelsdk/2026-10-22-object-property-direct-sub-properties-not-read.json new file mode 100644 index 0000000000..fd67d6a346 --- /dev/null +++ b/.claude/skills/fix-issue/findings/modelsdk/2026-10-22-object-property-direct-sub-properties-not-read.json @@ -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 ``; the Calendar lists them directly under ``, each with a `` 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 ); 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"]} diff --git a/CHANGELOG.md b/CHANGELOG.md index b291e68f24..e0e83ad031 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **A pluggable widget's text-template parameter across an association builds, and describes so it re-executes** (mendixlabs/mxcli#1494) — `fieldset fs (legend: '{1}', legendParams: ({1} = M.Parent_Child/Label))` passed `check` and `exec` and failed the build with `[CE1365] "Attribute 'M.Child.Label' is not an attribute of entity 'M.Parent'."`: the parameter was written with its attribute but without the association hop, so mxbuild read the attribute against the widget's own entity. The same writer serves an object-list item's template (an Accordion group's `headerText`) and a Data grid 2 column's dynamic text, and all three failed; on Mendix 11.12.4 they now build with 0 errors. `describe page` printed such a parameter as the bare attribute (`{1} = Label`), which re-executed into CE1613 "The selected attribute 'M.Parent.Label' no longer exists"; it now prints `M.Parent_Child/Label`. The companion report, an expression parameter (`{1} = if 1 = 1 then 'A' else 'B'`) stored as an attribute name (mendixlabs/mxcli#1495), was fixed in v0.25.0 (#823) and is now pinned by a test. - **`alter entity … add event handler` adds a second handler for the same event** (mendixlabs/mxcli#1540) — `add event handler on before commit call M.Second($currentObject)` failed with "event handler already exists for Before Commit" when the entity already had one, though Studio Pro allows several handlers for one event and `create or modify entity` with two `on before commit` clauses stores both (measured on 11.12.4: two Before Commit handlers are 0 errors, a void-returning one is CE7247, so the check does look at them). A handler is now identified by its moment, event and microflow: another microflow adds a second handler, the same microflow again is still a duplicate. The same key had made `add event handler if not exists … call M.New` skip — reporting "already exists" — whenever any handler for the event existed, and `drop event handler on before commit` remove whichever handler was stored first. `drop event handler` now takes `call M.Mf` to say which one; without it, a drop that matches several handlers is refused and names them. +- **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 ``, not inside a ``, 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 `` 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. ## [0.28.0] - 2026-10-23 diff --git a/mdl-examples/bug-tests/1632-calendar-toolbaritems-definition.mdl b/mdl-examples/bug-tests/1632-calendar-toolbaritems-definition.mdl new file mode 100644 index 0000000000..05527e6a0a --- /dev/null +++ b/mdl-examples/bug-tests/1632-calendar-toolbaritems-definition.mdl @@ -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 , with no , 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 ` 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 + ) +}; diff --git a/modelsdk/widgets/mpk/mpk.go b/modelsdk/widgets/mpk/mpk.go index 6f16e3dc06..2137f59b07 100644 --- a/modelsdk/widgets/mpk/mpk.go +++ b/modelsdk/widgets/mpk/mpk.go @@ -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 (Appearance); + // 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 +// . 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 . @@ -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, "") } } @@ -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 != "" { @@ -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 @@ -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, "") } } diff --git a/modelsdk/widgets/mpk/objectdirect_test.go b/modelsdk/widgets/mpk/objectdirect_test.go new file mode 100644 index 0000000000..f203a1a2c0 --- /dev/null +++ b/modelsdk/widgets/mpk/objectdirect_test.go @@ -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 , with no around them, and give each one its +// category as a 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 = ` + + Cal + + + + + Custom top bar views + + + Item + Appearance + + + Day + Month + + + + Caption + + + + + Series + + + + Data set + Single + + + + + + + +` + + 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 + // 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"}}) +} diff --git a/modelsdk/widgets/objectdirect_test.go b/modelsdk/widgets/objectdirect_test.go new file mode 100644 index 0000000000..59d7daf697 --- /dev/null +++ b/modelsdk/widgets/objectdirect_test.go @@ -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 . 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 = ` + + + + +` + const widgetXML = ` + + Cal + + + + Custom top bar views + + + Item + Appearance + + + Day + Month + + + + Caption + + + + + +` + + 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 + // element. (Every sub-property of the real Calendar declares one.) + if cats[0] != "Appearance" { + t.Errorf("itemType Category = %q, want %q", cats[0], "Appearance") + } +}