Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <nil> 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)"}
Original file line number Diff line number Diff line change
@@ -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)"}
2 changes: 1 addition & 1 deletion .claude/skills/mendix/atlas-design/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<n>` |
| 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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)`.

---
2 changes: 1 addition & 1 deletion .claude/skills/mendix/create-page/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions .claude/skills/mendix/theme-styling/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`) |

<!-- /skill-index -->
# Theme & Styling — SCSS Workflow and Caveats
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <nil> 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} = <unbound>`, 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} = <unbound>` 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.
Expand Down
9 changes: 9 additions & 0 deletions mdl/ast/ast_page_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 78 additions & 0 deletions mdl/backend/modelsdk/listview_no_source_2183_test.go
Original file line number Diff line number Diff line change
@@ -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 <nil> 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)
}
}
16 changes: 15 additions & 1 deletion mdl/backend/modelsdk/widget_write.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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()
Expand Down
19 changes: 18 additions & 1 deletion mdl/executor/cmd_pages_builder_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading