Skip to content

Commit 753c346

Browse files
akoclaude
andcommitted
feat: extract object-list properties to .def.json (Phase 1 Layer 1+2)
Refs: mendixlabs#538 (Phase 1 of v0.10.0 milestone) Pluggable widgets like Accordion (groups), PopupMenu (basicItems / customItems), AreaChart (series), Maps (markers / dynamicMarkers), and the headline DataGrid (columns) have Type: "Object" + IsList: true properties — structured object lists with their own sub-property trees. Until now the widget init extraction silently dropped them, leaving these widgets non-functional via the pluggable engine. Layer 1: extend WidgetDefinition with ObjectLists []ObjectListMapping. Each mapping has an MDL keyword (singular form of the property key, e.g. groups → GROUP, basicItems → ITEM, series → SERIES) plus ItemProperties (scalar / attribute / datasource / texttemplate / etc. operations) and ItemSlots (widgets-typed sub-properties). Existing .def.json files stay valid (no field = behaves as before). Layer 2: extend mxcli widget init to detect object-type list properties in the MPK XML and emit ObjectListMapping entries. Sub-property trees recurse through both XML shapes Mendix uses: (a) <properties><propertyGroup><property>...</property></propertyGroup></properties> (Accordion, DataGrid) (b) <properties><property>...</property></properties> (PopupMenu, Maps) Verified extraction against the headline widgets: Accordion groups → GROUP (10 props, 2 slots) DataGrid columns → COLUMN (19 props, 2 slots) PopupMenu basicItems → ITEM (5 props) PopupMenu customItems → CUSTOMITEM (2 props, 1 slot) AreaChart series → SERIES (24 props) Maps markers → MARKER (7 props) Maps dynamicMarkers → DYNAMICMARKER (8 props) Layer 3 (grammar/visitor/executor for MDL child-block syntax) follows in a separate commit; this commit unblocks it by giving the engine the metadata it needs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 483c6d8 commit 753c346

3 files changed

Lines changed: 162 additions & 12 deletions

File tree

cmd/mxcli/cmd_widget.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,13 @@ func generateDefJSON(mpkDef *mpk.WidgetDefinition, mdlName string) *executor.Wid
164164
// Two passes: datasource first (association depends on entityContext set by datasource).
165165
var assocMappings []executor.PropertyMapping
166166
for _, p := range mpkDef.Properties {
167+
// Object-list properties (e.g. Accordion `groups`, DataGrid `columns`)
168+
// are emitted as ObjectListMapping entries. Each list item exposes its
169+
// own sub-property tree.
170+
if p.Type == "object" && p.IsList {
171+
def.ObjectLists = append(def.ObjectLists, makeObjectListMapping(p))
172+
continue
173+
}
167174
switch p.Type {
168175
case "widgets":
169176
container := strings.ToUpper(p.Key)
@@ -217,6 +224,93 @@ func generateDefJSON(mpkDef *mpk.WidgetDefinition, mdlName string) *executor.Wid
217224
return def
218225
}
219226

227+
// makeObjectListMapping converts an MPK object-list PropertyDef (e.g. Accordion
228+
// `groups`) into an ObjectListMapping. The MDL keyword is the singular form of
229+
// the property key (groups → GROUP, basicItems → ITEM, series → SERIES,
230+
// markers → MARKER). Sub-properties are walked into ItemProperties (scalar /
231+
// datasource / attribute / etc.) and ItemSlots (widgets-typed children).
232+
func makeObjectListMapping(p mpk.PropertyDef) executor.ObjectListMapping {
233+
mapping := executor.ObjectListMapping{
234+
PropertyKey: p.Key,
235+
MDLContainer: deriveObjectListKeyword(p.Key),
236+
}
237+
for _, child := range p.Children {
238+
if child.Type == "widgets" {
239+
mapping.ItemSlots = append(mapping.ItemSlots, executor.ItemSlotMapping{
240+
PropertyKey: child.Key,
241+
MDLContainer: strings.ToUpper(child.Key),
242+
Operation: "widgets",
243+
})
244+
continue
245+
}
246+
op := operationForType(child.Type)
247+
if op == "" {
248+
continue
249+
}
250+
item := executor.ItemPropertyMapping{
251+
PropertyKey: child.Key,
252+
Operation: op,
253+
}
254+
switch op {
255+
case "attribute":
256+
item.Source = "Attribute"
257+
case "datasource":
258+
item.Source = "DataSource"
259+
case "association":
260+
item.Source = "Association"
261+
case "primitive":
262+
if child.DefaultValue != "" {
263+
item.Value = child.DefaultValue
264+
}
265+
}
266+
mapping.ItemProperties = append(mapping.ItemProperties, item)
267+
}
268+
return mapping
269+
}
270+
271+
// deriveObjectListKeyword turns a property key like "groups" / "basicItems" /
272+
// "series" / "markers" into an uppercase MDL keyword in the singular form.
273+
// The singular is computed by stripping a trailing "s" from the lowercased key,
274+
// with overrides for English-irregular cases (series, items with prefixes, etc.).
275+
func deriveObjectListKeyword(propertyKey string) string {
276+
overrides := map[string]string{
277+
"basicItems": "ITEM",
278+
"customItems": "CUSTOMITEM",
279+
"dynamicMarkers": "DYNAMICMARKER",
280+
"attributesList": "ATTR",
281+
"filterOptions": "OPTION",
282+
"series": "SERIES", // Latin singular == plural
283+
}
284+
if k, ok := overrides[propertyKey]; ok {
285+
return k
286+
}
287+
lower := strings.ToLower(propertyKey)
288+
singular := strings.TrimSuffix(lower, "s")
289+
return strings.ToUpper(singular)
290+
}
291+
292+
// operationForType maps an MPK property type to the engine's operation name.
293+
// Returns "" for unsupported types (which are skipped in object-list extraction).
294+
func operationForType(t string) string {
295+
switch t {
296+
case "attribute":
297+
return "attribute"
298+
case "association":
299+
return "association"
300+
case "datasource":
301+
return "datasource"
302+
case "textTemplate":
303+
return "texttemplate"
304+
case "expression":
305+
return "expression"
306+
case "action":
307+
return "action"
308+
case "boolean", "integer", "decimal", "string", "enumeration":
309+
return "primitive"
310+
}
311+
return ""
312+
}
313+
220314
func runWidgetInit(cmd *cobra.Command, args []string) error {
221315
projectPath, _ := cmd.Flags().GetString("project")
222316
projectDir := filepath.Dir(projectPath)

mdl/executor/widget_engine.go

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@ const defaultSlotContainer = "template"
2525
// WidgetDefinition describes how to construct a pluggable widget from MDL syntax.
2626
// Loaded from embedded JSON definition files (*.def.json).
2727
type WidgetDefinition struct {
28-
WidgetID string `json:"widgetId"`
29-
MDLName string `json:"mdlName"`
30-
WidgetKind string `json:"widgetKind,omitempty"` // "pluggable" (React) or "custom" (legacy Dojo)
31-
TemplateFile string `json:"templateFile"`
32-
DefaultEditable string `json:"defaultEditable"`
33-
PropertyMappings []PropertyMapping `json:"propertyMappings,omitempty"`
34-
ChildSlots []ChildSlotMapping `json:"childSlots,omitempty"`
35-
Modes []WidgetMode `json:"modes,omitempty"`
28+
WidgetID string `json:"widgetId"`
29+
MDLName string `json:"mdlName"`
30+
WidgetKind string `json:"widgetKind,omitempty"` // "pluggable" (React) or "custom" (legacy Dojo)
31+
TemplateFile string `json:"templateFile"`
32+
DefaultEditable string `json:"defaultEditable"`
33+
PropertyMappings []PropertyMapping `json:"propertyMappings,omitempty"`
34+
ChildSlots []ChildSlotMapping `json:"childSlots,omitempty"`
35+
ObjectLists []ObjectListMapping `json:"objectLists,omitempty"`
36+
Modes []WidgetMode `json:"modes,omitempty"`
3637
}
3738

3839
// PropertyMapping maps an MDL source (attribute, association, literal, etc.)
@@ -65,6 +66,41 @@ type ChildSlotMapping struct {
6566
Operation string `json:"operation"`
6667
}
6768

69+
// ObjectListMapping maps an MDL child block keyword (e.g., GROUP, ITEM, SERIES)
70+
// to a pluggable widget property whose value is a list of structured objects
71+
// (Type: "Object" + IsList: true in the widget XML).
72+
//
73+
// Each list item has its own sub-property tree, expressed via ItemProperties.
74+
// ItemProperties supports the same operation kinds as top-level PropertyMapping
75+
// and ChildSlotMapping (primitive, attribute, datasource, widgets, texttemplate,
76+
// expression, action), as well as nested object lists.
77+
type ObjectListMapping struct {
78+
PropertyKey string `json:"propertyKey"`
79+
MDLContainer string `json:"mdlContainer"`
80+
ItemProperties []ItemPropertyMapping `json:"itemProperties,omitempty"`
81+
ItemSlots []ItemSlotMapping `json:"itemSlots,omitempty"`
82+
}
83+
84+
// ItemPropertyMapping maps a property of one object-list item (e.g. headerText
85+
// of an Accordion group) to its operation kind. Mirrors PropertyMapping but
86+
// scoped to the list item rather than the top-level widget.
87+
type ItemPropertyMapping struct {
88+
PropertyKey string `json:"propertyKey"`
89+
Source string `json:"source,omitempty"`
90+
Value string `json:"value,omitempty"`
91+
Operation string `json:"operation"`
92+
Default string `json:"default,omitempty"`
93+
}
94+
95+
// ItemSlotMapping maps a widget child slot inside one object-list item
96+
// (e.g. headerContent of an Accordion group, content of a DataGrid column).
97+
// Mirrors ChildSlotMapping but scoped to the list item.
98+
type ItemSlotMapping struct {
99+
PropertyKey string `json:"propertyKey"`
100+
MDLContainer string `json:"mdlContainer"`
101+
Operation string `json:"operation"`
102+
}
103+
68104
// BuildContext carries resolved values from MDL parsing for use by operations.
69105
type BuildContext struct {
70106
AttributePath string

sdk/widgets/mpk/mpk.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,11 @@ type xmlProperty struct {
8585
DataSource string `xml:"dataSource,attr"`
8686
Caption string `xml:"caption"`
8787
Description string `xml:"description"`
88-
// Nested properties for object type
89-
NestedProps []xmlPropGroup `xml:"properties>propertyGroup"`
88+
// Nested properties for object type — two XML shapes:
89+
// (a) <properties><propertyGroup><property>...</property></propertyGroup></properties>
90+
// (b) <properties><property>...</property></properties> (no group wrapper)
91+
NestedProps []xmlPropGroup `xml:"properties>propertyGroup"`
92+
NestedDirectProps []xmlProperty `xml:"properties>property"`
9093
}
9194

9295
// xmlSystemProp represents <systemProperty key="..."/> element.
@@ -237,11 +240,28 @@ func walkPropertyGroup(pg xmlPropGroup, parentCategory string, def *WidgetDefini
237240
DataSource: p.DataSource,
238241
}
239242

240-
// Parse nested properties for object-type properties
241-
if p.Type == "object" && len(p.NestedProps) > 0 {
243+
// Parse nested properties for object-type properties.
244+
// Two XML shapes coexist across Mendix widgets:
245+
// (a) <properties><propertyGroup><property>...</property></propertyGroup></properties>
246+
// (e.g. Accordion groups, DataGrid columns)
247+
// (b) <properties><property>...</property></properties>
248+
// (e.g. PopupMenu basicItems, Maps markers)
249+
if p.Type == "object" {
242250
for _, npg := range p.NestedProps {
243251
collectNestedProperties(npg, &prop)
244252
}
253+
for _, np := range p.NestedDirectProps {
254+
prop.Children = append(prop.Children, PropertyDef{
255+
Key: np.Key,
256+
Type: np.Type,
257+
Caption: np.Caption,
258+
Description: np.Description,
259+
Required: np.Required == "true",
260+
DefaultValue: np.DefaultValue,
261+
IsList: np.IsList == "true",
262+
DataSource: np.DataSource,
263+
})
264+
}
245265
}
246266

247267
def.Properties = append(def.Properties, prop)

0 commit comments

Comments
 (0)