Skip to content

Commit 91b054b

Browse files
claudeako
authored andcommitted
fix(widgets): reconcile generated-widget definitions to close Chart CE0463 in v2
Charts (Pie/Column/Line/Bar/Area) have no embedded template, so they are built by GenerateFromMPK. That path drifted from mxbuild's WidgetType in three ways, all checked by CE0463, so every chart instance failed mx check in an MPR v2 project (only mx update-widgets cleared it, downgrading v2->v1): 1. It omitted the widget's declared <systemProperty> entries (Name, TabIndex, Visibility). Now emitted in their declared document position as System-typed PropertyTypes (via the .mpk's AllTopLevel order), with no Object WidgetProperty. 2. It left each ValueType's schema-derived fields empty (enum option sets, expression returnType, selectionTypes, multiline). GenerateFromMPK now runs the same .mpk reconciliation augment applies to template-based widgets. 3. The .mpk parser read helpUrl/studioCategory as XML attributes; Mendix declares them as elements. Also parse the multiline attr and <selectionType> elements. Result: the emitted WidgetType is byte-identical to update-widgets output for Pie/Column/Line/Bar/Area on Mendix 11.12.1 — 0 CE0463, MPR v2 preserved. No regression on template-based widgets (ComboBox, DataGrid2) or the reconcile path. Known remaining: TimeSeries has a nested-series textTemplate that serializes as an empty ClientTemplate instead of null (object-side, same class as the Timeline visibility nulling but nested in an object-list item) — separate follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
1 parent 32231b3 commit 91b054b

4 files changed

Lines changed: 216 additions & 29 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
-- ============================================================================
2+
-- Bug: Chart widgets (Pie / Column / …) trip CE0463 in MPR v2 projects
3+
-- ============================================================================
4+
--
5+
-- Symptom:
6+
-- A PieChart or ColumnChart authored via MDL persisted its series properties
7+
-- correctly but every instance failed `mx check` with:
8+
-- [CE0463] "The definition of this widget has changed. Update this widget…"
9+
-- The only thing that cleared it was `mx update-widgets`, which downgrades an
10+
-- MPR v2 project to v1 (drops mprcontents/). So charts could not ship in v2.
11+
-- By contrast the Image widget (which has an embedded template) authored cleanly.
12+
--
13+
-- Root cause:
14+
-- Charts have no embedded template, so they are built by GenerateFromMPK. That
15+
-- path (a) omitted the widget's declared <systemProperty> entries (Name,
16+
-- TabIndex, Visibility) that mxbuild emits into the WidgetType; (b) left each
17+
-- ValueType's schema-derived fields (enum option sets, expression returnType,
18+
-- selectionTypes, multiline) empty; and (c) parsed helpUrl/studioCategory as XML
19+
-- attributes when Mendix declares them as elements. All three are checked by
20+
-- CE0463, so the generated WidgetType drifted from the installed widget.
21+
--
22+
-- Fix:
23+
-- GenerateFromMPK now emits system PropertyTypes in their declared position and
24+
-- runs the same .mpk reconciliation augment uses on template-based widgets; the
25+
-- .mpk parser reads helpUrl/studioCategory as elements and parses multiline +
26+
-- <selectionType>. Result: the emitted WidgetType is byte-identical to
27+
-- update-widgets output — no CE0463, MPR v2 preserved.
28+
--
29+
-- Verify:
30+
-- mxcli exec mdl-examples/bug-tests/chart-widget-ce0463-v2.mdl -p app.mpr
31+
-- mx check app.mpr # 0 errors (v2 preserved)
32+
-- Requires: Charts .mpk installed in the project's widgets/.
33+
-- ============================================================================
34+
35+
create module ChartBug;
36+
/
37+
create entity ChartBug.Sales ( Region: String, Total: Decimal );
38+
/
39+
create or replace page ChartBug.Home ( layout: Atlas_Core.Atlas_Default ) {
40+
-- Pie chart: single implicit series (widget-level DataSource/ValueAttribute).
41+
pluggablewidget 'com.mendix.widget.web.piechart.PieChart' pie1 (
42+
DataSource: database from ChartBug.Sales,
43+
ValueAttribute: Total,
44+
SeriesName: 'Sales by Region'
45+
)
46+
-- Column chart: an explicit `series` object-list.
47+
pluggablewidget 'com.mendix.widget.web.columnchart.ColumnChart' col1 {
48+
series s (
49+
DataSet: 'static',
50+
DataSource: database from ChartBug.Sales,
51+
StaticXAttribute: Region,
52+
StaticYAttribute: Total,
53+
StaticName: 'Revenue'
54+
)
55+
}
56+
}

modelsdk/widgets/augment.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,10 @@ func reconcileValueTypesFromMPK(tmpl *WidgetTemplate, byKey map[string]mpk.Prope
385385
}
386386
vt["Required"] = pd.Required
387387
vt["IsList"] = pd.IsList
388+
vt["Multiline"] = pd.Multiline
388389
vt["DefaultValue"] = pd.DefaultValue
389390
vt["AllowedTypes"] = buildAllowedTypesArray(pd.AllowedTypes)
391+
vt["SelectionTypes"] = buildSelectionTypesArray(pd.SelectionTypes)
390392
vt["DataSourceProperty"] = pd.DataSource
391393
// Normalize the mutually-exclusive type-specific fields to the
392394
// authoritative .mpk type. A ValueType cloned from a wrong-typed
@@ -533,6 +535,18 @@ func buildAllowedTypesArray(types []string) []any {
533535
return arr
534536
}
535537

538+
// buildSelectionTypesArray builds a ValueType.SelectionTypes list (leading Mendix
539+
// array marker 1 followed by the selection type names, e.g. "None"/"Single") from a
540+
// .mpk selection property's declared <selectionType> elements. Empty (marker only)
541+
// when the property declares none.
542+
func buildSelectionTypesArray(names []string) []any {
543+
arr := []any{float64(1)}
544+
for _, n := range names {
545+
arr = append(arr, n)
546+
}
547+
return arr
548+
}
549+
536550
// mpkEnumValuesByKey indexes a widget's enumeration option sets by property key,
537551
// across both top-level and nested (object-list) properties.
538552
func mpkEnumValuesByKey(def *mpk.WidgetDefinition) map[string][]mpk.EnumValue {

modelsdk/widgets/generate.go

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,32 @@ import "github.com/mendixlabs/mxcli/modelsdk/widgets/mpk"
77
// GenerateFromMPK builds a complete WidgetTemplate from a parsed MPK WidgetDefinition.
88
// All $IDs are placeholder IDs (aa000000... prefix). loader.go's collectIDs remaps them
99
// to real UUIDs before BSON serialisation — matching the lifecycle of embedded templates.
10-
// System properties (Label, Visibility, Editability) are not added; Studio Pro injects them.
10+
//
11+
// System properties (<systemProperty> — Name, TabIndex, Visibility, Label,
12+
// Editability) ARE emitted, in their declared document position, as System-typed
13+
// PropertyTypes with no corresponding Object WidgetProperty (they map to the outer
14+
// CustomWidget's Name/TabIndex/visibility fields). mxbuild's update-widgets emits
15+
// them and CE0463 checks the Type's PropertyTypes, so a generated widget that omits
16+
// them (e.g. any Charts widget, which has no embedded template) drifts → CE0463.
1117
func GenerateFromMPK(def *mpk.WidgetDefinition) *WidgetTemplate {
1218
typeID := placeholderID()
1319
objTypeID := placeholderID()
1420

1521
propTypes := []any{float64(2)} // Mendix array version marker
1622
objProps := []any{float64(2)}
1723

18-
for _, p := range def.Properties {
24+
// Iterate the full declared order (regular + system interleaved) so system
25+
// PropertyTypes land at their .mpk-declared position; fall back to the regular
26+
// list if the ordered view is unavailable.
27+
source := def.AllTopLevel
28+
if len(source) == 0 {
29+
source = def.Properties
30+
}
31+
for _, p := range source {
32+
if p.IsSystem {
33+
propTypes = append(propTypes, createSystemPropertyType(p))
34+
continue // system props have no Object WidgetProperty
35+
}
1936
bsonType := xmlTypeToBSONType(p.Type)
2037
if bsonType == "" {
2138
continue // unknown XML type — skip silently
@@ -61,12 +78,73 @@ func GenerateFromMPK(def *mpk.WidgetDefinition) *WidgetTemplate {
6178
"Properties": objProps,
6279
}
6380

64-
return &WidgetTemplate{
81+
tmpl := &WidgetTemplate{
6582
WidgetID: def.ID,
6683
Name: def.Name,
6784
Version: def.Version,
6885
Generated: true,
6986
Type: typeMap,
7087
Object: objectMap,
7188
}
89+
90+
// createDefaultValueType emits a minimal ValueType (empty enum/returnType/etc.)
91+
// — for template-based widgets augment's reconcile pass fills these in from the
92+
// .mpk, but a generated widget skips augment. Run the same reconciliation here so
93+
// each PropertyType's schema-derived fields (enum option sets, expression
94+
// returnType, ValueType scalars) match the installed widget; otherwise a
95+
// generated widget (e.g. any Charts widget) drifts within-key → CE0463.
96+
byKey := mpkPropDefsByKey(def)
97+
reconcileEnumValues(tmpl.Type, mpkEnumValuesByKey(def))
98+
reconcilePropertyMetadata(tmpl.Type, byKey)
99+
reconcileValueTypesFromMPK(tmpl, byKey)
100+
completeValueTypeEnvelope(tmpl.Type)
101+
102+
return tmpl
103+
}
104+
105+
// createSystemPropertyType builds a System-typed CustomWidgets$WidgetPropertyType for
106+
// a <systemProperty> (Name/TabIndex/Visibility/Label/Editability), matching mxbuild's
107+
// output: Caption "<system:Key>", Category from the enclosing group chain, and a
108+
// ValueType with Type "System" and the standard empty envelope (array markers as
109+
// Studio Pro emits them). The placeholder $IDs are remapped by the loader's ID phase.
110+
func createSystemPropertyType(p mpk.PropertyDef) map[string]any {
111+
return map[string]any{
112+
"$ID": placeholderID(),
113+
"$Type": "CustomWidgets$WidgetPropertyType",
114+
"Caption": "<system:" + p.Key + ">",
115+
"Category": p.Category,
116+
"Description": "",
117+
"IsDefault": false,
118+
"PropertyKey": p.Key,
119+
"ValueType": map[string]any{
120+
"$ID": placeholderID(),
121+
"$Type": "CustomWidgets$WidgetValueType",
122+
"ActionVariables": []any{float64(2)},
123+
"AllowNonPersistableEntities": false,
124+
"AllowUpload": false,
125+
"AllowedTypes": []any{float64(1)},
126+
"AssociationTypes": []any{float64(1)},
127+
"DataSourceProperty": "",
128+
"DefaultType": "None",
129+
"DefaultValue": "",
130+
"EntityProperty": "",
131+
"EnumerationValues": []any{float64(2)},
132+
"IsLinked": false,
133+
"IsList": false,
134+
"IsMetaData": false,
135+
"IsPath": "No",
136+
"Multiline": false,
137+
"ObjectType": nil,
138+
"OnChangeProperty": "",
139+
"ParameterIsList": false,
140+
"PathType": "None",
141+
"Required": false,
142+
"ReturnType": nil,
143+
"SelectableObjectsProperty": "",
144+
"SelectionTypes": []any{float64(1)},
145+
"SetLabel": false,
146+
"Translations": []any{float64(2)},
147+
"Type": "System",
148+
},
149+
}
72150
}

modelsdk/widgets/mpk/mpk.go

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@ import (
1717

1818
// PropertyDef describes a single property from a widget XML definition.
1919
type PropertyDef struct {
20-
Key string // e.g. "staticDataSourceCaption"
21-
Type string // XML type: "attribute", "expression", "textTemplate", "widgets", etc.
22-
Caption string
23-
Description string
24-
Category string // from enclosing propertyGroup captions, joined with "::"
25-
Required bool
26-
DefaultValue string // for enumeration/boolean/integer types
27-
IsList bool
28-
IsSystem bool // true for <systemProperty> elements
29-
DataSource string // dataSource attribute reference
30-
ReturnType string // for expression properties: the <returnType type="..."/> Mendix type
20+
Key string // e.g. "staticDataSourceCaption"
21+
Type string // XML type: "attribute", "expression", "textTemplate", "widgets", etc.
22+
Caption string
23+
Description string
24+
Category string // from enclosing propertyGroup captions, joined with "::"
25+
Required bool
26+
DefaultValue string // for enumeration/boolean/integer types
27+
IsList bool
28+
Multiline bool // for string/textTemplate: multiline="true"
29+
SelectionTypes []string // for selection properties: <selectionType name="..."/>
30+
IsSystem bool // true for <systemProperty> elements
31+
DataSource string // dataSource attribute reference
32+
ReturnType string // for expression properties: the <returnType type="..."/> Mendix type
3133
// ReturnTypeAssignableTo is the <returnType assignableTo="..."/> reference (e.g.
3234
// "../staticAttribute"), when the expression's return type is derived from another
3335
// property rather than a concrete type. mxbuild emits Type "None" with this set.
@@ -94,17 +96,19 @@ type xmlWidgetFile struct {
9496

9597
// xmlWidget represents <widget> root element in widget XML.
9698
type xmlWidget struct {
97-
ID string `xml:"id,attr"`
98-
PluginWidget string `xml:"pluginWidget,attr"`
99-
OfflineCapable string `xml:"offlineCapable,attr"`
100-
NeedsEntityContext string `xml:"needsEntityContext,attr"`
101-
SupportedPlatform string `xml:"supportedPlatform,attr"`
102-
HelpURL string `xml:"helpUrl,attr"`
103-
StudioCategory string `xml:"studioCategory,attr"`
104-
StudioProCategory string `xml:"studioProCategory,attr"`
105-
Name string `xml:"name"`
106-
Description string `xml:"description"`
107-
PropertyGroups []xmlPropGroup `xml:"properties>propertyGroup"`
99+
ID string `xml:"id,attr"`
100+
PluginWidget string `xml:"pluginWidget,attr"`
101+
OfflineCapable string `xml:"offlineCapable,attr"`
102+
NeedsEntityContext string `xml:"needsEntityContext,attr"`
103+
SupportedPlatform string `xml:"supportedPlatform,attr"`
104+
// helpUrl, studioCategory, studioProCategory are child ELEMENTS in Mendix
105+
// widget XML (e.g. <studioProCategory>Charts</studioProCategory>), not attributes.
106+
HelpURL string `xml:"helpUrl"`
107+
StudioCategory string `xml:"studioCategory"`
108+
StudioProCategory string `xml:"studioProCategory"`
109+
Name string `xml:"name"`
110+
Description string `xml:"description"`
111+
PropertyGroups []xmlPropGroup `xml:"properties>propertyGroup"`
108112
}
109113

110114
// xmlPropGroup represents <propertyGroup caption="..."> element.
@@ -198,17 +202,24 @@ type xmlProperty struct {
198202
DefaultValue string `xml:"defaultValue,attr"`
199203
Required string `xml:"required,attr"`
200204
IsList string `xml:"isList,attr"`
205+
Multiline string `xml:"multiline,attr"`
201206
DataSource string `xml:"dataSource,attr"`
202207
Caption string `xml:"caption"`
203208
Description string `xml:"description"`
204209
AttributeTypes []xmlAttributeType `xml:"attributeTypes>attributeType"`
205210
EnumValues []xmlEnumValue `xml:"enumerationValues>enumerationValue"`
211+
SelectionTypes []xmlSelectionType `xml:"selectionTypes>selectionType"`
206212
ReturnType xmlReturnType `xml:"returnType"`
207213
Translations []xmlTranslation `xml:"translations>translation"`
208214
// Nested properties for object type
209215
NestedProps []xmlPropGroup `xml:"properties>propertyGroup"`
210216
}
211217

218+
// xmlSelectionType represents <selectionType name="..."/> on a selection property.
219+
type xmlSelectionType struct {
220+
Name string `xml:"name,attr"`
221+
}
222+
212223
// xmlReturnType represents <returnType type="..."/> or
213224
// <returnType assignableTo="../otherProp"/> on an expression property. A widget may
214225
// declare either a concrete Mendix type or an assignableTo reference (the expression
@@ -382,6 +393,8 @@ func walkPropertyGroup(pg xmlPropGroup, parentCategory string, def *WidgetDefini
382393
Required: p.Required != "false",
383394
DefaultValue: p.DefaultValue,
384395
IsList: p.IsList == "true",
396+
Multiline: p.Multiline == "true",
397+
SelectionTypes: toSelectionTypes(p.SelectionTypes),
385398
DataSource: p.DataSource,
386399
ReturnType: p.ReturnType.Type,
387400
ReturnTypeAssignableTo: p.ReturnType.AssignableTo,
@@ -452,6 +465,8 @@ func collectNestedProperties(pg xmlPropGroup, parent *PropertyDef, parentCategor
452465
Required: p.Required != "false",
453466
DefaultValue: p.DefaultValue,
454467
IsList: p.IsList == "true",
468+
Multiline: p.Multiline == "true",
469+
SelectionTypes: toSelectionTypes(p.SelectionTypes),
455470
DataSource: p.DataSource,
456471
ReturnType: p.ReturnType.Type,
457472
ReturnTypeAssignableTo: p.ReturnType.AssignableTo,
@@ -473,6 +488,21 @@ func collectNestedProperties(pg xmlPropGroup, parent *PropertyDef, parentCategor
473488
}
474489
}
475490

491+
// toSelectionTypes converts parsed <selectionType> XML elements to a name list
492+
// (e.g. ["None","Single"]), the ValueType.SelectionTypes a selection property carries.
493+
func toSelectionTypes(xs []xmlSelectionType) []string {
494+
if len(xs) == 0 {
495+
return nil
496+
}
497+
out := make([]string, 0, len(xs))
498+
for _, x := range xs {
499+
if x.Name != "" {
500+
out = append(out, x.Name)
501+
}
502+
}
503+
return out
504+
}
505+
476506
// toTranslations converts parsed <translation> XML elements to Translation records,
477507
// trimming caption whitespace (the XML pretty-prints chardata with indentation).
478508
func toTranslations(xts []xmlTranslation) []Translation {
@@ -721,7 +751,7 @@ func buildDefinition(widget *xmlWidget, version string) *WidgetDefinition {
721751
byKey[def.Properties[i].Key] = &def.Properties[i]
722752
}
723753
for _, pg := range widget.PropertyGroups {
724-
collectTopLevelOrder(pg, def, byKey)
754+
collectTopLevelOrder(pg, "", def, byKey)
725755
}
726756
return def
727757
}
@@ -732,17 +762,26 @@ func buildDefinition(widget *xmlWidget, version string) *WidgetDefinition {
732762
// PropertyDef (via byKey); system entries are recorded as Key + IsSystem. It does
733763
// not descend into an object property's nested properties — only the top-level
734764
// PropertyType order is reproduced here.
735-
func collectTopLevelOrder(pg xmlPropGroup, def *WidgetDefinition, byKey map[string]*PropertyDef) {
765+
func collectTopLevelOrder(pg xmlPropGroup, parentCategory string, def *WidgetDefinition, byKey map[string]*PropertyDef) {
766+
category := pg.Caption
767+
if parentCategory != "" && category != "" {
768+
category = parentCategory + "::" + category
769+
} else if parentCategory != "" {
770+
category = parentCategory
771+
}
736772
for _, c := range pg.Children {
737773
switch {
738774
case c.Property != nil:
739775
if pd := byKey[c.Property.Key]; pd != nil {
740776
def.AllTopLevel = append(def.AllTopLevel, *pd)
741777
}
742778
case c.SystemProp != nil:
743-
def.AllTopLevel = append(def.AllTopLevel, PropertyDef{Key: c.SystemProp.Key, IsSystem: true})
779+
// Category is the enclosing group chain (e.g. "General::Common"),
780+
// mirroring walkPropertyGroup; GenerateFromMPK emits it on the System
781+
// PropertyType so a generated widget matches mxbuild's definition.
782+
def.AllTopLevel = append(def.AllTopLevel, PropertyDef{Key: c.SystemProp.Key, IsSystem: true, Category: category})
744783
case c.SubGroup != nil:
745-
collectTopLevelOrder(*c.SubGroup, def, byKey)
784+
collectTopLevelOrder(*c.SubGroup, category, def, byKey)
746785
}
747786
}
748787
}

0 commit comments

Comments
 (0)