From c59fe00b169dbe85fb9716a1bbbf582ca0af88bd Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 00:53:56 +0000 Subject: [PATCH 1/6] fix: describe keeps a gallery's sort when its source runs over an association (mendixlabs/mxcli#1599) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DESCRIBE PAGE printed a gallery whose "Database" source runs over an association as `$currentObject/Assoc` with no `sort by` and no `where`, and `create page` refused the faithful `database from $currentObject/Assoc/Entity` form for anything but a list view. Studio Pro stores that source as a CustomWidgets$CustomWidgetXPathSource with an IndirectEntityRef, its XPath and sort bar beside it. The reader treated it as an association source (the #941 reading), which drops both and replays as a Forms$AssociationSource — a different retrieve. - Reader: a pluggable XPath source with complete hops reads through parseEntityPathSource, as the list view source already does; a hop without a stored destination keeps the association reading. - Writer: on 10+ buildPluggableDataSourceV3 builds the form and customWidgetDataSourceToGen stores the hops as an IndirectEntityRef with the start parameter in SourceVariable (9.x keeps the #1399 database source). Widgets with their own builder still refuse it in buildDataSourceV3. Measured on 11.12.1: the reporter's shape authored in MDL, mx check 0 errors, describe -> exec "Unchanged page"; with the association byte-patched to a nonexistent name mx check reports CE1613 at Gallery 'g1'. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Hjeuk6NPBWq3QTxCKxsARF --- ...escribe-gallery-sort-over-association.json | 1 + docs-site/src/language/widget-types.md | 7 +- ...describe-gallery-sort-over-association.mdl | 41 +++++ .../cw_xpath_source_over_association_test.go | 64 ++++++++ mdl/backend/modelsdk/widget_write.go | 17 +- mdl/executor/cmd_pages_builder_v3.go | 44 ++--- .../cmd_pages_describe_assoc_context_test.go | 12 +- mdl/executor/cmd_pages_describe_datasource.go | 25 +-- ...listview_database_over_association_test.go | 25 ++- mdl/executor/pluggable_database_source.go | 15 +- .../pluggable_database_source_test.go | 8 +- .../pluggable_xpath_over_association_test.go | 152 ++++++++++++++++++ 12 files changed, 332 insertions(+), 79 deletions(-) create mode 100644 .claude/skills/fix-issue/findings/mdl-executor/2026-10-21-describe-gallery-sort-over-association.json create mode 100644 mdl-examples/bug-tests/1599-describe-gallery-sort-over-association.mdl create mode 100644 mdl/backend/modelsdk/cw_xpath_source_over_association_test.go create mode 100644 mdl/executor/pluggable_xpath_over_association_test.go diff --git a/.claude/skills/fix-issue/findings/mdl-executor/2026-10-21-describe-gallery-sort-over-association.json b/.claude/skills/fix-issue/findings/mdl-executor/2026-10-21-describe-gallery-sort-over-association.json new file mode 100644 index 0000000000..76e053255e --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-executor/2026-10-21-describe-gallery-sort-over-association.json @@ -0,0 +1 @@ +{"area":"mdl/executor","date":"2026-10-21","symptom":"`describe page` printed a gallery whose Database source runs over an association as `gallery g1 (DataSource: $currentObject/Probe.Item_Parent, …)` — no `sort by`, no `where`, although the stored CustomWidgets$CustomWidgetXPathSource carries a GridSortBar; and `create page` refused the faithful spelling with \"`database from $currentObject/…` is a LIST VIEW source reached over an association, and gallery cannot store one\"","cause":"parseDataSource sent an XPath source with an IndirectEntityRef to parseEntityPathSource only for Forms$ListViewXPathSource (#721 L5); the pluggable CustomWidgetXPathSource fell to parseContextSource (the #941 reading), which yields an association source and drops XPath and sort — and replays as a Forms$AssociationSource, a different retrieve. On the write side checkDatabaseOverAssociationIsOnAListView refused the form for every non-list-view widget on 10+, and customWidgetDataSourceToGen wrote only a DirectEntityRef","file":"`mdl/executor/cmd_pages_describe_datasource.go` (parseDataSource), `mdl/executor/pluggable_database_source.go` (buildPluggableDataSourceV3), `mdl/executor/cmd_pages_builder_v3.go` (check removed; buildDataSourceV3 refusal reworded), `mdl/backend/modelsdk/widget_write.go` (customWidgetDataSourceToGen: IndirectEntityRef + SourceVariable)","insight":"The 9.x fix for the same shape (#1399) already had the whole write path — buildDatabaseOverAssociationV3 and the version gate — so the 10+ case was a one-branch change once the reader stopped treating the XPath source as an association; check which source TYPE a reading replays to, not just whether the text parses. The #941 association reading was itself lossy (type change on replay) and only looked faithful because a source with no sort replays to the same rows. Measured on 11.12.1: the reporter's shape authored directly in MDL, mx check 0 errors, describe -> exec `Unchanged page`; control with the association name byte-patched to a nonexistent one -> CE1613 at Gallery 'g1', so mx check does read the path. A hop with no stored destination keeps the association reading","refs":["mendixlabs/mxcli#1599","ako/mxcli#1509"],"test":"mdl/executor/pluggable_xpath_over_association_test.go; mdl/backend/modelsdk/cw_xpath_source_over_association_test.go"} diff --git a/docs-site/src/language/widget-types.md b/docs-site/src/language/widget-types.md index 8c819ce0b7..759c7341aa 100644 --- a/docs-site/src/language/widget-types.md +++ b/docs-site/src/language/widget-types.md @@ -210,7 +210,12 @@ trailing association with no entity gets the end opposite the context. It is a different source from `$Task/System.WorkflowUserTask_Assignees` — an **association** source, which follows the association in memory and has no -XPath, sort or search. List views only; on any other widget it is refused. +XPath, sort or search. + +A gallery, a data grid and any other pluggable widget take the same form for +their "Database" source, with its XPath and sort (`search by` stays a list view +search bar). Widgets with a source of their own, such as a chart series or a +dynamic image, refuse it. #### Specialization templates diff --git a/mdl-examples/bug-tests/1599-describe-gallery-sort-over-association.mdl b/mdl-examples/bug-tests/1599-describe-gallery-sort-over-association.mdl new file mode 100644 index 0000000000..fcc182785e --- /dev/null +++ b/mdl-examples/bug-tests/1599-describe-gallery-sort-over-association.mdl @@ -0,0 +1,41 @@ +-- ============================================================================ +-- Bug test: mendixlabs/mxcli#1599 — DESCRIBE PAGE drops a gallery's sort when +-- its "Database" source runs over an association +-- ============================================================================ +-- +-- Studio Pro stores a gallery bound to "Database" over an association from its +-- context object as a CustomWidgets$CustomWidgetXPathSource whose EntityRef is +-- an IndirectEntityRef, with its XPath and sort bar beside it. DESCRIBE read it +-- as the association source `$currentObject/Assoc`, without the sort; replayed, +-- that is a Forms$AssociationSource — a different retrieve. MDL could not +-- author the shape at all: `create page` refused `database from +-- $currentObject/…` on a gallery. +-- +-- To verify: +-- 1. mxcli exec this script, then: describe page Bug1599.P1 +-- -> gallery g1 (DataSource: database from +-- $currentObject/Bug1599.Item_Parent/Bug1599.Item sort by Code asc, …) +-- 2. exec that output -> "Unchanged page Bug1599.P1". +-- 3. mx check: 0 errors (measured on 11.12.1). +-- ============================================================================ + +mdl 1; + +create module Bug1599; + +create persistent entity Bug1599.Parent (Name: String(50)); +create persistent entity Bug1599.Item (Code: String(20), Amount: Integer); +create association Bug1599.Item_Parent from Bug1599.Item to Bug1599.Parent; + +create page Bug1599.P1 (Title: 'P', Layout: Atlas_Core.Atlas_Default) { + listview lv (DataSource: database from Bug1599.Parent) { + gallery g1 ( + DataSource: database from $currentObject/Bug1599.Item_Parent/Bug1599.Item where [Amount > 0] sort by Code asc, + Selection: None + ) { + template { + dynamictext x (Content: '{1}', ContentParams: ({1} = Code)) + } + } + } +}; diff --git a/mdl/backend/modelsdk/cw_xpath_source_over_association_test.go b/mdl/backend/modelsdk/cw_xpath_source_over_association_test.go new file mode 100644 index 0000000000..8233b47d5e --- /dev/null +++ b/mdl/backend/modelsdk/cw_xpath_source_over_association_test.go @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 + +package modelsdkbackend + +import ( + "testing" + + "go.mongodb.org/mongo-driver/v2/bson" + + "github.com/mendixlabs/mxcli/modelsdk/codec" + "github.com/mendixlabs/mxcli/sdk/pages" +) + +// mendixlabs/mxcli#1599. On 10+ a gallery's "Database" source reached over an +// association is a CustomWidgets$CustomWidgetXPathSource whose EntityRef is an +// IndirectEntityRef of the hops, with its sort bar beside it — the shape the +// report's bson dump shows. The writer stored only a DirectEntityRef, so the +// hops (and with them the context restriction) could not be written at all. +func TestCustomWidgetXPathSourceToGen_OverAssociation(t *testing.T) { + el, err := customWidgetDataSourceToGen(&pages.DatabaseSource{ + EntityName: "Probe.Item", + EntitySteps: []pages.AttributeRefStep{{ + Association: "Probe.Item_Parent", DestinationEntity: "Probe.Item", + }}, + ContextVariable: "Parent", + Sorting: []*pages.GridSort{{ + AttributePath: "Probe.Item.Code", Direction: "Ascending", + }}, + }) + if err != nil { + t.Fatalf("customWidgetDataSourceToGen: %v", err) + } + raw, err := (&codec.Encoder{}).Encode(el) + if err != nil { + t.Fatalf("encode: %v", err) + } + var d bson.D + if err := bson.Unmarshal(raw, &d); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if got := lookup(d, "$Type"); got != "CustomWidgets$CustomWidgetXPathSource" { + t.Fatalf("$Type = %v, want CustomWidgets$CustomWidgetXPathSource", got) + } + ref, _ := lookup(d, "EntityRef").(bson.D) + if lookup(ref, "$Type") != "DomainModels$IndirectEntityRef" { + t.Fatalf("EntityRef = %v, want an IndirectEntityRef", ref) + } + steps, _ := lookup(ref, "Steps").(bson.A) + if len(steps) != 2 { + t.Fatalf("Steps = %v, want one hop", steps) + } + step, _ := steps[1].(bson.D) + if lookup(step, "Association") != "Probe.Item_Parent" || lookup(step, "DestinationEntity") != "Probe.Item" { + t.Errorf("step = %v, want Probe.Item_Parent → Probe.Item", step) + } + sv, _ := lookup(d, "SourceVariable").(bson.D) + if lookup(sv, "PageParameter") != "Parent" { + t.Errorf("SourceVariable = %v, want page parameter Parent", sv) + } + bar, _ := lookup(d, "SortBar").(bson.D) + if items, _ := lookup(bar, "SortItems").(bson.A); len(items) != 2 { + t.Errorf("SortBar.SortItems = %v, want one sort item", items) + } +} diff --git a/mdl/backend/modelsdk/widget_write.go b/mdl/backend/modelsdk/widget_write.go index c209843f8f..82e371fd19 100644 --- a/mdl/backend/modelsdk/widget_write.go +++ b/mdl/backend/modelsdk/widget_write.go @@ -1850,7 +1850,22 @@ func customWidgetDataSourceToGen(ds pages.DataSource) (element.Element, error) { assignID(src) src.SetForceFullObjects(false) src.SetXPathConstraint(d.XPathConstraint) - if d.EntityName != "" { + switch { + case len(d.EntitySteps) > 0: + // "Database" reached over associations from a context object: the + // hops as an IndirectEntityRef inside the XPath source, the start + // parameter in its SourceVariable — the shape listViewSourceToGen + // writes, and the one Studio Pro stores for a gallery or grid + // bound this way (mendixlabs/mxcli#1599). + src.SetEntityRef(pageEntityRefToGen(d.EntitySteps)) + if d.ContextVariable != "" { + kind := "" + if d.IsSnippetParameter { + kind = "snippet" + } + src.SetSourceVariable(sourceVariableToGen(d.ContextVariable, kind)) + } + case d.EntityName != "": ref := genDm.NewDirectEntityRef() assignID(ref) ref.SetEntityQualifiedName(d.EntityName) diff --git a/mdl/executor/cmd_pages_builder_v3.go b/mdl/executor/cmd_pages_builder_v3.go index bf140dff91..169da8d7db 100644 --- a/mdl/executor/cmd_pages_builder_v3.go +++ b/mdl/executor/cmd_pages_builder_v3.go @@ -368,12 +368,10 @@ func (pb *pageBuilder) buildWidgetV3(w *ast.WidgetV3) (pages.Widget, error) { if err := checkSearchByIsOnAListView(w); err != nil { return nil, err } - // On a 9.x project a pluggable widget stores this form too, as a - // CustomWidgetDatabaseSource (mendixlabs/mxcli#1399); a widget with its - // own builder still refuses it there, in buildDataSourceV3. - if err := checkDatabaseOverAssociationIsOnAListView(w); err != nil && !pb.customWidgetDatabaseSourceSupported() { - return nil, err - } + // `database from $ctx/Assoc/Entity` needs no check here: a list view and + // every widget the pluggable engine writes store it (buildPluggableDataSourceV3, + // mendixlabs/mxcli#1399, #1599), and a widget with its own builder refuses + // it in buildDataSourceV3. if err := checkWidgetHasName(w); err != nil { return nil, err } @@ -864,27 +862,6 @@ func checkSearchByIsOnAListView(w *ast.WidgetV3) error { w.Name, strings.ToLower(w.Type), strings.ToLower(w.Type))) } -// checkDatabaseOverAssociationIsOnAListView refuses `database from $ctx/Assoc/…` -// on a widget other than a list view. Only the List View writer stores a -// database source reached over an association (a Forms$ListViewXPathSource with -// an IndirectEntityRef); every other writer would store the entity alone and -// drop the path — a retrieve of every row instead of the context's -// (ako/mxcli#721 L5). A data view is refused by MDL-WIDGET09 whatever the form. -func checkDatabaseOverAssociationIsOnAListView(w *ast.WidgetV3) error { - if w == nil || strings.EqualFold(w.Type, "listview") || strings.EqualFold(w.Type, "dataview") { - return nil - } - ds := w.GetDataSource() - if ds == nil || ds.Type != "database" || ds.AssociationPath == "" { - return nil - } - return mdlerrors.NewValidation(fmt.Sprintf( - "widget %q (%s): `database from $%s/%s` is a LIST VIEW source reached over an association, "+ - "and %s cannot store one — use the association source `$%s/%s`, or a `listview`.", - w.Name, strings.ToLower(w.Type), ds.ContextVariable, ds.AssociationPath, - strings.ToLower(w.Type), ds.ContextVariable, ds.AssociationPath)) -} - // buildDataSourceV3 converts a V3 DataSource AST to a pages.DataSource. // Returns the datasource, the entity name for context, and any error. func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource, string, error) { @@ -929,14 +906,15 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource case "database": if ds.AssociationPath != "" { - // Only buildListViewDataSourceV3 may build this form. Every other - // caller — a widget-engine datasource under its own key, a chart - // series, a dynamic image — hands the result to a writer that stores - // the entity alone, a retrieve of every row instead of the context's + // Only buildListViewDataSourceV3 and buildPluggableDataSourceV3 may + // build this form. Every other caller — a chart series, a dynamic + // image — hands the result to a writer that stores the entity + // alone, a retrieve of every row instead of the context's // (ako/mxcli#721 L5). return nil, "", mdlerrors.NewValidationf( - "`database from $%s/%s` is a LIST VIEW source reached over an association; "+ - "only a `listview` DataSource can store one — use the association source `$%s/%s` here", + "`database from $%s/%s` is a database source reached over an association; "+ + "only a `listview` or a pluggable widget's DataSource (gallery, datagrid, …) can store one "+ + "— use the association source `$%s/%s` here", ds.ContextVariable, ds.AssociationPath, ds.ContextVariable, ds.AssociationPath) } // Database source: DATABASE Entity diff --git a/mdl/executor/cmd_pages_describe_assoc_context_test.go b/mdl/executor/cmd_pages_describe_assoc_context_test.go index 9525f207b9..49c9df60d7 100644 --- a/mdl/executor/cmd_pages_describe_assoc_context_test.go +++ b/mdl/executor/cmd_pages_describe_assoc_context_test.go @@ -147,20 +147,22 @@ func TestDescribe_AssociationSourceContextIsTheEntityReached(t *testing.T) { // The pluggable Gallery / Data grid 2 / chart datasource reads through the same // parser (#941): an XPath source with no entity and the association hops in -// EntityRef. +// EntityRef. It is a database retrieve over the association, spelled as one so +// its XPath and sort survive (mendixlabs/mxcli#1599); the context is the entity +// reached either way. func TestParseDataSource_PluggableAssociationSourceContextIsTheEntityReached(t *testing.T) { ds := parseDataSource(map[string]any{ "$Type": "CustomWidgets$CustomWidgetXPathSource", "EntityRef": indirectRef(assocStep("Shop.Order_Line", "Shop.OrderLine")), }) - if ds == nil || ds.Type != "association" { - t.Fatalf("parseDataSource = %+v, want an association source", ds) + if ds == nil || ds.Type != "database" { + t.Fatalf("parseDataSource = %+v, want a database source over the association", ds) } if got := dataSourceEntityContext(nil, ds); got != "Shop.OrderLine" { t.Errorf("dataSourceEntityContext = %q, want Shop.OrderLine", got) } - if got := dataSourceExpr(nil, ds); got != "$currentObject/Shop.Order_Line" { - t.Errorf("dataSourceExpr = %q, want the unchanged $currentObject/Shop.Order_Line", got) + if got := dataSourceExpr(nil, ds); got != "database from $currentObject/Shop.Order_Line/Shop.OrderLine" { + t.Errorf("dataSourceExpr = %q, want database from $currentObject/Shop.Order_Line/Shop.OrderLine", got) } } diff --git a/mdl/executor/cmd_pages_describe_datasource.go b/mdl/executor/cmd_pages_describe_datasource.go index 567d93030b..e457773df3 100644 --- a/mdl/executor/cmd_pages_describe_datasource.go +++ b/mdl/executor/cmd_pages_describe_datasource.go @@ -118,22 +118,23 @@ func parseDataSource(ds map[string]any) *rawDataSource { if got := parseEntitySource(ds); got != nil { return got } - // A List View's database source reached over an association from a - // context object. It is still a database retrieve — XPath, sort bar and - // search bar included — so it must not fall through to the association - // reading below, whose spelling executes as a Forms$AssociationSource - // and drops all three (ako/mxcli#721 L5). Only the List View source is - // known to mean this; the pluggable XPath source keeps the #941 reading. - if dsType == dsTypeListViewXPath { + // A database source reached over an association from a context object. + // It is still a database retrieve — XPath and sort bar included, and a + // List View's search bar — so it must not fall through to the + // association reading below, whose spelling executes as a + // Forms$AssociationSource and drops all three: a List View's + // (ako/mxcli#721 L5), and a Gallery's or Data grid 2's "Database" + // source, stored as a CustomWidgetXPathSource with the same + // IndirectEntityRef (mendixlabs/mxcli#1599). + if dsType == dsTypeListViewXPath || dsType == dsTypeCustomWidgetXPath { if got := parseEntityPathSource(ds); got != nil { return got } } - // An entity-backed source with no entity is really a context source: - // Studio Pro stores a pluggable list bound over an association or to a - // page parameter as an XPath source whose EntityRef is empty and whose - // navigation lives in Steps / SourceVariable / EntityPath. Reading only - // EntityRef produced `database from ` with an empty slot (#941). + // A context source: an XPath source whose navigation lives in + // SourceVariable / EntityPath, or whose hops lack a stored destination + // and so cannot be spelled as a database path. Reading only EntityRef + // produced `database from ` with an empty slot (#941). if got := parseContextSource(ds); got != nil { return got } diff --git a/mdl/executor/listview_database_over_association_test.go b/mdl/executor/listview_database_over_association_test.go index fc1d55b919..ea64f53fe3 100644 --- a/mdl/executor/listview_database_over_association_test.go +++ b/mdl/executor/listview_database_over_association_test.go @@ -121,21 +121,16 @@ func TestAssociationSugarStillParsesAsAssociation(t *testing.T) { } } -// Only the List View writer stores the path. A gallery or grid would write the -// entity alone — every row instead of the context's — so the builder refuses. -func TestDatabaseOverAssociationRefusedOffAListView(t *testing.T) { - prog, errs := visitor.Build(`create page W.P ( Title: 'P' ) { - gallery g (DataSource: database from $currentObject/W.A_B/W.B) { dynamictext t (Content: 'x') } -}`) - if len(errs) > 0 { - t.Fatalf("parse: %v", errs) - } - w := prog.Statements[0].(*ast.CreatePageStmtV3).Widgets[0] - if err := checkDatabaseOverAssociationIsOnAListView(w); err == nil { - t.Error("a gallery accepted `database from $currentObject/…`") +// A list view and the pluggable engine store the path; any other caller of +// buildDataSourceV3 (a chart series, a dynamic image) would write the entity +// alone — every row instead of the context's — so it refuses. +func TestDatabaseOverAssociationRefusedOutsideListViewAndPluggable(t *testing.T) { + pb := pluggableAssocPageBuilder(11, 12) + ds := &ast.DataSourceV3{Type: "database", ContextVariable: "currentObject", AssociationPath: "Probe.Item_Parent/Probe.Item"} + if _, _, err := pb.buildDataSourceV3(ds); err == nil { + t.Error("buildDataSourceV3 accepted `database from $currentObject/…`") } - w.Type = "listview" - if err := checkDatabaseOverAssociationIsOnAListView(w); err != nil { - t.Errorf("a list view was refused: %v", err) + if _, _, err := pb.buildPluggableDataSourceV3(ds); err != nil { + t.Errorf("a pluggable widget was refused: %v", err) } } diff --git a/mdl/executor/pluggable_database_source.go b/mdl/executor/pluggable_database_source.go index fff8ae45f1..d5e5123a0d 100644 --- a/mdl/executor/pluggable_database_source.go +++ b/mdl/executor/pluggable_database_source.go @@ -29,12 +29,15 @@ func (pb *pageBuilder) customWidgetDatabaseSourceSupported() bool { // buildPluggableDataSourceV3 builds the datasource of a widget the pluggable // engine writes. `database from $ctx/Assoc/Entity` is the one form it adds to -// buildDataSourceV3: on a 9.x project it is the CustomWidgetDatabaseSource -// Studio Pro writes for a repeater, gallery or grid bound to "Database" over an -// association, its WHERE stored as constraint rows (mendixlabs/mxcli#1399). -// Everywhere else the form keeps buildDataSourceV3's refusal. +// buildDataSourceV3: the "Database" source Studio Pro writes for a repeater, +// gallery or grid bound over an association. On 10+ that is a +// CustomWidgetXPathSource holding the hops as an IndirectEntityRef beside its +// XPath and sort bar (mendixlabs/mxcli#1599); on a 9.x project it is the +// CustomWidgetDatabaseSource, its WHERE stored as constraint rows +// (mendixlabs/mxcli#1399). A widget with its own builder keeps +// buildDataSourceV3's refusal. func (pb *pageBuilder) buildPluggableDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource, string, error) { - if ds == nil || ds.Type != "database" || ds.AssociationPath == "" || !pb.customWidgetDatabaseSourceSupported() { + if ds == nil || ds.Type != "database" || ds.AssociationPath == "" { return pb.buildDataSourceV3(ds) } built, entity, err := pb.buildDatabaseOverAssociationV3(ds) @@ -42,7 +45,7 @@ func (pb *pageBuilder) buildPluggableDataSourceV3(ds *ast.DataSourceV3) (pages.D return nil, "", err } src, ok := built.(*pages.DatabaseSource) - if !ok { + if !ok || !pb.customWidgetDatabaseSourceSupported() { return built, entity, nil } constraints, err := parseDatabaseConstraints(src.XPathConstraint, entity) diff --git a/mdl/executor/pluggable_database_source_test.go b/mdl/executor/pluggable_database_source_test.go index d6e68660b2..37415fa007 100644 --- a/mdl/executor/pluggable_database_source_test.go +++ b/mdl/executor/pluggable_database_source_test.go @@ -6,7 +6,6 @@ import ( "reflect" "testing" - "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/backend/mock" "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/pages" @@ -21,7 +20,8 @@ func pageBuilderAt(major, minor int) *pageBuilder { } // The source type exists in 9.x only: mx convert 10.12.19 rewrites every one -// into an XPath source. On 10+ the pluggable form stays refused, as before. +// into an XPath source, which is what 10+ builds (mendixlabs/mxcli#1599, +// TestBuildPluggableDataSource_DatabaseOverAssociationOn10). func TestCustomWidgetDatabaseSourceVersionGate(t *testing.T) { if !pageBuilderAt(9, 24).customWidgetDatabaseSourceSupported() { t.Error("9.24 does not get the 9.x database source") @@ -32,10 +32,6 @@ func TestCustomWidgetDatabaseSourceVersionGate(t *testing.T) { if (&pageBuilder{backend: &mock.MockBackend{}}).customWidgetDatabaseSourceSupported() { t.Error("an unknown version got the 9.x source") } - ds := &ast.DataSourceV3{Type: "database", ContextVariable: "P", AssociationPath: "M.A_B/M.A"} - if _, _, err := pageBuilderAt(10, 24).buildPluggableDataSourceV3(ds); err == nil { - t.Error("10.24 built `database from $P/…` on a pluggable widget") - } } // mendixlabs/mxcli#1399, write half. A 9.x "Database" source on a pluggable diff --git a/mdl/executor/pluggable_xpath_over_association_test.go b/mdl/executor/pluggable_xpath_over_association_test.go new file mode 100644 index 0000000000..0752e7cd10 --- /dev/null +++ b/mdl/executor/pluggable_xpath_over_association_test.go @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/mdl/visitor" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/domainmodel" + "github.com/mendixlabs/mxcli/sdk/pages" +) + +// gallerySortBar is the Forms$GridSortBar Studio Pro stores on a pluggable +// widget's XPath source: one ascending item on Probe.Item.Code. +func gallerySortBar() map[string]any { + return map[string]any{ + "$Type": "Forms$GridSortBar", + "SortItems": []any{int32(2), map[string]any{ + "$Type": "Forms$GridSortItem", + "AttributeRef": map[string]any{"$Type": "DomainModels$AttributeRef", "Attribute": "Probe.Item.Code"}, + "SortDirection": "Ascending", + }}, + } +} + +// mendixlabs/mxcli#1599: a gallery whose "Database" source runs over an +// association from its context object is stored as a +// CustomWidgets$CustomWidgetXPathSource with an IndirectEntityRef and a sort +// bar. Describe read it as the association source `$currentObject/Assoc`, +// which drops the sort (and the XPath) and re-executes as a +// Forms$AssociationSource — a different retrieve. +func TestParseDataSource_PluggableXPathOverAssociationKeepsSort(t *testing.T) { + ds := parseDataSource(map[string]any{ + "$Type": dsTypeCustomWidgetXPath, + "EntityRef": indirectRef(assocStep("Probe.Item_Parent", "Probe.Item")), + "XPathConstraint": "", + "SortBar": gallerySortBar(), + }) + if ds == nil { + t.Fatal("datasource dropped") + } + want := "database from $currentObject/Probe.Item_Parent/Probe.Item sort by Code asc" + if got := dataSourceExpr(nil, ds); got != want { + t.Errorf("dataSourceExpr = %q, want %q", got, want) + } + if got := dataSourceEntityContext(nil, ds); got != "Probe.Item" { + t.Errorf("dataSourceEntityContext = %q, want Probe.Item", got) + } +} + +// The XPath constraint rides along as well, and a page-parameter start is kept. +func TestParseDataSource_PluggableXPathOverAssociationKeepsWhereAndParameter(t *testing.T) { + ds := parseDataSource(map[string]any{ + "$Type": dsTypeCustomWidgetXPath, + "EntityRef": indirectRef(assocStep("Probe.Item_Parent", "Probe.Item")), + "SourceVariable": pageVariable("Parent"), + "XPathConstraint": "[Amount > 3]", + }) + got := dataSourceExpr(nil, ds) + want := "database from $Parent/Probe.Item_Parent/Probe.Item where [Amount > 3]" + if got != want { + t.Errorf("dataSourceExpr = %q, want %q", got, want) + } +} + +// Control for the change above: a hop with no stored destination cannot be +// spelled as a database path, and keeps the association reading (#941). +func TestParseDataSource_PluggableXPathWithoutDestinationStaysAssociation(t *testing.T) { + ds := parseDataSource(dsBSON(dsTypeCustomWidgetXPath, + "EntityRef", map[string]any{"Steps": []any{map[string]any{"Association": "Mod.Item_Bucket"}}}, + )) + if ds == nil || ds.Type != "association" { + t.Fatalf("parseDataSource = %+v, want an association source", ds) + } +} + +// Write half: on 10+ a pluggable widget builds `database from $ctx/Assoc/Entity` +// as a database source carrying the hops, its sort and the start variable, for +// customWidgetDataSourceToGen to store as a CustomWidgetXPathSource. +func TestBuildPluggableDataSource_DatabaseOverAssociationOn10(t *testing.T) { + pb := pluggableAssocPageBuilder(11, 12) + prog, errs := visitor.Build(`create page Probe.P ( Title: 'P' ) { + gallery g (DataSource: database from $currentObject/Probe.Item_Parent/Probe.Item sort by Code asc) { dynamictext t (Content: 'x') } +}`) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + w := prog.Statements[0].(*ast.CreatePageStmtV3).Widgets[0] + built, entity, err := pb.buildPluggableDataSourceV3(w.GetDataSource()) + if err != nil { + t.Fatalf("buildPluggableDataSourceV3: %v", err) + } + if entity != "Probe.Item" { + t.Errorf("entity = %q, want Probe.Item", entity) + } + src, ok := built.(*pages.DatabaseSource) + if !ok { + t.Fatalf("built %T, want *pages.DatabaseSource", built) + } + if src.UseConstraints { + t.Error("10+ got the 9.x constraint-row source") + } + if len(src.EntitySteps) != 1 || src.EntitySteps[0].Association != "Probe.Item_Parent" || src.EntitySteps[0].DestinationEntity != "Probe.Item" { + t.Errorf("EntitySteps = %+v, want one hop Probe.Item_Parent → Probe.Item", src.EntitySteps) + } + if len(src.Sorting) != 1 || !strings.HasSuffix(src.Sorting[0].AttributePath, "Code") || src.Sorting[0].Direction != pages.SortDirectionAscending { + t.Errorf("Sorting = %+v, want Code ascending", src.Sorting) + } +} + +// pluggableAssocPageBuilder is a page builder at the given Mendix version over +// the issue's domain model: Probe.Item_Parent from Probe.Item to Probe.Parent, +// with the context object a Probe.Parent (the enclosing list view's). +func pluggableAssocPageBuilder(major, minor int) *pageBuilder { + mod := &model.Module{BaseElement: model.BaseElement{ID: model.ID("mod-probe")}, Name: "Probe"} + b := &mock.MockBackend{ + ProjectVersionFunc: func() *types.ProjectVersion { + return &types.ProjectVersion{MajorVersion: major, MinorVersion: minor} + }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListDomainModelsFunc: func() ([]*domainmodel.DomainModel, error) { + return []*domainmodel.DomainModel{{ + BaseElement: model.BaseElement{ID: model.ID("dm-probe")}, + ContainerID: mod.ID, + Entities: []*domainmodel.Entity{ + {BaseElement: model.BaseElement{ID: model.ID("e-parent")}, Name: "Parent", + Attributes: []*domainmodel.Attribute{{Name: "Name"}}}, + {BaseElement: model.BaseElement{ID: model.ID("e-item")}, Name: "Item", + Attributes: []*domainmodel.Attribute{{Name: "Code"}, {Name: "Amount"}}}, + }, + Associations: []*domainmodel.Association{{ + BaseElement: model.BaseElement{ID: model.ID("a-item-parent")}, + Name: "Item_Parent", + ParentID: model.ID("e-item"), + ChildID: model.ID("e-parent"), + }}, + }}, nil + }, + } + return &pageBuilder{ + backend: b, + entityContext: "Probe.Parent", + paramScope: map[string]model.ID{}, + paramEntityNames: map[string]string{}, + widgetScope: map[string]model.ID{}, + } +} From 3240d49c9cb978944a26e6308e1ab16f7cad8329 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 00:53:57 +0000 Subject: [PATCH 2/6] fix: describe prints a combo box's and a data grid filter's settings by key (mendixlabs/mxcli#1586) DESCRIBE PAGE omitted expression-typed (and other) settings of the widgets with a dedicated describe reader: a combo box's customEditability / customEditabilityExpression / ariaRequired, a text or number filter's defaultValue expression and delay. Exec wrote them; describe -> edit -> exec reset them. The report's own case, a generic pluggable widget's top-level expression with a false MDL-WIDGET06 warning, reproduces only on the binary before mendixlabs/mxcli#1578 and is already fixed on main. - The combo box and the four data grid filters now run the by-key reader (#1573) beside their keyword reader, skipping the keys it already prints. - Values are compared with what exec writes for an absent key (the embedded template), not the schema DefaultValue: the combo box template stores readOnlyStyle `bordered` against a default of `text`, so Studio Pro's combo boxes (Administration.Account_Edit) lost `text` on an edit-replay. - A combo box's database/static-mode settings, a mode DESCRIBE does not spell, are named in a NOT re-executable note instead of printed. - `check` without -p tops the hand-crafted definitions up from the embedded template, so describe output passes it as it does with -p. Corpus diff against a baseline build over the Blank 11.12.1 app: only the intended lines change, every description replays "Unchanged", mx check 0. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Hjeuk6NPBWq3QTxCKxsARF --- ...scribe-combobox-filter-bykey-settings.json | 1 + .../bug-patterns/describe-round-trip-gaps.md | 12 ++ ...cribe-combobox-filter-expression-props.mdl | 54 +++++++ mdl/executor/cmd_pages_describe_bykey.go | 146 ++++++++++++++++-- mdl/executor/cmd_pages_describe_output.go | 6 +- mdl/executor/cmd_pages_describe_parse.go | 16 ++ .../cmd_pages_describe_shared_bykey_test.go | 103 ++++++++++++ mdl/executor/validate_widgets.go | 5 + mdl/executor/widget_known_props_from_mpk.go | 70 +++++++-- 9 files changed, 385 insertions(+), 28 deletions(-) create mode 100644 .claude/skills/fix-issue/findings/mdl-executor/2026-10-21-describe-combobox-filter-bykey-settings.json create mode 100644 mdl-examples/bug-tests/1586-describe-combobox-filter-expression-props.mdl create mode 100644 mdl/executor/cmd_pages_describe_shared_bykey_test.go diff --git a/.claude/skills/fix-issue/findings/mdl-executor/2026-10-21-describe-combobox-filter-bykey-settings.json b/.claude/skills/fix-issue/findings/mdl-executor/2026-10-21-describe-combobox-filter-bykey-settings.json new file mode 100644 index 0000000000..2612880d39 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-executor/2026-10-21-describe-combobox-filter-bykey-settings.json @@ -0,0 +1 @@ +{"area":"mdl/executor","date":"2026-10-21","symptom":"`describe page` omitted a combo box's `customEditability` / `customEditabilityExpression` and a data grid text/number filter's `defaultValue` expression and `delay`, though exec stored them (bson dump shows `Expression: $currentObject/Locked`); the report's own generic widget (`entitypicker … Placeholder:`) and its MDL-WIDGET06 \"not yet persisted\" warning reproduce only on the pre-#1578 binary","cause":"Widgets with a dedicated describe reader (isKnownCustomWidgetType) never ran extractExplicitProperties, and only Data grid 2 and Gallery had been given the by-key reader (#1573); the combo box and filter branch printed its keyword set and nothing else. Separately, nonDefaultPropsByKey compared with the schema DefaultValue, but exec writes the embedded template's value for an absent property — combo box readOnlyStyle is `bordered` in the template against a default of `text`","file":"`mdl/executor/cmd_pages_describe_bykey.go` (sharedBranchByKeySkip, templateWrittenDefaults, extractSharedBranchByKeyProps, withoutUndescribedComboBoxModes), `cmd_pages_describe_parse.go`, `cmd_pages_describe_output.go` (generic branch no longer claims a known widget with explicit props), `widget_known_props_from_mpk.go` + `validate_widgets.go` (enrichKnownPropertiesFromTemplates for check without -p)","insight":"Retest an old report against a pre-fix worktree before theorising: the reporter's exact shape was already fixed a day earlier, and only a probe on SHIPPED widgets with dedicated readers found what was still live. Compare by-key values with what exec writes when the key is absent (the template object), not the schema default — the corpus diff against a baseline build showed Studio Pro's own Administration combo boxes store `readOnlyStyle: text`, which describe omitted and an edit-replay would have flipped to `bordered`. Settings of a mode DESCRIBE does not spell (combo box database/static) are named in a NOT re-executable note, not printed into a combo box of another mode. And a key describe prints must pass `check` WITHOUT -p too: the hand-crafted defs only learn the widget's keys from the project's .mpk, so the embedded template now stands in when there is none. Not done: datagrid.json's template stores columnsFilterable=false against default true — the #1573 reader still compares with the default there","refs":["mendixlabs/mxcli#1586","ako/mxcli#1491","mendixlabs/mxcli#1578"],"test":"mdl/executor/cmd_pages_describe_shared_bykey_test.go"} diff --git a/docs-wiki/bug-patterns/describe-round-trip-gaps.md b/docs-wiki/bug-patterns/describe-round-trip-gaps.md index 743142f294..f10f8a40a1 100644 --- a/docs-wiki/bug-patterns/describe-round-trip-gaps.md +++ b/docs-wiki/bug-patterns/describe-round-trip-gaps.md @@ -8,6 +8,7 @@ sources: - .claude/skills/fix-issue/findings/mdl-executor/ - mdl/executor/cmd_workflows.go - mdl/executor/cmd_pages_describe_pluggable.go + - mdl/executor/cmd_pages_describe_bykey.go - mdl/executor/cmd_microflows_show_crossed.go - mdl/executor/cmd_microflows_normalize.go - mdl/executor/describe_graph_invariant_test.go @@ -153,6 +154,17 @@ The same third state appears without translations: an argument field Studio Pro stores as `Argument: ""` is neither the value `empty` nor absence, and a describer that conflates it with either cannot round-trip to `Unchanged`. +**"Default" means what exec writes for the key left out, not the schema's +`DefaultValue`.** A by-key reader prints what differs from the default, and the +two disagree wherever the embedded template stores something else — a combo +box's `readOnlyStyle` is `bordered` in the template against a default of +`text`. Compared with the schema, every mxcli-written widget prints a value +nobody authored (*invents*), and every Studio Pro one storing the schema default +prints nothing and replays as the template's value (*silently drops*). A reader +for a widget with a dedicated describer has to run alongside it, or every key +outside its keyword set is lost — and what it prints must also pass `check` +without `-p`, where only the built-in definition knows the widget's keys. + **Fixing one half is worse than the bug.** Where a describer has two defects at once — say, quoting *and* a missing property — shipping the quoting fix alone turns unparseable output into output that parses cleanly while silently dropping diff --git a/mdl-examples/bug-tests/1586-describe-combobox-filter-expression-props.mdl b/mdl-examples/bug-tests/1586-describe-combobox-filter-expression-props.mdl new file mode 100644 index 0000000000..8fff2d4625 --- /dev/null +++ b/mdl-examples/bug-tests/1586-describe-combobox-filter-expression-props.mdl @@ -0,0 +1,54 @@ +-- ============================================================================ +-- Bug test: mendixlabs/mxcli#1586 — DESCRIBE PAGE omits expression-type +-- pluggable widget properties +-- ============================================================================ +-- +-- A combo box and the data grid filters have their own DESCRIBE readers, which +-- knew only the keys MDL has keywords for. Every other setting — written by +-- exec's explicit-property pass — described back as nothing: a combo box's +-- `customEditabilityExpression`, a text filter's `defaultValue` expression and +-- `delay`. A describe -> edit -> exec cycle reset them to their defaults. +-- +-- (A generic pluggable widget's top-level expression, the report's own +-- `entitypicker … Placeholder:`, is covered by mendixlabs/mxcli#1578.) +-- +-- To verify: +-- 1. mxcli exec this script, then: describe page Bug1586.Edit and +-- describe page Bug1586.Grid +-- -> combobox cb (…, customEditability: 'conditionally', +-- customEditabilityExpression: $currentObject/Locked) +-- -> textfilter tf (defaultValue: '''abc''', delay: 300) +-- 2. exec each output -> "Unchanged page". +-- 3. mx check: 0 errors. +-- ============================================================================ + +mdl 1; + +create module Bug1586; + +create persistent entity Bug1586.Parent (Name: String(50)); +create persistent entity Bug1586.Rec (Code: String(20), Amount: Integer, Locked: Boolean); +create association Bug1586.Rec_Parent from Bug1586.Rec to Bug1586.Parent; + +create page Bug1586.Edit (Title: 'Edit', Layout: Atlas_Core.Atlas_Default, Params: ($Rec: Bug1586.Rec)) { + dataview dv (DataSource: $Rec) { + combobox cb ( + Attribute: Bug1586.Rec_Parent, + DataSource: database from Bug1586.Parent, + CaptionAttribute: Name, + customEditability: conditionally, + customEditabilityExpression: '$currentObject/Locked' + ) + } +}; + +create page Bug1586.Grid (Title: 'Grid', Layout: Atlas_Core.Atlas_Default) { + datagrid dg (DataSource: database from Bug1586.Rec) { + column (Attribute: Code, Caption: 'Code') { + textfilter tf (defaultValue: '''abc''', delay: 300) + } + column (Attribute: Amount, Caption: 'Amount') { + numberfilter nf (defaultValue: 5) + } + } +}; diff --git a/mdl/executor/cmd_pages_describe_bykey.go b/mdl/executor/cmd_pages_describe_bykey.go index 8ee3aec952..4e47dd5c03 100644 --- a/mdl/executor/cmd_pages_describe_bykey.go +++ b/mdl/executor/cmd_pages_describe_bykey.go @@ -5,6 +5,9 @@ package executor import ( "sort" "strings" + "sync" + + mwidgets "github.com/mendixlabs/mxcli/modelsdk/widgets" ) // Settings addressed by the widget's own property key. @@ -140,6 +143,20 @@ type byKeyOptions struct { // AttrEntity returns the entity a bare attribute name of property sc // resolves against on re-execution; "" when there is none. AttrEntity func(sc widgetPropSchema) string + // WrittenDefault, when set, returns the primitive or expression value exec + // writes for property key left out, where that is not the schema's + // DefaultValue (templateWrittenDefaults). + WrittenDefault func(key string) (string, bool) +} + +// defaultFor is the value a stored primitive or expression is compared with. +func (o byKeyOptions) defaultFor(sc widgetPropSchema) string { + if o.WrittenDefault != nil { + if v, ok := o.WrittenDefault(sc.Key); ok { + return v + } + } + return sc.Default } // nonDefaultPropsByKey returns, in key order, the properties of a stored widget @@ -170,13 +187,13 @@ func nonDefaultPropsByKey(ctx *ExecContext, objProps []any, schemas map[string]w continue case "Boolean", "Integer", "Decimal", "String", "Enumeration": pv := extractString(value["PrimitiveValue"]) - if pv == "" || pv == sc.Default { + if pv == "" || pv == opts.defaultFor(sc) { continue } props = append(props, rawExplicitProp{Key: sc.Key, Value: pv, ValueType: sc.Type}) case "Expression": expr := extractString(value["Expression"]) - if expr == "" || expr == sc.Default { + if expr == "" || expr == opts.defaultFor(sc) { continue } props = append(props, expressionByKeyProp(ctx, sc.Key, expr)) @@ -352,6 +369,97 @@ var galleryByKeySkip = map[string]bool{ "desktopItems": true, "tabletItems": true, "phoneItems": true, "onClickTrigger": true, } +// sharedBranchByKeySkip are, per widget, the keys the combo box's and the data +// grid filters' readers already print under an MDL keyword (Attribute:, +// DataSource:, CaptionAttribute:, Attributes:, FilterType:, the expression +// caption) or that select the mode those keywords imply. Every other scalar +// setting is printed by its own key. +var sharedBranchByKeySkip = map[string]map[string]bool{ + "combobox": { + "source": true, "optionsSourceType": true, + "attributeEnumeration": true, "attributeBoolean": true, "attributeAssociation": true, + "optionsSourceAssociationCaptionType": true, "optionsSourceAssociationCaptionAttribute": true, + "optionsSourceAssociationCaptionExpression": true, + }, + "textfilter": {"attrChoice": true, "defaultFilter": true}, + "numberfilter": {"attrChoice": true, "defaultFilter": true}, + "datefilter": {"attrChoice": true, "defaultFilter": true}, + "dropdownfilter": { + "baseType": true, "attrChoice": true, "attr": true, "refEntity": true, "refCaption": true, + }, +} + +// comboBoxUndescribedModePrefixes are the keys of the combo box's database and +// static option sources. DESCRIBE spells only the association and enumeration +// modes, so these settings are named as not reproduced rather than printed: on +// their own, without the mode and its datasource, they would be written into a +// combo box in another mode — an attribute resolved against the wrong entity. +var comboBoxUndescribedModePrefixes = []string{ + "optionsSourceDatabase", "optionsSourceStatic", "staticDataSource", "staticAttribute", "databaseAttributeString", +} + +// withoutUndescribedComboBoxModes moves the settings of a combo box's +// undescribed option-source modes from props to omitted. +func withoutUndescribedComboBoxModes(props []rawExplicitProp, omitted []string) ([]rawExplicitProp, []string) { + kept := props[:0:0] + for _, p := range props { + undescribed := false + for _, prefix := range comboBoxUndescribedModePrefixes { + if strings.HasPrefix(p.Key, prefix) { + undescribed = true + break + } + } + if undescribed { + omitted = append(omitted, p.Key) + continue + } + kept = append(kept, p) + } + sort.Strings(omitted) + return kept, omitted +} + +var templateWrittenDefaultsCache sync.Map // widgetID → map[key]string + +// templateWrittenDefaults returns, per property key, the primitive or +// expression value the widget's embedded template stores — which is what exec +// writes for a property left out. It is not always the schema's DefaultValue: +// the combo box template stores `readOnlyStyle: bordered` against a default of +// `text`, so comparing with the default printed `readOnlyStyle: 'bordered'` on +// every combo box mxcli wrote, and left a Studio Pro one's `text` unprinted — +// which re-executed as `bordered`. Empty for a widget with no embedded template. +func templateWrittenDefaults(widgetID string) map[string]string { + if v, ok := templateWrittenDefaultsCache.Load(widgetID); ok { + return v.(map[string]string) + } + out := map[string]string{} + if tmpl, err := mwidgets.GetTemplate(widgetID); err == nil && tmpl != nil { + keys := map[string]string{} + for id, sc := range widgetPropertySchemas(map[string]any{"Type": tmpl.Type}) { + keys[id] = sc.Key + } + for _, prop := range getBsonArrayElements(tmpl.Object["Properties"]) { + propMap, ok := prop.(map[string]any) + if !ok { + continue + } + key := keys[extractBinaryID(propMap["TypePointer"])] + value, ok := propMap["Value"].(map[string]any) + if key == "" || !ok { + continue + } + if pv := extractString(value["PrimitiveValue"]); pv != "" { + out[key] = pv + } else if expr := extractString(value["Expression"]); expr != "" { + out[key] = expr + } + } + } + templateWrittenDefaultsCache.Store(widgetID, out) + return out +} + // extractDataGrid2ByKeyProps returns the grid-level settings of a Data grid 2 // that DESCRIBE has no keyword for and that differ from their defaults, and the // keys it could not print. enclosingEntity is the entity of the data container @@ -369,19 +477,35 @@ func extractGalleryByKeyProps(ctx *ExecContext, w map[string]any, enclosingEntit } func extractWidgetByKeyProps(ctx *ExecContext, w map[string]any, skip map[string]bool, enclosingEntity, ownEntity string) ([]rawExplicitProp, []string) { + return widgetByKeyProps(ctx, w, byKeyOptions{Skip: skip}, enclosingEntity, ownEntity) +} + +// extractSharedBranchByKeyProps is extractWidgetByKeyProps for a combo box or +// a data grid filter, compared with what exec writes for a property left out +// (templateWrittenDefaults) rather than with the schema's DefaultValue. +func extractSharedBranchByKeyProps(ctx *ExecContext, w map[string]any, skip map[string]bool, enclosingEntity, ownEntity string) ([]rawExplicitProp, []string) { + written := templateWrittenDefaults(extractCustomWidgetID(ctx, w)) + return widgetByKeyProps(ctx, w, byKeyOptions{ + Skip: skip, + WrittenDefault: func(key string) (string, bool) { + v, ok := written[key] + return v, ok + }, + }, enclosingEntity, ownEntity) +} + +func widgetByKeyProps(ctx *ExecContext, w map[string]any, opts byKeyOptions, enclosingEntity, ownEntity string) ([]rawExplicitProp, []string) { obj, ok := w["Object"].(map[string]any) if !ok { return nil, nil } - return nonDefaultPropsByKey(ctx, getBsonArrayElements(obj["Properties"]), widgetPropertySchemas(w), byKeyOptions{ - Skip: skip, - AttrEntity: func(sc widgetPropSchema) string { - if sc.DataSourceProperty != "" { - return ownEntity - } - return enclosingEntity - }, - }) + opts.AttrEntity = func(sc widgetPropSchema) string { + if sc.DataSourceProperty != "" { + return ownEntity + } + return enclosingEntity + } + return nonDefaultPropsByKey(ctx, getBsonArrayElements(obj["Properties"]), widgetPropertySchemas(w), opts) } // extractDataGrid2ColumnByKeyProps is extractDataGrid2ByKeyProps for one diff --git a/mdl/executor/cmd_pages_describe_output.go b/mdl/executor/cmd_pages_describe_output.go index b6c58fcabd..65f5363a83 100644 --- a/mdl/executor/cmd_pages_describe_output.go +++ b/mdl/executor/cmd_pages_describe_output.go @@ -884,7 +884,7 @@ func outputWidgetMDLV3(ctx *ExecContext, w rawWidget, indent int) { // twice (#721 C). props = appendAppearanceProps(ctx, props, w) formatWidgetProps(ctx.Output, prefix, header, props, "\n") - } else if (len(w.ExplicitProperties) > 0 || len(w.ObjectLists) > 0 || w.OnClick != "" || + } else if ((len(w.ExplicitProperties) > 0 && !isKnownCustomWidgetType(widgetType)) || len(w.ObjectLists) > 0 || w.OnClick != "" || (w.OnChange != "" && !isKnownCustomWidgetType(widgetType)) || len(w.NamedActions) > 0 || len(w.ChildSlots) > 0 || len(w.OmittedContainers) > 0) && w.WidgetID != "" { // Generic pluggable widget with explicit properties, object-list child @@ -1021,7 +1021,11 @@ func outputWidgetMDLV3(ctx *ExecContext, w rawWidget, indent int) { if w.FilterExpression != "" && w.FilterExpression != "contains" { props = append(props, fmt.Sprintf("FilterType: %s", w.FilterExpression)) } + // The settings with no keyword, by the widget's own key + // (mendixlabs/mxcli#1586), and a note for those not reproduced. + props = appendExplicitProps(ctx, props, w.ExplicitProperties) props = appendAppearanceProps(ctx, props, w) + writeOmittedPropsNote(ctx.Output, prefix, w.OmittedProps) formatWidgetProps(ctx.Output, prefix, header, props, "\n") } diff --git a/mdl/executor/cmd_pages_describe_parse.go b/mdl/executor/cmd_pages_describe_parse.go index 0502004d44..119b4e2dd6 100644 --- a/mdl/executor/cmd_pages_describe_parse.go +++ b/mdl/executor/cmd_pages_describe_parse.go @@ -607,6 +607,22 @@ func parseRawWidget(ctx *ExecContext, w map[string]any, parentEntityContext ...s } } } + // Every other setting of a combo box or a data grid filter, by the + // widget's own key. Their readers above know only the keys MDL has a + // keyword for, so a stored `customEditabilityExpression` or a filter's + // `defaultValue` expression — written by exec's explicit-property pass + // — described back as nothing, and a describe → exec cycle reset it + // (mendixlabs/mxcli#1586). + if skip, ok := sharedBranchByKeySkip[widget.RenderMode]; ok { + ownEntity := "" + if widget.DataSource != nil { + ownEntity = dataSourceEntityContext(ctx, widget.DataSource) + } + widget.ExplicitProperties, widget.OmittedProps = extractSharedBranchByKeyProps(ctx, w, skip, inheritedCtx, ownEntity) + if widget.RenderMode == "combobox" { + widget.ExplicitProperties, widget.OmittedProps = withoutUndescribedComboBoxModes(widget.ExplicitProperties, widget.OmittedProps) + } + } // For pluggable Image widget, extract image-specific properties if widget.RenderMode == "image" { extractImageProperties(ctx, w, &widget) diff --git a/mdl/executor/cmd_pages_describe_shared_bykey_test.go b/mdl/executor/cmd_pages_describe_shared_bykey_test.go new file mode 100644 index 0000000000..a88e7719ef --- /dev/null +++ b/mdl/executor/cmd_pages_describe_shared_bykey_test.go @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/mendixlabs/mxcli/modelsdk/widgets" +) + +// widgetFromTemplate is an untouched stored widget: the shipped template's +// schema and the values exec writes for a property left out. +func widgetFromTemplate(t *testing.T, widgetID, name string) map[string]any { + t.Helper() + tmpl, err := widgets.GetTemplate(widgetID) + if err != nil || tmpl == nil { + t.Fatalf("%s template: %v, %v", widgetID, tmpl, err) + } + raw, err := json.Marshal(map[string]any{"Type": tmpl.Type, "Object": tmpl.Object}) + if err != nil { + t.Fatal(err) + } + var w map[string]any + if err := json.Unmarshal(raw, &w); err != nil { + t.Fatal(err) + } + normalizeTemplateJSON(w) + w["$Type"] = "CustomWidgets$CustomWidget" + w["Name"] = name + return w +} + +const comboBoxWidgetID = "com.mendix.widget.web.combobox.Combobox" + +// mendixlabs/mxcli#1586: a combo box's expression-typed settings are written by +// exec's explicit-property pass and were never described — the combo box has +// its own reader, which knew only the keys MDL has keywords for. A describe → +// exec cycle reset `customEditabilityExpression` to `false`. +func TestDescribeComboBox_PrintsExpressionSettingsByKey(t *testing.T) { + w := widgetFromTemplate(t, comboBoxWidgetID, "cb") + gridValue(t, w, "customEditability")["PrimitiveValue"] = "conditionally" + gridValue(t, w, "customEditabilityExpression")["Expression"] = "$currentObject/Locked" + gridValue(t, w, "ariaRequired")["Expression"] = "true" + + got := describeParsed(t, w) + for _, want := range []string{ + "customEditability: 'conditionally'", + "customEditabilityExpression: $currentObject/Locked", + "ariaRequired: true", + } { + if !strings.Contains(got, want) { + t.Errorf("describe lacks %q:\n%s", want, got) + } + } +} + +// Control: an untouched combo box prints none of them. The template stores +// `readOnlyStyle: bordered` against a schema default of `text`, and exec writes +// the template's value for a property left out — so that is the value compared +// with, or every combo box mxcli wrote would print it. +func TestDescribeComboBox_UntouchedPrintsNothingByKey(t *testing.T) { + w := widgetFromTemplate(t, comboBoxWidgetID, "cb") + got := describeParsed(t, w) + if strings.Contains(got, "readOnlyStyle") || strings.Contains(got, "NOT re-executable") { + t.Errorf("untouched combo box printed settings:\n%s", got) + } + // The schema default, which Studio Pro stores, differs from what exec + // writes, so it is printed. + gridValue(t, w, "readOnlyStyle")["PrimitiveValue"] = "text" + if got := describeParsed(t, w); !strings.Contains(got, "readOnlyStyle: 'text'") { + t.Errorf("a stored readOnlyStyle text was not printed:\n%s", got) + } +} + +// A combo box in a mode DESCRIBE does not spell has its settings named, not +// printed into a combo box of another mode. +func TestDescribeComboBox_UndescribedModeIsNamedNotPrinted(t *testing.T) { + w := widgetFromTemplate(t, comboBoxWidgetID, "cb") + gridValue(t, w, "optionsSourceDatabaseCaptionType")["PrimitiveValue"] = "expression" + got := describeParsed(t, w) + if strings.Contains(got, "optionsSourceDatabaseCaptionType:") { + t.Errorf("database-mode setting printed:\n%s", got) + } + if !strings.Contains(got, "-- NOT re-executable: optionsSourceDatabaseCaptionType") { + t.Errorf("database-mode setting not named:\n%s", got) + } +} + +// The data grid filters share the combo box's branch and had the same loss: a +// text filter's `defaultValue` expression and its `delay`. +func TestDescribeTextFilter_PrintsSettingsByKey(t *testing.T) { + w := widgetFromTemplate(t, "com.mendix.widget.web.datagridtextfilter.DatagridTextFilter", "tf") + gridValue(t, w, "defaultValue")["Expression"] = "'abc'" + gridValue(t, w, "delay")["PrimitiveValue"] = "300" + got := describeParsed(t, w) + for _, want := range []string{"defaultValue: '''abc'''", "delay: 300"} { + if !strings.Contains(got, want) { + t.Errorf("describe lacks %q:\n%s", want, got) + } + } +} diff --git a/mdl/executor/validate_widgets.go b/mdl/executor/validate_widgets.go index 8138073c11..6c26961f29 100644 --- a/mdl/executor/validate_widgets.go +++ b/mdl/executor/validate_widgets.go @@ -68,6 +68,11 @@ func LoadWidgetRegistry(projectPath string) *WidgetRegistry { // from the same .mpk DESCRIBE parses. See // widget_known_props_from_mpk.go for why this is not a list of nine. enrichKnownPropertiesFromMPK(registry, projectPath) + } else { + // No project to read packages from: the embedded template is the + // widget's schema, so check without -p accepts what DESCRIBE prints + // by key (mendixlabs/mxcli#1586). + enrichKnownPropertiesFromTemplates(registry) } return registry } diff --git a/mdl/executor/widget_known_props_from_mpk.go b/mdl/executor/widget_known_props_from_mpk.go index 2f0e38cfe7..095c58f9f9 100644 --- a/mdl/executor/widget_known_props_from_mpk.go +++ b/mdl/executor/widget_known_props_from_mpk.go @@ -7,6 +7,7 @@ import ( "sort" "strings" + mwidgets "github.com/mendixlabs/mxcli/modelsdk/widgets" "github.com/mendixlabs/mxcli/modelsdk/widgets/mpk" ) @@ -70,29 +71,66 @@ func enrichKnownPropertiesFromMPK(r *WidgetRegistry, projectPath string) { if !ok { continue } - allowed, _ := allowedWidgetProperties(def) - seen := make(map[string]bool, len(def.KnownProperties)) - for _, k := range def.KnownProperties { - seen[strings.ToLower(k)] = true - } - var added []string + var keys []string for _, p := range props { - if p.Key == "" || p.IsSystem { - continue + if !p.IsSystem { + keys = append(keys, p.Key) } - l := strings.ToLower(p.Key) - if allowed[l] || seen[l] { - continue + } + addKnownProperties(def, keys) + } +} + +// enrichKnownPropertiesFromTemplates is enrichKnownPropertiesFromMPK for a +// check with no project: the widget's embedded template stands in for its +// installed package. Without it the hand-crafted definitions rejected, as +// MDL-WIDGET01, the settings DESCRIBE prints for a combo box or a data grid +// filter by the widget's own key — so the description failed a check that +// the same check with -p passes. +func enrichKnownPropertiesFromTemplates(r *WidgetRegistry) { + if r == nil { + return + } + for _, def := range r.byWidgetID { + tmpl, err := mwidgets.GetTemplate(def.WidgetID) + if err != nil || tmpl == nil { + continue + } + var keys []string + for _, p := range propsFromTemplate(tmpl.Type) { + if !strings.EqualFold(p.Type, "System") { + keys = append(keys, p.Key) } - seen[l] = true - added = append(added, p.Key) } - if len(added) == 0 { + addKnownProperties(def, keys) + } +} + +// addKnownProperties appends to def.KnownProperties each key it neither maps +// nor already knows, in sorted order. +func addKnownProperties(def *WidgetDefinition, keys []string) { + allowed, _ := allowedWidgetProperties(def) + seen := make(map[string]bool, len(def.KnownProperties)) + for _, k := range def.KnownProperties { + seen[strings.ToLower(k)] = true + } + var added []string + for _, key := range keys { + if key == "" { + continue + } + l := strings.ToLower(key) + if allowed[l] || seen[l] { continue } - sort.Strings(added) - def.KnownProperties = append(def.KnownProperties, added...) + seen[l] = true + added = append(added, key) + } + if len(added) == 0 { + return } + sort.Strings(added) + def.KnownProperties = append(def.KnownProperties, added...) } // mpkPropertiesByWidgetID parses every package in the project's widgets/ folder From bdd7973138eb65a78b807210a4601a713703322f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 00:53:57 +0000 Subject: [PATCH 3/6] test: bug-test for describe of a snippet's root data source (mendixlabs/mxcli#1520) The snippet half of the report: a data view sourced from a snippet parameter described without its DataSource. Already fixed by the snippet-parameter slot read (ako/mxcli#721 L3); retested on 11.12.1, describe prints `DataSource: $View` and replays "Unchanged snippet". The page half was fixed by ako/mxcli#1416. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Hjeuk6NPBWq3QTxCKxsARF --- .../1520-describe-snippet-root-datasource.mdl | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 mdl-examples/bug-tests/1520-describe-snippet-root-datasource.mdl diff --git a/mdl-examples/bug-tests/1520-describe-snippet-root-datasource.mdl b/mdl-examples/bug-tests/1520-describe-snippet-root-datasource.mdl new file mode 100644 index 0000000000..ee93d94fb6 --- /dev/null +++ b/mdl-examples/bug-tests/1520-describe-snippet-root-datasource.mdl @@ -0,0 +1,28 @@ +-- ============================================================================ +-- Bug test: mendixlabs/mxcli#1520 — DESCRIBE SNIPPET omits the root data +-- source of a snippet +-- ============================================================================ +-- +-- A data view inside a snippet, sourced from a snippet parameter, stores the +-- parameter in Forms$PageVariable.SnippetParameter with PageParameter empty. +-- DESCRIBE read PageParameter alone and printed the data view with no +-- DataSource, so exec of the described text failed with CE0488 (fixed with +-- ako/mxcli#721 L3; this script keeps the shape the report names). +-- +-- To verify: +-- 1. mxcli exec this script, then: describe snippet Bug1520S.S +-- -> dataview dv (DataSource: $View, …) +-- 2. exec that output -> "Unchanged snippet Bug1520S.S". +-- ============================================================================ + +mdl 1; + +create module Bug1520S; + +create persistent entity Bug1520S.Rec (Label: String(50)); + +create snippet Bug1520S.S (Params: ($View: Bug1520S.Rec)) { + dataview dv (DataSource: $View) { + textbox tb (Attribute: Label) + } +}; From 06f9adcc390a544e507264cbc2b6b895f19fb3aa Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 01:02:29 +0000 Subject: [PATCH 4/6] fix(check): MDL-WIDGET42 judges a known-but-unmapped key by its template type A key a widget definition knows but does not map (a combo box's emptyOptionText) was skipped by MDL-WIDGET42 on the assumption that MDL-WIDGET01/06 spoke for it. Neither does once the key is persisted by the explicit-property pass, so `emptyOptionText: 'a' + 'b'` passed `check -p` and failed at exec. With the template enrichment for a check without -p (mendixlabs/mxcli#1586) the same gap opened there, which TestWidgetExpressionInAPlainProperty_IsAnError caught. The kind now comes from the widget's embedded template, as it already did for a widget with no definition. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Hjeuk6NPBWq3QTxCKxsARF --- mdl/executor/validate_widget_expression_value.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/mdl/executor/validate_widget_expression_value.go b/mdl/executor/validate_widget_expression_value.go index 4ba898ed02..4af0d74504 100644 --- a/mdl/executor/validate_widget_expression_value.go +++ b/mdl/executor/validate_widget_expression_value.go @@ -62,8 +62,17 @@ func validateWidgetExpressionValuesIn(w *ast.WidgetV3, def *WidgetDefinition, it continue } op, known := expressionValueOperation(key, def, item) - if !known && def == nil && item == nil { - op, known = schema.operation(key) + if !known && item == nil { + if def == nil { + op, known = schema.operation(key) + } else { + // A key the definition knows but does not map — a combo box's + // `emptyOptionText` — is written by the explicit-property pass + // with the kind its template declares, and a kind other than + // Expression refuses the expression at exec. Decide it here + // from the same template (mendixlabs/mxcli#1586). + op, known = loadEmbeddedSchema(def.WidgetID, "").operation(key) + } } switch { case op == "expression": From b07f7d9acd53bd719b82d7268bfc0fe854ef0171 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 05:42:01 +0000 Subject: [PATCH 5/6] =?UTF-8?q?test(roundtrip):=20a=20combo=20box's=20name?= =?UTF-8?q?d=20datasource=20keeps=20`database=20from=20$ctx/=E2=80=A6`=20(?= =?UTF-8?q?mendixlabs/mxcli#1599)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestDatabaseOverAssociationRefusedUnderANamedDatasourceKey asserted the refusal this PR removes, on the premise that the pluggable writer stored the entity alone. It now stores the hops as an IndirectEntityRef with the start parameter in SourceVariable, so the probe asserts the path survives exec -> describe instead. Measured on 11.12.1 with the same combo-box shape: mx check 0 errors, describe -> exec "Unchanged page". WorkflowCommons.WorkflowGroup_Edit now passes every law on TestApp, so it leaves knownFailures, which may only shrink. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Hjeuk6NPBWq3QTxCKxsARF --- ...listview_database_over_association_test.go | 25 +++++++++---------- mdl/roundtrip/testapp_allowlist_test.go | 1 - 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/mdl/roundtrip/listview_database_over_association_test.go b/mdl/roundtrip/listview_database_over_association_test.go index fd53c6f27c..2e3e534bcb 100644 --- a/mdl/roundtrip/listview_database_over_association_test.go +++ b/mdl/roundtrip/listview_database_over_association_test.go @@ -163,14 +163,13 @@ func firstListViewSource(t *testing.T, unit []byte) []byte { return ds } -// Only the List View writer stores the path. A datasource property the widget -// engine reaches under its own key — here a combo box's -// optionsSourceAssociationDataSource — bypassed the `DataSource:` guard: exec -// reported "Created page" and the pluggable writer stored the entity alone, a -// retrieve of every row instead of the context's (describe read it back as -// `database from Rules.BusinessRule`). Every non-List View call site of the -// datasource builder must refuse it. -func TestDatabaseOverAssociationRefusedUnderANamedDatasourceKey(t *testing.T) { +// A combo box's named datasource takes `database from $ctx/Assoc/Entity` and +// keeps the path: the pluggable writer stores it as a CustomWidgetXPathSource +// with the hops in an IndirectEntityRef and the start parameter in its +// SourceVariable, so the retrieve stays the context's +// (mendixlabs/mxcli#1599). It used to be refused, because that writer stored +// the entity alone and every row would have been retrieved. +func TestDatabaseOverAssociationKeptUnderANamedDatasourceKey(t *testing.T) { h := newFixtureHarness(t, testApp) defer h.close() h.restore() @@ -188,11 +187,11 @@ func TestDatabaseOverAssociationRefusedUnderANamedDatasourceKey(t *testing.T) { ) } };`) - if err == nil { - t.Fatal("a combo box's named datasource accepted `database from $ctx/Assoc/Entity` — " + - "its writer stores the entity alone, so every row would be retrieved") + if err != nil { + t.Fatalf("exec: %v", err) } - if !strings.Contains(err.Error(), "list view") && !strings.Contains(err.Error(), "LIST VIEW") { - t.Errorf("refused for another reason: %v", err) + const want = "database from $RuleCategory/Rules.BusinessRule_RuleCategory/Rules.BusinessRule" + if got := h.mustDescribeMdl0(t, "page Rules.L5NamedKeyProbe"); !strings.Contains(got, want) { + t.Errorf("the combo box's datasource lost its path — describe lacks %q:\n%s", want, got) } } diff --git a/mdl/roundtrip/testapp_allowlist_test.go b/mdl/roundtrip/testapp_allowlist_test.go index 3a15447150..1f61b39ac5 100644 --- a/mdl/roundtrip/testapp_allowlist_test.go +++ b/mdl/roundtrip/testapp_allowlist_test.go @@ -146,7 +146,6 @@ var testAppKnownFailures = map[string]knownFailure{ "page WorkflowCommons.WorkflowDefinition_CleanUp": {laws: []law{lawGetPut}, issue: "#721", why: "page: breaks getput on TestApp, measured when it joined the harness (#743); not yet triaged into #721's classes"}, "page WorkflowCommons.WorkflowDefinition_View": {laws: []law{lawExec}, issue: "#721", why: "page: breaks exec on TestApp, measured when it joined the harness (#743); not yet triaged into #721's classes"}, "page WorkflowCommons.WorkflowEndedUserTask": {laws: []law{lawGetPut}, issue: "#721", why: "page: breaks getput on TestApp, measured when it joined the harness (#743); not yet triaged into #721's classes"}, - "page WorkflowCommons.WorkflowGroup_Edit": {laws: []law{lawExec}, issue: "#721", why: "page: breaks exec on TestApp, measured when it joined the harness (#743); not yet triaged into #721's classes"}, "page WorkflowCommons.WorkflowGroup_User_Select": {laws: []law{lawGetPut}, issue: "#721", why: "page: breaks getput on TestApp, measured when it joined the harness (#743); not yet triaged into #721's classes"}, "page WorkflowCommons.WorkflowSubProcess_View": {laws: []law{lawExec}, issue: "#721", why: "page: breaks exec on TestApp, measured when it joined the harness (#743); not yet triaged into #721's classes"}, "page WorkflowCommons.WorkflowUserTaskView_View": {laws: []law{lawGetPut}, issue: "#721 #826", why: "executes since describe prints snippet call Params (#826); the page Appearance keeps 1 of 3 DesignProperties (#721 C)"}, From 3751ce7c765b379dcdc08a716ec5283223632ec3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 05:46:17 +0000 Subject: [PATCH 6/6] build: move to Go 1.27.3 for govulncheck (GO-2026-6809) Ported from ako/mxcli#1527 so this PR's build-and-test can go green: govulncheck fails on crypto/x509@go1.27.2 (GO-2026-6809, fixed in 1.27.3), reached through internal/marketplace/client.go. main is red on the same check. Identical change; it no-ops once #1527 lands. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Hjeuk6NPBWq3QTxCKxsARF --- .github/workflows/nightly.yml | 8 ++++---- .github/workflows/push-test.yml | 8 ++++---- .github/workflows/release.yml | 2 +- go.mod | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5bdbdda112..da3b5719ea 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -40,7 +40,7 @@ jobs: - uses: actions/setup-go@v7 with: - go-version: '1.27.2' + go-version: '1.27.3' - name: Cache ANTLR4 JAR uses: actions/cache@v6 @@ -89,7 +89,7 @@ jobs: - uses: actions/setup-go@v7 with: - go-version: '1.27.2' + go-version: '1.27.3' # mxbuild 9 draws through System.Drawing; without libgdiplus every mx call # dies with "The type initializer for 'Gdip' threw an exception." @@ -144,7 +144,7 @@ jobs: - uses: actions/checkout@v7 - uses: actions/setup-go@v7 with: - go-version: '1.27.2' + go-version: '1.27.3' - name: Cache ANTLR4 JAR uses: actions/cache@v6 with: @@ -175,7 +175,7 @@ jobs: - uses: actions/setup-go@v7 with: - go-version: '1.27.2' + go-version: '1.27.3' - uses: oven-sh/setup-bun@v2 diff --git a/.github/workflows/push-test.yml b/.github/workflows/push-test.yml index 1b876f965d..f9feb55789 100644 --- a/.github/workflows/push-test.yml +++ b/.github/workflows/push-test.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v7 - uses: actions/setup-go@v7 with: - go-version: '1.27.2' + go-version: '1.27.3' - name: Test the tunnel seam shell: bash # Scoped with -run to the seam's own tests. The full test binaries are still @@ -78,7 +78,7 @@ jobs: - uses: actions/checkout@v7 - uses: actions/setup-go@v7 with: - go-version: '1.27.2' + go-version: '1.27.3' - name: Test the Windows process helpers shell: bash # Scoped with -run: the rest of the docker package's tests assert POSIX @@ -122,7 +122,7 @@ jobs: submodules: true - uses: actions/setup-go@v7 with: - go-version: '1.27.2' + go-version: '1.27.3' - name: Cache ANTLR4 JAR uses: actions/cache@v6 with: @@ -256,7 +256,7 @@ jobs: submodules: ${{ matrix.suite == 'roundtrip' || matrix.suite == 'parity' }} - uses: actions/setup-go@v7 with: - go-version: '1.27.2' + go-version: '1.27.3' - name: Cache ANTLR4 JAR uses: actions/cache@v6 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f0b8d4021..5bdcf8eb98 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-go@v7 with: - go-version: '1.27.2' + go-version: '1.27.3' - uses: oven-sh/setup-bun@v2 - name: Cache ANTLR4 JAR uses: actions/cache@v6 diff --git a/go.mod b/go.mod index 48d47e5da9..73f39f6398 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/mendixlabs/mxcli go 1.26.0 -toolchain go1.27.2 +toolchain go1.27.3 require ( github.com/alecthomas/chroma/v2 v2.26.1