Skip to content

Commit 77d1761

Browse files
MendixMauclaude
andcommitted
fix(pages): null hidden object-list item TextTemplates under nested visibility rules (closes mendixlabs#1198)
A pluggable widget whose editorConfig hides an object-list ITEM sub-property (a nested rule, ListPropertyKey set) kept the shipped default text in that item's required TextTemplate even while the rule hid it. Studio Pro stores null there, so mx check reported CE0463. Found on File Uploader 2.5.0: allowedFileFormats[].typeFormatDescription is a required TextTemplate hidden when configMode = "simple", so every simple-mode format failed mx check. ApplyVisibilityRules now skips nested rules at the top level and evaluates them per item: when the rule is determinable from the item's own primitive values and fires, that item's TextTemplate is nulled. Only the hidden direction and only TextTemplate values are touched; an indeterminable rule leaves the item unchanged. Tested: make test (84 packages ok), gofmt, go vet. Mendix 11.12.2: simple-mode .txt format plus advanced .zip format, mx check 0 errors, runtime upload of both files stored, downloaded bytes sha256-equal to source (2/2), .csv rejected by the widget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DyrzWc5YejD1iBLgJ7TS7q
1 parent 9509176 commit 77d1761

2 files changed

Lines changed: 168 additions & 0 deletions

File tree

‎mdl/backend/widgetobj/builder.go‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,10 @@ func ApplyVisibilityRules(object bson.D, propertyTypeIDs map[string]pages.Proper
818818
hidden := make(map[string]bool, len(rules))
819819
conditional := make([]string, 0, len(rules))
820820
for _, rule := range rules {
821+
if rule.Nested() {
822+
// Item sub-property of an object list: evaluated per item below.
823+
continue
824+
}
821825
entry, ok := propertyTypeIDs[rule.PropertyKey]
822826
if !ok || entry.ValueType != "TextTemplate" {
823827
continue
@@ -853,6 +857,83 @@ func ApplyVisibilityRules(object bson.D, propertyTypeIDs map[string]pages.Proper
853857
return val
854858
})
855859
}
860+
return applyNestedVisibilityRules(object, propertyTypeIDs, rules)
861+
}
862+
863+
// applyNestedVisibilityRules nulls the TextTemplate of an object-list ITEM
864+
// sub-property that a nested rule (ListPropertyKey set) hides under that
865+
// item's own configuration.
866+
//
867+
// The File Uploader 2.5.0 `allowedFileFormats` item is the case that found it:
868+
// `typeFormatDescription` is a REQUIRED TextTemplate hidden when
869+
// `configMode = "simple"`. buildObjectListItemBSON fills unset required
870+
// templates with the shipped default text (#891), which is right while the
871+
// property is visible and CE0463 while it is hidden: Studio Pro stores null
872+
// there, and `mx update-widgets` rewrites exactly that field and nothing else.
873+
//
874+
// Only the hidden direction is applied. A visible item template is already
875+
// handled at item build time (#891 / emptyClientTemplateRules), and an
876+
// indeterminable rule leaves the item untouched.
877+
func applyNestedVisibilityRules(object bson.D, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, rules []types.WidgetVisibilityRule) bson.D {
878+
byList := map[string][]types.WidgetVisibilityRule{}
879+
for _, rule := range rules {
880+
if rule.Nested() {
881+
byList[rule.ListPropertyKey] = append(byList[rule.ListPropertyKey], rule)
882+
}
883+
}
884+
if len(byList) == 0 {
885+
return object
886+
}
887+
listKeys := make([]string, 0, len(byList))
888+
for k := range byList {
889+
listKeys = append(listKeys, k)
890+
}
891+
sort.Strings(listKeys)
892+
for _, listKey := range listKeys {
893+
entry, ok := propertyTypeIDs[listKey]
894+
if !ok || len(entry.NestedPropertyIDs) == 0 {
895+
continue
896+
}
897+
listRules := byList[listKey]
898+
object = updateWidgetPropertyValue(object, propertyTypeIDs, listKey, func(val bson.D) bson.D {
899+
for i, elem := range val {
900+
if elem.Key != "Objects" {
901+
continue
902+
}
903+
arr, ok := elem.Value.(bson.A)
904+
if !ok {
905+
continue
906+
}
907+
out := make(bson.A, len(arr))
908+
for j, it := range arr {
909+
item, ok := it.(bson.D)
910+
if !ok {
911+
out[j] = it
912+
continue
913+
}
914+
values := primitiveValuesOf(item, entry.NestedPropertyIDs)
915+
for _, rule := range listRules {
916+
sub, ok := entry.NestedPropertyIDs[rule.PropertyKey]
917+
if !ok || sub.ValueType != "TextTemplate" {
918+
continue
919+
}
920+
fires, determinable := rule.Fires(func(c types.WidgetVisibilityCondition) (string, bool) {
921+
v, ok := values[c.PropertyKey]
922+
return v, ok
923+
})
924+
if determinable && fires {
925+
item = updateWidgetPropertyValue(item, entry.NestedPropertyIDs, rule.PropertyKey, func(v bson.D) bson.D {
926+
return setBSONField(v, "TextTemplate", nil)
927+
})
928+
}
929+
}
930+
out[j] = item
931+
}
932+
val[i] = bson.E{Key: "Objects", Value: out}
933+
}
934+
return val
935+
})
936+
}
856937
return object
857938
}
858939

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package widgetobj
4+
5+
import (
6+
"testing"
7+
8+
"go.mongodb.org/mongo-driver/bson"
9+
10+
"github.com/mendixlabs/mxcli/mdl/types"
11+
"github.com/mendixlabs/mxcli/sdk/pages"
12+
)
13+
14+
// TestApplyPropertyVisibility_NestedObjectListItem locks in the File Uploader
15+
// 2.5.0 fix: `allowedFileFormats` items carry a required TextTemplate
16+
// `typeFormatDescription` that is hidden when the item's `configMode` is
17+
// "simple". Studio Pro stores null there; a populated template is CE0463. The
18+
// rule is nested (ListPropertyKey set) and must be evaluated per item, against
19+
// that item's own configMode.
20+
func TestApplyPropertyVisibility_NestedObjectListItem(t *testing.T) {
21+
const (
22+
listID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
23+
modeID = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
24+
descID = "cccccccc-cccc-cccc-cccc-cccccccccccc"
25+
objType = "dddddddd-dddd-dddd-dddd-dddddddddddd"
26+
)
27+
populated := bson.D{{Key: "$Type", Value: "Forms$ClientTemplate"}}
28+
mkProp := func(id, prim string, tt any) bson.D {
29+
return bson.D{
30+
{Key: "$Type", Value: "CustomWidgets$WidgetProperty"},
31+
{Key: "TypePointer", Value: types.UUIDToBlob(id)},
32+
{Key: "Value", Value: bson.D{
33+
{Key: "$Type", Value: "CustomWidgets$WidgetValue"},
34+
{Key: "PrimitiveValue", Value: prim},
35+
{Key: "TextTemplate", Value: tt},
36+
}},
37+
}
38+
}
39+
mkItem := func(mode string) bson.D {
40+
return bson.D{
41+
{Key: "$Type", Value: "CustomWidgets$WidgetObject"},
42+
{Key: "TypePointer", Value: types.UUIDToBlob(objType)},
43+
{Key: "Properties", Value: bson.A{int32(2), mkProp(modeID, mode, nil), mkProp(descID, "", populated)}},
44+
}
45+
}
46+
listProp := bson.D{
47+
{Key: "$Type", Value: "CustomWidgets$WidgetProperty"},
48+
{Key: "TypePointer", Value: types.UUIDToBlob(listID)},
49+
{Key: "Value", Value: bson.D{
50+
{Key: "$Type", Value: "CustomWidgets$WidgetValue"},
51+
{Key: "Objects", Value: bson.A{int32(2), mkItem("simple"), mkItem("advanced")}},
52+
}},
53+
}
54+
ob := &Builder{
55+
object: bson.D{{Key: "Properties", Value: bson.A{int32(2), listProp}}},
56+
propertyTypeIDs: map[string]pages.PropertyTypeIDEntry{
57+
"allowedFileFormats": {PropertyTypeID: listID, ValueType: "Object", ObjectTypeID: objType,
58+
NestedPropertyIDs: map[string]pages.PropertyTypeIDEntry{
59+
"configMode": {PropertyTypeID: modeID, ValueType: "Enumeration"},
60+
"typeFormatDescription": {PropertyTypeID: descID, ValueType: "TextTemplate", Required: true},
61+
}},
62+
},
63+
}
64+
ob.ApplyPropertyVisibility([]types.WidgetVisibilityRule{{
65+
PropertyKey: "typeFormatDescription",
66+
ListPropertyKey: "allowedFileFormats",
67+
HiddenWhen: &types.WidgetVisibilityCondition{PropertyKey: "configMode", Operator: "eq", Value: "simple"},
68+
}})
69+
70+
var items bson.A
71+
for _, e := range ob.object {
72+
if e.Key == "Properties" {
73+
val := findField(t, e.Value.(bson.A)[1].(bson.D), "Value").(bson.D)
74+
items = findField(t, val, "Objects").(bson.A)
75+
}
76+
}
77+
descOf := func(item bson.D) any {
78+
props := findField(t, item, "Properties").(bson.A)
79+
return findField(t, findField(t, props[2].(bson.D), "Value").(bson.D), "TextTemplate")
80+
}
81+
if tt := descOf(items[1].(bson.D)); tt != nil {
82+
t.Errorf("simple item: typeFormatDescription TextTemplate = %v, want nil", tt)
83+
}
84+
if tt := descOf(items[2].(bson.D)); tt == nil {
85+
t.Errorf("advanced item: typeFormatDescription TextTemplate was nulled; it is visible and must be kept")
86+
}
87+
}

0 commit comments

Comments
 (0)