diff --git a/sdk/widgets/augment.go b/sdk/widgets/augment.go index 0b3132a1..2542a563 100644 --- a/sdk/widgets/augment.go +++ b/sdk/widgets/augment.go @@ -87,8 +87,17 @@ func AugmentTemplate(tmpl *WidgetTemplate, def *mpk.WidgetDefinition) error { } } - // Nothing to add/remove - if len(missing) == 0 && len(stale) == 0 { + // Check if nested augmentation is needed (skip early return if so) + hasNestedChildren := false + for _, p := range def.Properties { + if len(p.Children) > 0 { + hasNestedChildren = true + break + } + } + + // Nothing to add/remove at top level, and no nested children to process + if len(missing) == 0 && len(stale) == 0 && !hasNestedChildren { return nil } @@ -131,10 +140,237 @@ func AugmentTemplate(tmpl *WidgetTemplate, def *mpk.WidgetDefinition) error { } } - // Write back + // Write back top-level setArrayField(objType, "PropertyTypes", propTypes) setArrayField(tmpl.Object, "Properties", objProps) + // Augment nested ObjectType properties (e.g., DataGrid2 column properties). + // Top-level augmentation syncs the property list, but nested ObjectTypes inside + // IsList Object properties also need syncing when the .mpk version differs + // from the template version. + for _, mpkProp := range def.Properties { + if len(mpkProp.Children) == 0 { + continue + } + if err := augmentNestedObjectType(propTypes, objProps, mpkProp); err != nil { + return fmt.Errorf("augment nested %s: %w", mpkProp.Key, err) + } + } + + return nil +} + +// augmentNestedObjectType syncs nested ObjectType PropertyTypes for an Object-type property. +// When a .mpk defines children for a property (e.g., DataGrid2 "columns" has showContentAs, +// attribute, content, header, etc.), this function ensures the template's nested ObjectType +// has the same PropertyTypes as the .mpk, adding missing ones and removing stale ones. +func augmentNestedObjectType(propTypes []any, objProps []any, mpkProp mpk.PropertyDef) error { + // Find the PropertyType matching this .mpk property + var matchedPT map[string]any + var matchedPTID string + for _, pt := range propTypes { + ptMap, ok := pt.(map[string]any) + if !ok { + continue + } + key, _ := ptMap["PropertyKey"].(string) + if key == mpkProp.Key { + matchedPT = ptMap + matchedPTID, _ = ptMap["$ID"].(string) + break + } + } + if matchedPT == nil { + return nil // PropertyType not found; top-level augmentation should have added it + } + + // Navigate to ValueType.ObjectType.PropertyTypes + vt, ok := getMapField(matchedPT, "ValueType") + if !ok { + return nil + } + nestedObjType, ok := getMapField(vt, "ObjectType") + if !ok || nestedObjType == nil { + return nil + } + nestedPropTypes, ok := getArrayField(nestedObjType, "PropertyTypes") + if !ok { + return nil + } + + // Build set of existing nested property keys + existingKeys := make(map[string]bool) + nestedExemplars := make(map[string]int) + for i, npt := range nestedPropTypes { + nptMap, ok := npt.(map[string]any) + if !ok { + continue + } + key, _ := nptMap["PropertyKey"].(string) + if key == "" { + continue + } + existingKeys[key] = true + + // Record exemplar by value type + nvt, ok := getMapField(nptMap, "ValueType") + if ok { + vtType, _ := nvt["Type"].(string) + if vtType != "" { + if _, exists := nestedExemplars[vtType]; !exists { + nestedExemplars[vtType] = i + } + } + } + } + + // Build set of .mpk child keys + mpkChildKeys := make(map[string]bool) + for _, child := range mpkProp.Children { + mpkChildKeys[child.Key] = true + } + + // Find missing (in .mpk but not in template nested ObjectType) + var missing []mpk.PropertyDef + for _, child := range mpkProp.Children { + if !existingKeys[child.Key] { + missing = append(missing, child) + } + } + + // Find stale (in template but not in .mpk) + var staleKeys []string + for key := range existingKeys { + if !mpkChildKeys[key] { + staleKeys = append(staleKeys, key) + } + } + + if len(missing) == 0 && len(staleKeys) == 0 { + return nil + } + + // Also find the corresponding Object WidgetProperty and its nested WidgetObjects + var nestedObjProps [][]any // one per WidgetObject in the Objects array + var objPropContainers []map[string]any + for _, prop := range objProps { + propMap, ok := prop.(map[string]any) + if !ok { + continue + } + tp, _ := propMap["TypePointer"].(string) + if tp != matchedPTID { + continue + } + // Navigate to Value.Objects + val, ok := getMapField(propMap, "Value") + if !ok { + continue + } + objects, ok := getArrayField(val, "Objects") + if !ok { + continue + } + for _, obj := range objects { + objMap, ok := obj.(map[string]any) + if !ok { + continue + } + props, ok := getArrayField(objMap, "Properties") + if ok { + nestedObjProps = append(nestedObjProps, props) + objPropContainers = append(objPropContainers, objMap) + } + } + break + } + + // Remove stale nested PropertyTypes and Properties + if len(staleKeys) > 0 { + staleSet := make(map[string]bool, len(staleKeys)) + for _, key := range staleKeys { + staleSet[key] = true + } + // Collect IDs of stale PropertyTypes + staleIDs := make(map[string]bool) + var filteredPropTypes []any + for _, npt := range nestedPropTypes { + nptMap, ok := npt.(map[string]any) + if !ok { + filteredPropTypes = append(filteredPropTypes, npt) + continue + } + key, _ := nptMap["PropertyKey"].(string) + if staleSet[key] { + id, _ := nptMap["$ID"].(string) + if id != "" { + staleIDs[id] = true + } + continue + } + filteredPropTypes = append(filteredPropTypes, npt) + } + nestedPropTypes = filteredPropTypes + + // Remove matching Properties from each WidgetObject + for i, nop := range nestedObjProps { + var filtered []any + for _, prop := range nop { + propMap, ok := prop.(map[string]any) + if !ok { + filtered = append(filtered, prop) + continue + } + tp, _ := propMap["TypePointer"].(string) + if staleIDs[tp] { + continue + } + filtered = append(filtered, prop) + } + nestedObjProps[i] = filtered + } + } + + // Add missing nested PropertyTypes and Properties + for _, child := range missing { + bsonType := xmlTypeToBSONType(child.Type) + if bsonType == "" { + continue + } + + exemplarIdx, hasExemplar := nestedExemplars[bsonType] + var newPropType, newProp map[string]any + if hasExemplar && len(nestedObjProps) > 0 { + var err error + newPropType, newProp, err = clonePropertyPair(nestedPropTypes, nestedObjProps[0], exemplarIdx, child) + if err != nil { + return fmt.Errorf("clone nested property %q: %w", child.Key, err) + } + } + if newPropType == nil || newProp == nil { + newPropType, newProp = createPropertyPair(child, bsonType) + } + + if newPropType != nil { + nestedPropTypes = append(nestedPropTypes, newPropType) + } + if newProp != nil { + for i := range nestedObjProps { + nestedObjProps[i] = append(nestedObjProps[i], newProp) + } + } + } + + // Write back nested PropertyTypes + setArrayField(nestedObjType, "PropertyTypes", nestedPropTypes) + + // Write back nested Object Properties + for i, container := range objPropContainers { + if i < len(nestedObjProps) { + setArrayField(container, "Properties", nestedObjProps[i]) + } + } + return nil } diff --git a/sdk/widgets/augment_test.go b/sdk/widgets/augment_test.go index 0c5ddbab..05d93548 100644 --- a/sdk/widgets/augment_test.go +++ b/sdk/widgets/augment_test.go @@ -697,3 +697,199 @@ func bsonTypeToXmlType(bsonType string) string { return "string" } } + +func TestAugmentTemplate_NestedObjectType_AddMissing(t *testing.T) { + ResetPlaceholderCounter() + + // Template has an Object-type property "columns" with one nested property "header" + tmpl := &WidgetTemplate{ + WidgetID: "test.DataGrid", + Type: map[string]any{ + "$ID": "type0001", + "$Type": "CustomWidgets$CustomWidgetType", + "ObjectType": map[string]any{ + "$ID": "objtype0001", + "$Type": "CustomWidgets$WidgetObjectType", + "PropertyTypes": []any{ + float64(2), + map[string]any{ + "$ID": "pt-columns", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Columns", + "Category": "General", + "Description": "", + "IsDefault": false, + "PropertyKey": "columns", + "ValueType": map[string]any{ + "$ID": "vt-columns", + "$Type": "CustomWidgets$WidgetValueType", + "Type": "Object", + "IsList": true, + "Required": false, + "ObjectType": map[string]any{ + "$ID": "nested-objtype", + "$Type": "CustomWidgets$WidgetObjectType", + "PropertyTypes": []any{ + float64(2), + map[string]any{ + "$ID": "npt-header", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Header", + "Category": "General", + "Description": "", + "IsDefault": false, + "PropertyKey": "header", + "ValueType": map[string]any{ + "$ID": "nvt-header", + "$Type": "CustomWidgets$WidgetValueType", + "Type": "TextTemplate", + "DefaultValue": "", + "Required": false, + "IsList": false, + }, + }, + }, + }, + "DefaultValue": "", + "DataSourceProperty": "", + "EnumerationValues": []any{float64(2)}, + "ReturnType": nil, + "AllowNonPersistableEntities": false, + "AllowedTypes": []any{float64(1)}, + "AssociationTypes": []any{float64(1)}, + "DefaultType": "None", + "EntityProperty": "", + "IsLinked": false, + }, + }, + }, + }, + }, + Object: map[string]any{ + "$ID": "obj0001", + "$Type": "CustomWidgets$WidgetObject", + "Properties": []any{ + float64(2), + map[string]any{ + "$ID": "prop-columns", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "pt-columns", + "Value": map[string]any{ + "$ID": "val-columns", + "$Type": "CustomWidgets$WidgetValue", + "TypePointer": "vt-columns", + "Objects": []any{ + float64(2), + map[string]any{ + "$ID": "col-obj-1", + "$Type": "CustomWidgets$WidgetObject", + "TypePointer": "nested-objtype", + "Properties": []any{ + float64(2), + map[string]any{ + "$ID": "col-prop-header", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "npt-header", + "Value": map[string]any{ + "$ID": "col-val-header", + "$Type": "CustomWidgets$WidgetValue", + "TypePointer": "nvt-header", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + // .mpk defines "columns" with two children: "header" (existing) and "tooltip" (new) + def := &mpk.WidgetDefinition{ + ID: "test.DataGrid", + Name: "DataGrid", + Properties: []mpk.PropertyDef{ + { + Key: "columns", + Type: "object", + Children: []mpk.PropertyDef{ + {Key: "header", Type: "textTemplate", Caption: "Header"}, + {Key: "tooltip", Type: "textTemplate", Caption: "Tooltip"}, + }, + }, + }, + } + + err := AugmentTemplate(tmpl, def) + if err != nil { + t.Fatalf("AugmentTemplate failed: %v", err) + } + + // Verify nested ObjectType now has 2 PropertyTypes + objType, _ := getMapField(tmpl.Type, "ObjectType") + propTypes, _ := getArrayField(objType, "PropertyTypes") + // Find the columns PropertyType + for _, pt := range propTypes { + ptMap, ok := pt.(map[string]any) + if !ok { + continue + } + if ptMap["PropertyKey"] != "columns" { + continue + } + vt, _ := getMapField(ptMap, "ValueType") + nestedOT, _ := getMapField(vt, "ObjectType") + nestedPTs, _ := getArrayField(nestedOT, "PropertyTypes") + + // Count non-marker entries + count := 0 + hasTooltip := false + for _, npt := range nestedPTs { + nptMap, ok := npt.(map[string]any) + if !ok { + continue + } + count++ + if nptMap["PropertyKey"] == "tooltip" { + hasTooltip = true + } + } + if count != 2 { + t.Errorf("expected 2 nested PropertyTypes, got %d", count) + } + if !hasTooltip { + t.Error("expected nested PropertyType 'tooltip' to be added") + } + } + + // Verify nested Object Properties also has the new property + objProps, _ := getArrayField(tmpl.Object, "Properties") + for _, prop := range objProps { + propMap, ok := prop.(map[string]any) + if !ok { + continue + } + if propMap["TypePointer"] != "pt-columns" { + continue + } + val, _ := getMapField(propMap, "Value") + objects, _ := getArrayField(val, "Objects") + for _, obj := range objects { + objMap, ok := obj.(map[string]any) + if !ok { + continue + } + nestedProps, _ := getArrayField(objMap, "Properties") + count := 0 + for _, np := range nestedProps { + if _, ok := np.(map[string]any); ok { + count++ + } + } + if count != 2 { + t.Errorf("expected 2 nested Object Properties, got %d", count) + } + } + } +}