From 4f97210129878bbb95c07458e5bcb2bbcc353c38 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 22:35:55 +0000 Subject: [PATCH 1/2] Carry a building block's own dynamic-text style instead of refusing it `use building block PlatformDesignSystem.Icon_After_Text as b_` (and the three Wizard blocks) failed with `dynamictext "b_text13": an inline `style` crashes MxBuild`. The block's text is styled `--icon-size: auto;`. Cause: MDL-WIDGET03 (#673) refuses any dynamictext with a style in applyWidgetAppearance. A block is expanded by rendering its stored widgets to MDL and re-parsing them, so a style Studio Pro wrote reached the guard looking exactly like hand-written MDL. Fix: the expander marks every widget it copies in (ast.WidgetV3.FromBuildingBlock, kept by cloneWidget's struct copy), and the guard skips marked widgets. The block's style is written as stored. Hand-written MDL is still refused by both check and exec. Measured with Mendix 11.12.1 mxbuild: - testdata/testapp has a Studio Pro-authored dynamic text styled 'word-wrap: break-word;'. - A dynamic text with an inline style written by this engine (hand-written, guard stubbed, --no-check) passes mx check and `mxcli docker build`. The #673 NullReferenceException did not reproduce. The refusal for hand-written MDL may be obsolete, but it is not lifted here. - A copy of EvSpike whose Card block was patched to carry the styled text: main refuses the page; this branch places it, and the page builds. Tests: use_building_block_unauthored_2183_test.go. The block test fails with the reported error when markFromBuildingBlock is stubbed. A control checks that the hand-written refusal still applies. Refs ako/mxcli#2183, mendixlabs/mxcli#1836 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QiMsnXa9crge2qnSk9S7Zn --- ...mictext-style-refused-as-hand-written.json | 1 + mdl/ast/ast_page_v3.go | 9 ++ mdl/executor/cmd_pages_builder_v3.go | 19 ++- ...use_building_block_unauthored_2183_test.go | 131 ++++++++++++++++++ 4 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/fix-issue/findings/mdl-executor/2026-11-12-building-block-dynamictext-style-refused-as-hand-written.json create mode 100644 mdl/executor/use_building_block_unauthored_2183_test.go 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/mdl/ast/ast_page_v3.go b/mdl/ast/ast_page_v3.go index 644710236..882b77242 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/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/use_building_block_unauthored_2183_test.go b/mdl/executor/use_building_block_unauthored_2183_test.go new file mode 100644 index 000000000..b9797c362 --- /dev/null +++ b/mdl/executor/use_building_block_unauthored_2183_test.go @@ -0,0 +1,131 @@ +// 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"}}, + }, + }, + } +} + +// 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) + } +} From b31e469a0c8d3ef9cbad8053379084d64c0f0085 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 22:36:11 +0000 Subject: [PATCH 2/2] Write a list view with no data source the way Studio Pro stores it `use building block PlatformDesignSystem.Avatar_Group_SM as b_` (and Avatar_Group_XS) failed with `CreatePage: encode: ListView source is not supported by either engine`. The block's list view has no data source configured. A hand-written `listview` with no `DataSource:` passed `mxcli check` and then failed the same way at exec. Cause: listViewSourceToGen had no case for a nil source, so it hit the refusal meant for unknown source types (in place since the ListView writer was added). Forms$ListViewXPathSource also registered only SourceVariable as a null field, so an XPath source with no entity left out the EntityRef key. Fix: a nil source is written as Studio Pro's unconfigured source, Forms$ListViewXPathSource with: - EntityRef and SourceVariable null - XPathConstraint "" - ForceFullObjects false - an empty SortBar and Search (marker 2) This applies to block and hand-written list views alike. exec warns, and mx check reports CE0488 until the author binds a source. Reference: all 13 unconfigured List Views in the Atlas page templates of testdata/testapp (and 13 in PedApp's) have this exact key set. The new test pins it. Measured with Mendix 11.12.1 mxbuild, on a copy of EvSpike whose Card block carries a Studio Pro list view with no source (grafted in with $IDs and pointers renumbered): - main refuses the page. - This branch places it with a warning. mx check reports only CE0488 "No entity configured for the data source of this list view". - With `(datasource: database from Administration.Account)` mx check reports 0 errors and `mxcli docker build` succeeds. Also updates the skills that stated the refusals without the building-block exception, and adds the CHANGELOG entry covering both fixes. Tests: listview_no_source_2183_test.go (writer, plus a control that unknown source types are still refused) and use_building_block_unauthored_2183_test.go (the block is placed with a warning; a list view with a source does not warn). Before the fix both fail with the reported error or with no warning. Refs ako/mxcli#2183, mendixlabs/mxcli#1836 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QiMsnXa9crge2qnSk9S7Zn --- ...urce-refused-not-written-unconfigured.json | 1 + .claude/skills/mendix/atlas-design/SKILL.md | 2 +- .../atlas-design/reference/building-blocks.md | 6 +- .claude/skills/mendix/create-page/SKILL.md | 2 +- .claude/skills/mendix/theme-styling/SKILL.md | 12 ++- CHANGELOG.md | 1 + .../modelsdk/listview_no_source_2183_test.go | 78 +++++++++++++++++++ mdl/backend/modelsdk/widget_write.go | 16 +++- mdl/executor/cmd_pages_builder_v3_widgets.go | 22 ++++++ ...use_building_block_unauthored_2183_test.go | 59 ++++++++++++++ 10 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 .claude/skills/fix-issue/findings/mdl-backend/2026-11-12-listview-without-source-refused-not-written-unconfigured.json create mode 100644 mdl/backend/modelsdk/listview_no_source_2183_test.go 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/mendix/atlas-design/SKILL.md b/.claude/skills/mendix/atlas-design/SKILL.md index 81210ac8c..7bce40f63 100644 --- a/.claude/skills/mendix/atlas-design/SKILL.md +++ b/.claude/skills/mendix/atlas-design/SKILL.md @@ -417,7 +417,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 8ca0c120e..085d23cbf 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. - **`describe page` loads the page it describes** (ako/mxcli#2160) — it loaded every page with the same local name, in whatever module, before picking the one asked for, and read the page's document twice. `Overview`, `NewEdit` and `Home` repeat across the modules of most projects, and rewriting a page describes the stored one up to three times, so a script that re-runs its pages paid for it per statement. Output is unchanged. Re-running a settled script of 600 `create or modify page` statements: 87.8 s before, 77.2 s after. - **The decode cache compares a unit's bytes once per run** (ako/mxcli#2160) — the cache that spares a script decoding the same unit again recognises the unit by its bytes. It kept the buffer of an earlier pass, so in the next pass every listing compared every unit of the type byte for byte: all of a project's pages, twice per page statement. It now keeps the buffer it matched. Re-running a settled script of 600 `create or modify page` statements: 103.7 s before, 86.8 s after. - **Parsing MDL is several times faster** (ako/mxcli#2160) — the parser decided nearly every statement twice, the second time with ANTLR's full-context prediction, which is slow and not cached: four fifths of the time of a parse. It now parses in the fast SLL mode first and falls back to the full one only for input that mode reports a syntax error in, so error messages are what they were. Compared on every script under `mdl-examples/`, whole and truncated: identical programs and messages. `check` of a 600-page script without a project: 4.5 s before, 0.4 s after. Re-running a settled script of 600 `create or modify microflow` statements, which parses a description of each stored microflow to compare it: 23.3 s before, 7.5 s after. 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_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 index b9797c362..140d5416f 100644 --- a/mdl/executor/use_building_block_unauthored_2183_test.go +++ b/mdl/executor/use_building_block_unauthored_2183_test.go @@ -38,6 +38,24 @@ func rawStyledDynamicText(name, style string) map[string]any { } } +// 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) { @@ -129,3 +147,44 @@ func TestHandWrittenDynamicTextStyleStillRefused(t *testing.T) { 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()) + } +}