diff --git a/.claude/skills/fix-issue/findings/mdl-backend/2026-11-12-listview-without-source-refused-not-written-unconfigured.json b/.claude/skills/fix-issue/findings/mdl-backend/2026-11-12-listview-without-source-refused-not-written-unconfigured.json new file mode 100644 index 000000000..79580dcba --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-backend/2026-11-12-listview-without-source-refused-not-written-unconfigured.json @@ -0,0 +1 @@ +{"area": "mdl/backend", "date": "2026-11-12", "symptom": "`use building block PlatformDesignSystem.Avatar_Group_SM` (and Avatar_Group_XS) failed with `CreatePage: encode: CreatePage: ListView source is not supported by either engine — please file an issue`; a hand-written `listview` with no `DataSource:` passed `mxcli check` and failed the same way at exec", "cause": "listViewSourceToGen had no case for a nil source, so an unconfigured list view fell to the unsupported-type refusal written for unknown $Types. Separately, Forms$ListViewXPathSource registered only SourceVariable as a null field, so an XPath source with no entity omitted its EntityRef key", "file": "mdl/backend/modelsdk/widget_write.go (listViewSourceToGen, Forms$ListViewXPathSource TypeDefaults); mdl/executor/cmd_pages_builder_v3_widgets.go (warnListViewWithoutSource)", "fix": "A nil source is written as Studio Pro's unconfigured source: Forms$ListViewXPathSource with EntityRef and SourceVariable null, XPathConstraint '', ForceFullObjects false, empty SortBar/Search (EntityRef added to NullFields). exec warns; mx check reports CE0488 'No entity configured for the data source of this list view' until the author binds it", "insight": "The reference shape was already on disk: every unconfigured List View in the Atlas page templates of testdata/testapp (13, identical; 13 likewise in PedApp) — scan the Studio Pro-authored templates before asking for a Studio Pro example. A patched copy of a building block (a Studio Pro list view grafted in, $IDs AND the pointers to them renumbered — renumbering $IDs alone made mxbuild throw KeyNotFoundException while loading) reproduces a Marketplace block that is not downloadable. A block's unconfigured list view is not an mx check error in the block itself; only once placed on a page", "refs": "ako/mxcli#2183, mendixlabs/mxcli#1836", "test": "mdl/backend/modelsdk/listview_no_source_2183_test.go; mdl/executor/use_building_block_unauthored_2183_test.go (PlacesSourcelessListView, WithSourceDoesNotWarn)"} diff --git a/.claude/skills/fix-issue/findings/mdl-executor/2026-11-12-building-block-dynamictext-style-refused-as-hand-written.json b/.claude/skills/fix-issue/findings/mdl-executor/2026-11-12-building-block-dynamictext-style-refused-as-hand-written.json new file mode 100644 index 000000000..cd98f2aa3 --- /dev/null +++ b/.claude/skills/fix-issue/findings/mdl-executor/2026-11-12-building-block-dynamictext-style-refused-as-hand-written.json @@ -0,0 +1 @@ +{"area": "mdl/executor", "date": "2026-11-12", "symptom": "`use building block PlatformDesignSystem.Icon_After_Text` (also Wizard_Horizontal_Responsive, Wizard_Mobile, Wizard_Vertical) failed with `failed to build widget: dynamictext \"b_text13\": an inline `style` crashes MxBuild — wrap it in a container and style the container instead` — the block's own text carries Style '--icon-size: auto;'", "cause": "The MDL-WIDGET03 guard in applyWidgetAppearance (#673) refuses any dynamictext with a style. A block is expanded by rendering its stored widgets to MDL and re-parsing them, so the block's Studio Pro-authored style reached the guard indistinguishable from hand-written MDL", "file": "mdl/executor/cmd_pages_builder_v3.go (applyWidgetAppearance, expandBuildingBlockRef/markFromBuildingBlock); mdl/ast/ast_page_v3.go (WidgetV3.FromBuildingBlock)", "fix": "The expander marks every widget it copies in (FromBuildingBlock, carried by cloneWidget's struct copy); the guard skips marked widgets, so the block's stored style is written verbatim. Hand-written MDL is still refused (check and exec)", "insight": "Measured on Mendix 11.12.1 with real mxbuild: a dynamic text with an inline style written by the current modelsdk engine — hand-written via --no-check with the guard stubbed, and via a patched Card block — passes mx check with 0 errors and `mxcli docker build` (BUILD SUCCEEDED); testdata/testapp also has a Studio Pro-authored dynamic text styled 'word-wrap: break-word;'. The #673 crash did not reproduce, so the hand-written refusal may be obsolete (it predates the modelsdk engine); not lifted here. Any guard written for authored MDL also fires on block/fragment expansion unless it knows the widget's origin", "refs": "ako/mxcli#2183, mendixlabs/mxcli#1836, #673", "test": "mdl/executor/use_building_block_unauthored_2183_test.go (CarriesDynamicTextStyle, HandWrittenDynamicTextStyleStillRefused)"} diff --git a/.claude/skills/mendix/atlas-design/SKILL.md b/.claude/skills/mendix/atlas-design/SKILL.md index 2ed7c6e2b..efa7b9a1b 100644 --- a/.claude/skills/mendix/atlas-design/SKILL.md +++ b/.claude/skills/mendix/atlas-design/SKILL.md @@ -422,7 +422,7 @@ row before opening files. | `sort by` not allowed on **association-sourced** listviews | sort the parent, or use a DB datasource | | Reserved widget identifiers exist (e.g. `v3`) | prefix names (`sv3`); avoid bare `v` | | Pluggable widgets impose their own DOM (charts / timeline / treenode) | for pixel-fidelity use a native `listview` / `gallery` you fully style | -| Inline `style:` on a `dynamictext` crashes MxBuild (NullReferenceException) | use `class:`, or wrap the text in a styled `container` | +| Inline `style:` on a `dynamictext` crashes MxBuild (NullReferenceException) | use `class:`, or wrap the text in a styled `container` (a block's own stored style is placed as is by `use building block`) | | `alter styling` can't find widgets in MDL-builder-created pages | apply classes via `Class:` / `DynamicClasses:` in `create page` / `alter page` | | Full-screen page wanted (no Atlas sidebar) but no blank layout resolves | keep a normal Atlas layout; hide the shell per-page with `.mx-page:has(.my-app) .region-sidebar { display:none }` | | "Colour by state" (status pills / cards) | one `dynamicclasses` enum→class expression + one `--st` CSS var cascaded into pill/number/dot/border | diff --git a/.claude/skills/mendix/atlas-design/reference/building-blocks.md b/.claude/skills/mendix/atlas-design/reference/building-blocks.md index 6b89e5dba..048b8a14c 100644 --- a/.claude/skills/mendix/atlas-design/reference/building-blocks.md +++ b/.claude/skills/mendix/atlas-design/reference/building-blocks.md @@ -263,6 +263,10 @@ more idiomatic form to mirror from a `describe building block`. Notes: - Compound properties (Spacing, Border) take a **nested list**: `['Spacing': ['margin-top': 'Large', 'margin-bottom': 'None']]`. - **Never** put inline `style:` on a `dynamictext` — it crashes MxBuild. Use `class:` - or wrap in a styled `container`. (`theme-styling`.) + or wrap in a styled `container`. (`theme-styling`.) A block's *own* stored style + is the exception: `use building block` places it as stored. +- A block's list view whose data source is not configured is placed unconfigured + (`exec` warns); `mx check` then reports CE0488 until you set one — bind it with + `use building block … (datasource: database from Module.Entity)`. --- diff --git a/.claude/skills/mendix/create-page/SKILL.md b/.claude/skills/mendix/create-page/SKILL.md index ed7fc7798..7273c39da 100644 --- a/.claude/skills/mendix/create-page/SKILL.md +++ b/.claude/skills/mendix/create-page/SKILL.md @@ -158,7 +158,7 @@ actionbutton btn (caption: 'Save', class: 'btn-lg') container c (style: 'background-color: #f8f9fa; padding: 16px;') { ... } ``` -> **Warning:** Do NOT use `style` directly on DYNAMICTEXT widgets — it crashes MxBuild with a NullReferenceException. Wrap the DYNAMICTEXT in a styled CONTAINER instead. +> **Warning:** Do NOT use `style` directly on DYNAMICTEXT widgets — it crashes MxBuild with a NullReferenceException. Wrap the DYNAMICTEXT in a styled CONTAINER instead. (A building block's own styled dynamic text is placed as stored by `use building block`; the refusal is for hand-written MDL.) **Design Properties** — Atlas UI structured properties (spacing, colors, toggles): ```sql diff --git a/.claude/skills/mendix/theme-styling/SKILL.md b/.claude/skills/mendix/theme-styling/SKILL.md index 8b08c9d28..7060b2700 100644 --- a/.claude/skills/mendix/theme-styling/SKILL.md +++ b/.claude/skills/mendix/theme-styling/SKILL.md @@ -13,10 +13,10 @@ description: "The SCSS workflow and its traps — where styling actually compile | 24-43 | When to Use This Skill | | 44-297 | SCSS Compilation Chain | | 298-316 | CSS Hot-Reload Workflow | -| 317-416 | Caveats | -| 417-431 | Validation with `mxcli check -p` | -| 432-440 | Checklist | -| 441-443 | Default styling + runtime theme switching (`mxcli theme list/show/create/apply/remove/switcher`, `mxcli new --theme`) | +| 317-420 | Caveats | +| 421-435 | Validation with `mxcli check -p` | +| 436-444 | Checklist | +| 445-447 | Default styling + runtime theme switching (`mxcli theme list/show/create/apply/remove/switcher`, `mxcli new --theme`) | # Theme & Styling — SCSS Workflow and Caveats @@ -330,6 +330,10 @@ container ctn (style: 'color: red;') { } ``` +The rule is for hand-written MDL (`mxcli check` refuses it as MDL-WIDGET03). A +dynamic text that `use building block` copies in keeps the inline style the block +stores — Studio Pro wrote it, and it is placed as stored (ako/mxcli#2183). + This also applies to `alter styling` and `alter page set style` — never target a DYNAMICTEXT widget with Style. ### Clipped navigation labels are the CLOSED sidebar, not the theme diff --git a/CHANGELOG.md b/CHANGELOG.md index 49cd2d5d6..ff48e3ab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- **`use building block` places a block's styled dynamic text and its unconfigured list view** (ako/mxcli#2183, mendixlabs/mxcli#1836) — two refusals meant for hand-written MDL also fired on widgets a building block copies in, so one widget the author never wrote blocked the whole block. PlatformDesignSystem's Icon_After_Text and Wizard blocks failed with "an inline `style` crashes MxBuild", because their dynamic text is styled `--icon-size: auto;`; Avatar_Group_SM and Avatar_Group_XS failed with "ListView source is not supported by either engine", because their list view has no data source configured yet. A block's own style is now written as the block stores it (a hand-written `style:` on a `dynamictext` is still refused, MDL-WIDGET03), and a list view with no data source, from a block or hand-written, is written the way Studio Pro stores one: an XPath source naming no entity. `exec` warns, and `mx check` reports CE0488 "No entity configured for the data source of this list view" until you bind it, e.g. with `use building block … (datasource: database from Module.Entity)`. Measured on Mendix 11.12.1 with a building block carrying both widgets: before, `exec` refused the page; after, `mx check` reports only CE0488, and with the data source bound it passes and `mxcli docker build` succeeds. - **`use building block` places a block whose text-template parameters are unbound** (ako/mxcli#2181, mendixlabs/mxcli#1834) — the block's widget tree is expanded by describing it as MDL and parsing that back, and `describe` prints a parameter nobody has bound yet as `{1} = `, which did not parse. Building blocks are full of them (PlatformDesignSystem's Gallery_List_Divider, Gallery_Card_Grid_Actions and Gallery_List_Panels; Atlas's ListItem_WithImage), so the whole block was refused with "could not expand widget tree: extraneous input '<'". The same line made `describe page` output of any page with an unbound parameter fail to re-execute. `{1} = ` now parses and writes the parameter back unbound, in its position, as Studio Pro stores it: the block is placed, mxbuild reports CE0402 "No value specified." for each parameter until it is bound, and describe -> exec of such a page is unchanged. - **`check` resolves an `alter page` target against the building blocks the page holds** (ako/mxcli#2182, mendixlabs/mxcli#1835) — an `alter page` / `alter snippet` on a document the same script creates was never resolved, so `set … on a_container99` after `use building block PlatformDesignSystem.Alert_Info as a_` passed `check -p` and exec then stopped on `widget "a_container99" not found`, with the page already created. A plain misspelt widget passed the same way. Check now replays the documents the script creates, expanding each `use building block` and `use fragment` as exec does (prefix included), and refuses a target they do not hold at that point as MDL-ALTER02, suggesting the nearest name (`did you mean a_container1`). On a stored page, the names a block inserted by `alter page … insert` are now its expanded names: `insert after s_container2` after inserting a block `as s_` is no longer refused, and `set … on s_text99` is no longer waived. Without a project a block cannot be expanded, so every name under its prefix is accepted (every name, for an unprefixed block), and a page holding DataGrid 2 columns or object-list items, whose addresses are derived, is not judged. The duplicate-name side (ako/mxcli#559) was already fixed. - **A pluggable widget's template is prepared once per widget type** (ako/mxcli#2160) — every data grid, filter and other pluggable widget a script builds loaded its template by deep-cloning the built-in one and merging the project's widget package into the clone, the same work each time. The merged template is now made once for each widget type and package and reused; each widget still gets IDs of its own. Creating 600 pages: 29.5 s before, 26.5 s after; re-running them settled: 83.1 s before, 73.9 s after. The pages written are the same: all 1,719 units of the two projects compare equal once IDs are set aside. diff --git a/mdl/ast/ast_page_v3.go b/mdl/ast/ast_page_v3.go index 3ad944687..d0389fbfb 100644 --- a/mdl/ast/ast_page_v3.go +++ b/mdl/ast/ast_page_v3.go @@ -193,6 +193,15 @@ type WidgetV3 struct { // diagnostic. Keyed by the property name as written; read it through // ExpressionSource, which matches case-insensitively. ValueSource map[string]string + + // FromBuildingBlock records that the widget was copied in by `use building + // block` rather than written by the author: Studio Pro authored it, and its + // values are the block's stored ones. A refusal meant to stop hand-written + // MDL from writing something Studio Pro would not (an inline style on a + // dynamictext, MDL-WIDGET03) does not apply to a value Studio Pro wrote + // itself — refusing it blocked the whole block (ako/mxcli#2183). Set by the + // building-block expander only. + FromBuildingBlock bool } // ExpressionSource returns the source text the visitor kept for key (see diff --git a/mdl/backend/modelsdk/listview_no_source_2183_test.go b/mdl/backend/modelsdk/listview_no_source_2183_test.go new file mode 100644 index 000000000..d042fe180 --- /dev/null +++ b/mdl/backend/modelsdk/listview_no_source_2183_test.go @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 + +package modelsdkbackend + +import ( + "reflect" + "sort" + "strings" + "testing" + + "go.mongodb.org/mongo-driver/v2/bson" + + "github.com/mendixlabs/mxcli/sdk/pages" +) + +// studioProUnsetListViewSourceKeys is the key set of the DataSource of every +// Studio Pro-authored List View whose source is not yet configured: 13 of them +// in the page templates of testdata/testapp, 13 more in testdata/pedapp's (e.g. +// testapp mprcontents/30/12/30127bfe-68f5-40ca-ba13-a73943dd6285.mxunit, +// listView1), all identical — +// +// {$ID, $Type: Forms$ListViewXPathSource, EntityRef: null, +// ForceFullObjects: false, Search: {SearchRefs: [2]}, +// SortBar: {SortItems: [2]}, SourceVariable: null, XPathConstraint: ""} +var studioProUnsetListViewSourceKeys = []string{ + "$ID", "$Type", "EntityRef", "ForceFullObjects", "Search", "SortBar", "SourceVariable", "XPathConstraint", +} + +// TestListViewWithoutSourceWritesStudioProShape is ako/mxcli#2183 +// (mendixlabs/mxcli#1836): `use building block PlatformDesignSystem.Avatar_Group_SM` +// failed with "ListView source is not supported by either engine" — the +// block's list view has no data source configured, which Studio Pro stores as +// an XPath source naming no entity. Written that way, `mx check` asks the author +// to pick an entity, instead of mxcli refusing the whole block. +func TestListViewWithoutSourceWritesStudioProShape(t *testing.T) { + lv, err := widgetToGen(&pages.ListView{PageSize: 10}) + if err != nil { + t.Fatalf("list view without a data source: %v", err) + } + ds, ok := lookup(encodeD(t, lv), "DataSource").(bson.D) + if !ok { + t.Fatalf("DataSource missing or not a document") + } + if got := lookup(ds, "$Type"); got != "Forms$ListViewXPathSource" { + t.Errorf("DataSource $Type = %v, want Forms$ListViewXPathSource", got) + } + keys := dKeys(ds) + sort.Strings(keys) + if !reflect.DeepEqual(keys, studioProUnsetListViewSourceKeys) { + t.Errorf("DataSource keys = %v, want Studio Pro's %v", keys, studioProUnsetListViewSourceKeys) + } + for _, k := range []string{"EntityRef", "SourceVariable"} { + if v := lookup(ds, k); v != nil { + t.Errorf("%s = %v, want null (no entity configured)", k, v) + } + } + if v := lookup(ds, "XPathConstraint"); v != "" { + t.Errorf("XPathConstraint = %v, want empty", v) + } + if v := lookup(ds, "ForceFullObjects"); v != false { + t.Errorf("ForceFullObjects = %v, want false", v) + } + if m := lvListMarker(t, lookup(ds, "Search").(bson.D), "SearchRefs"); m != int32(2) { + t.Errorf("Search.SearchRefs marker = %v, want 2", m) + } + if m := lvListMarker(t, lookup(ds, "SortBar").(bson.D), "SortItems"); m != int32(2) { + t.Errorf("SortBar.SortItems marker = %v, want 2", m) + } +} + +// Control: a source type the writer does not know is still refused loudly — +// only the absent source changed meaning. +func TestListViewUnknownSourceStillRefused(t *testing.T) { + _, err := listViewSourceToGen(&pages.ListenToWidgetSource{}) + if err == nil || !strings.Contains(err.Error(), "is not supported by either engine") { + t.Errorf("want the unsupported-source refusal, got %v", err) + } +} diff --git a/mdl/backend/modelsdk/widget_write.go b/mdl/backend/modelsdk/widget_write.go index 49885aeab..ae1387087 100644 --- a/mdl/backend/modelsdk/widget_write.go +++ b/mdl/backend/modelsdk/widget_write.go @@ -119,8 +119,11 @@ func init() { // A List View's XPath source always carries its SourceVariable key, null // when the source is not reached from a parameter: 44 of 44 in the // fixtures, as on the pluggable XPath source above (ako/mxcli#721 L5). + // EntityRef likewise: a List View with no entity configured yet stores the + // key null — every such List View in the testdata/testapp page templates — + // and mxcli omitted it (ako/mxcli#2183). codec.RegisterTypeDefaults("Forms$ListViewXPathSource", codec.TypeDefaults{ - NullFields: []string{"SourceVariable"}, + NullFields: []string{"EntityRef", "SourceVariable"}, }) // An association source likewise: 40 of 40 in FactoryManagement carry the // key, null (30) when the source starts from $currentObject (ako/mxcli#2106). @@ -1839,7 +1842,18 @@ func associationContextVariableToGen(d *pages.AssociationSource) element.Element // Forms$ListViewXPathSource (EntityRef + XPathConstraint + a GridSortBar and a // ListViewSearch sub-element, matching what Studio Pro/the legacy writer emit); // a microflow source uses Forms$MicroflowSource (as DataView does). +// +// No source at all is a List View whose data source is not configured yet — a +// building block's (PlatformDesignSystem's Avatar_Group_SM) or a hand-written +// `listview` without `DataSource:`. Studio Pro stores that as an XPath source +// naming no entity: the same element with EntityRef and SourceVariable null and +// an empty sort bar and search (every such List View in the page templates of +// testdata/testapp). `mx check` then asks for the entity, which is the author's +// to choose. Refusing it instead blocked the whole block (ako/mxcli#2183). func listViewSourceToGen(ds pages.DataSource) (element.Element, error) { + if ds == nil { + return listViewSourceToGen(&pages.DatabaseSource{}) + } switch d := ds.(type) { case *pages.DatabaseSource: src := genPg.NewListViewXPathSource() diff --git a/mdl/executor/cmd_pages_builder_v3.go b/mdl/executor/cmd_pages_builder_v3.go index 2baef6e22..eb0929a1b 100644 --- a/mdl/executor/cmd_pages_builder_v3.go +++ b/mdl/executor/cmd_pages_builder_v3.go @@ -696,7 +696,14 @@ func applyWidgetAppearance(widget pages.Widget, w *ast.WidgetV3, theme *ThemeReg // A DynamicText with an inline Style crashes MxBuild with a // NullReferenceException. Reject it here (the workaround is to wrap the // widget in a container and style the container instead). - if style != "" && strings.EqualFold(w.Type, "dynamictext") { + // + // Not for a widget a building block copied in: that style is the block's + // own, written by Studio Pro and carried through verbatim, and mxbuild + // builds it — PlatformDesignSystem's Icon_After_Text styles its text + // `--icon-size: auto;`, and refusing it refused the whole block + // (ako/mxcli#2183). Measured on 11.12.1: a dynamic text with that style, + // written by this engine, passes `mx check` and `mxcli docker build`. + if style != "" && strings.EqualFold(w.Type, "dynamictext") && !w.FromBuildingBlock { return mdlerrors.NewValidationf( "dynamictext %q: an inline `style` crashes MxBuild — wrap it in a container and style the container instead", w.Name, @@ -3351,6 +3358,7 @@ func (pb *pageBuilder) expandBuildingBlockRef(w *ast.WidgetV3) ([]*ast.WidgetV3, return nil, fmt.Errorf("use building block %s: could not expand widget tree", w.Name) } widgets := def.Widgets + markFromBuildingBlock(widgets) // Apply the optional prefix rename. if prefix, ok := w.Properties["Prefix"].(string); ok && prefix != "" { @@ -3390,6 +3398,15 @@ func (pb *pageBuilder) expandBuildingBlockRef(w *ast.WidgetV3) ([]*ast.WidgetV3, return widgets, nil } +// markFromBuildingBlock flags every widget of an expanded block, children +// included, as one the author did not write (see ast.WidgetV3.FromBuildingBlock). +func markFromBuildingBlock(widgets []*ast.WidgetV3) { + for _, w := range widgets { + w.FromBuildingBlock = true + markFromBuildingBlock(w.Children) + } +} + // isDataSourceWidget reports whether a widget is one that takes a datasource, // whether or not it currently carries one. The list is the data containers a // building block can be built around; anything else in a block is layout or a diff --git a/mdl/executor/cmd_pages_builder_v3_widgets.go b/mdl/executor/cmd_pages_builder_v3_widgets.go index 1f2d0f5f5..be3a94564 100644 --- a/mdl/executor/cmd_pages_builder_v3_widgets.go +++ b/mdl/executor/cmd_pages_builder_v3_widgets.go @@ -316,6 +316,26 @@ func (pb *pageBuilder) buildDataGridColumnV3(w *ast.WidgetV3) (*pages.DataGridCo return col, nil } +// warnListViewWithoutSource reports a list view written with no data source. +// It is placed — the writer stores Studio Pro's unconfigured source, an XPath +// source naming no entity — and `mx check` will ask for the entity until the +// author sets one. A building block ships list views like that +// (PlatformDesignSystem's Avatar_Group_SM); refusing them refused the whole +// block (ako/mxcli#2183). +func (pb *pageBuilder) warnListViewWithoutSource(w *ast.WidgetV3) { + if pb.ctx == nil { + return + } + origin := "" + if w.FromBuildingBlock { + origin = " (from the building block)" + } + fmt.Fprintf(pb.ctx.progress(), + "Warning: listview %q%s has no data source; it is placed unconfigured, as Studio Pro stores it, "+ + "and `mx check` reports it until one is set (e.g. `DataSource: database from Module.Entity`).\n", + w.Name, origin) +} + func (pb *pageBuilder) buildListViewV3(w *ast.WidgetV3) (*pages.ListView, error) { lv := &pages.ListView{ BaseWidget: pages.BaseWidget{ @@ -386,6 +406,8 @@ func (pb *pageBuilder) buildListViewV3(w *ast.WidgetV3) (*pages.ListView, error) if w.Name != "" && entityName != "" { pb.paramEntityNames[w.Name] = entityName } + } else { + pb.warnListViewWithoutSource(w) } // Register widget scope for SELECTION references diff --git a/mdl/executor/use_building_block_unauthored_2183_test.go b/mdl/executor/use_building_block_unauthored_2183_test.go new file mode 100644 index 000000000..140d5416f --- /dev/null +++ b/mdl/executor/use_building_block_unauthored_2183_test.go @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: Apache-2.0 + +// ako/mxcli#2183 (mendixlabs/mxcli#1836): two refusals meant for hand-written +// MDL also fired on widgets `use building block` copies in, so one widget the +// author never wrote blocked the whole block. +// +// - PlatformDesignSystem.Icon_After_Text carries a dynamic text styled +// `--icon-size: auto;`, and the inline-style refusal on a dynamictext +// (MDL-WIDGET03, #673) stopped the block. +// - PlatformDesignSystem.Avatar_Group_SM carries a list view with no data +// source configured, which the writer refused ("ListView source "). +package executor + +import ( + "bytes" + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/domainmodel" + "github.com/mendixlabs/mxcli/sdk/pages" +) + +// rawStyledDynamicText is the Icon_After_Text block's text as Studio Pro +// stores it: an inline Style on the dynamic text's own Appearance. +func rawStyledDynamicText(name, style string) map[string]any { + return map[string]any{ + "$Type": "Forms$DynamicText", + "Name": name, + "Appearance": map[string]any{"Class": "", "Style": style}, + "Content": map[string]any{ + "Template": map[string]any{ + "Items": []any{map[string]any{"Text": "Label", "LanguageCode": "en_US"}}, + }, + }, + } +} + +// rawSourcelessListView is the Avatar_Group_SM block's list view: Studio Pro's +// unconfigured source, an XPath source with no entity. +func rawSourcelessListView(name string) map[string]any { + return map[string]any{ + "$Type": "Forms$ListView", + "Name": name, + "PageSize": int32(10), + "Appearance": map[string]any{"Class": "pds-avatar-group__listview"}, + "DataSource": map[string]any{ + "$Type": "Forms$ListViewXPathSource", + "EntityRef": nil, + "SourceVariable": nil, + "XPathConstraint": "", + }, + "Widgets": []any{rawStyledDynamicText("inner", "")}, + } +} + +// blockCtx serves one PlatformDesignSystem block whose widgets are raw, and +// returns the builder and the output the builder warns on. +func blockCtx(t *testing.T, blockName string, widgets ...any) (*pageBuilder, *bytes.Buffer) { + t.Helper() + mod := mkModule("PlatformDesignSystem") + bb := mkBuildingBlock(mod.ID, blockName) + h := mkHierarchy(mod) + withContainer(h, bb.ContainerID, mod.ID) + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListBuildingBlocksFunc: func() ([]*pages.BuildingBlock, error) { + return []*pages.BuildingBlock{bb}, nil + }, + GetRawUnitFunc: func(id model.ID) (map[string]any, error) { + return map[string]any{"Widgets": widgets}, nil + }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + ListDomainModelsFunc: func() ([]*domainmodel.DomainModel, error) { + return []*domainmodel.DomainModel{mkDomainModel(mod.ID, mkEntity(mod.ID, "E"))}, nil + }, + } + ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) + return &pageBuilder{ + ctx: ctx, + backend: mb, + widgetScope: map[string]model.ID{}, + paramEntityNames: map[string]string{}, + }, buf +} + +func useBlock(name string) *ast.WidgetV3 { + return &ast.WidgetV3{ + Type: "USE_BUILDING_BLOCK", + Name: "PlatformDesignSystem." + name, + Properties: map[string]any{"Prefix": "b_"}, + } +} + +// expandAndBuild expands the block and builds each top-level widget, as the +// page builder does. +func expandAndBuild(t *testing.T, pb *pageBuilder, block string) ([]*ast.WidgetV3, []pages.Widget, error) { + t.Helper() + ws, err := pb.expandFragments([]*ast.WidgetV3{useBlock(block)}) + if err != nil { + t.Fatalf("expand %s: %v", block, err) + } + var built []pages.Widget + for _, w := range ws { + b, err := pb.buildWidgetV3(w) + if err != nil { + return ws, built, err + } + built = append(built, b) + } + return ws, built, nil +} + +// TestBuildingBlockCarriesDynamicTextStyle: the block's own style is placed as +// Studio Pro stores it, not refused. +func TestBuildingBlockCarriesDynamicTextStyle(t *testing.T) { + pb, _ := blockCtx(t, "Icon_After_Text", rawStyledDynamicText("text13", "--icon-size: auto;")) + _, built, err := expandAndBuild(t, pb, "Icon_After_Text") + if err != nil { + t.Fatalf("use building block Icon_After_Text: %v", err) + } + dt, ok := built[0].(*pages.DynamicText) + if !ok { + t.Fatalf("built %T, want *pages.DynamicText", built[0]) + } + if dt.Name != "b_text13" { + t.Errorf("name = %q, want b_text13", dt.Name) + } + if dt.Style != "--icon-size: auto;" { + t.Errorf("Style = %q, want the block's stored --icon-size: auto;", dt.Style) + } +} + +// Control: a hand-written dynamictext with an inline style is still refused +// (MDL-WIDGET03); only the block's stored value is carried. +func TestHandWrittenDynamicTextStyleStillRefused(t *testing.T) { + pb, _ := blockCtx(t, "Unused") + w := &ast.WidgetV3{ + Type: "DYNAMICTEXT", + Name: "dt", + Properties: map[string]any{"Content": "x", "Style": "color: red;"}, + } + _, err := pb.buildWidgetV3(w) + if err == nil || !strings.Contains(err.Error(), "an inline `style` crashes MxBuild") { + t.Errorf("want the inline-style refusal, got %v", err) + } +} + +// TestBuildingBlockPlacesSourcelessListView: the list view is placed with no +// source and the author is told it needs one. +func TestBuildingBlockPlacesSourcelessListView(t *testing.T) { + pb, out := blockCtx(t, "Avatar_Group_SM", rawSourcelessListView("listView2")) + _, built, err := expandAndBuild(t, pb, "Avatar_Group_SM") + if err != nil { + t.Fatalf("use building block Avatar_Group_SM: %v", err) + } + lv, ok := built[0].(*pages.ListView) + if !ok { + t.Fatalf("built %T, want *pages.ListView", built[0]) + } + if lv.DataSource != nil { + t.Errorf("DataSource = %T, want none (the writer stores Studio Pro's unconfigured source)", lv.DataSource) + } + if lv.PageSize != 10 { + t.Errorf("PageSize = %d, want the block's 10", lv.PageSize) + } + if !strings.Contains(out.String(), "b_listView2") || !strings.Contains(out.String(), "no data source") { + t.Errorf("want a warning naming b_listView2 and its missing data source, got %q", out.String()) + } +} + +// Control: a list view with a data source warns about nothing. +func TestListViewWithSourceDoesNotWarn(t *testing.T) { + pb, out := blockCtx(t, "Unused") + w := &ast.WidgetV3{ + Type: "LISTVIEW", + Name: "lv", + Properties: map[string]any{ + "DataSource": &ast.DataSourceV3{Type: "database", Reference: "PlatformDesignSystem.E"}, + }, + } + if _, err := pb.buildWidgetV3(w); err != nil { + t.Fatalf("build: %v", err) + } + if strings.Contains(out.String(), "no data source") { + t.Errorf("unexpected warning: %q", out.String()) + } +}