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
1 change: 1 addition & 0 deletions .claude/skills/fix-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ to the symptom table below, so the next similar issue costs fewer reads.
| Studio Pro shows a dropdown / property as its default value even though MDL set it explicitly (e.g. CREATE ODATA CLIENT with `ConfigurationMicroflow:` set, but the "Configuration source" dropdown reads "Constants only") | The BSON key mxcli writes isn't what Studio Pro reads — either the key name is wrong, or multiple dropdown options actually share a single field discriminated by something other than the key name (return type of a referenced microflow, sibling property, etc.) | The `serializeXxx` function in `sdk/mpr/writer_*.go` for the affected document type | (1) Ask the user to duplicate the offending object in Studio Pro and **explicitly pick each dropdown option** on the duplicate(s). An unconfigured duplicate just looks like "Constants only" and tells you nothing about which field the option uses. (2) Re-dump the duplicates from `mprcontents/**/*.mxunit` **after every Studio Pro change** — cached `/tmp/svc-*.json` files go stale the instant the user edits the project (see [[feedback-refresh-bson-dumps]]). (3) Diff against mxcli's output to find the renamed key. (4) Don't assume one-state-per-key. The OData "Configuration microflow" / "Headers microflow" case stores BOTH options in the single `ConfigurationMicroflow` BSON field — Studio Pro picks the dropdown label from the referenced microflow's return type. When a discriminator like that exists, have both MDL keywords write to the same model field. Issues #573, #587 and 2026-05-27 unify-config-microflow fix |
| `DESCRIBE` of a document type omits a property the user set in the CREATE (e.g. enum-level `/** doc */` vanishes after a roundtrip; `CREATE OR REPLACE` silently runs as plain CREATE for some types) — even though the AST struct has the field, the model has the field, the writer serialises it, and DESCRIBE prints it | Visitor never copied the parsed value into the AST struct — every layer below the visitor is wired but the visitor's `ExitCreateXxxStatement` is missing the assignment | `mdl/visitor/visitor_<type>.go` → `ExitCreateXxxStatement` | Diff the visitor against a known-good sibling (e.g. enumeration vs constant). Standard wiring: `stmt.Documentation = findDocCommentText(ctx)` for doc-comments; `if createStmt.OR() != nil && (createStmt.MODIFY() != nil \|\| createStmt.REPLACE() != nil) { stmt.CreateOrModify = true }` for OR MODIFY/REPLACE. When adding a new CREATE statement, grep `mdl/visitor/visitor_constant.go` and copy these two blocks verbatim. Issue #393 |
| `create [or modify] association ... to System.X` passes `mxcli check --references` and `mxcli diff` but fails at `mxcli exec` with `child entity not found: System.X` | Two divergent entity resolvers: the write path's `findEntity` resolved the owning module via `h.FindModuleID(dm.ID)`, but the virtual System domain model is not a real unit, so the hierarchy walk yielded an empty module name and System entities never matched. The validation path (`buildEntityQualifiedNames`) keyed on `dm.ContainerID` and worked, hence the check-passes/exec-fails split | `mdl/executor/oql_type_inference.go` → `findEntity` | Resolve the module from `dm.ContainerID` (the module ID `BuildSystemDomainModel` sets), not by walking up from the DM's own unit ID. When a symptom is "passes check/diff but fails exec," suspect two resolvers and make the write-path one match the validation-path one; add an `exec`-level test, not just a `check` test. Issue #610 |
| `ALTER STYLING ON PAGE/SNIPPET ... SET ...` fails with `unsupported container type: PAGE` (and `DESCRIBE STYLING` silently shows "No widgets found"); even past that, design-property writes never reached builder-created pages | Two layered bugs: (1) the visitor emits uppercase `ContainerType` `"PAGE"`/`"SNIPPET"` but `execAlterStyling`/`execDescribeStyling` compared lowercase, so it fell through to the unsupported-container error; (2) ALTER STYLING used the reflection walker `walkPageWidgets` (legacy `ListPages`/`UpdatePage`), which can't locate widgets in MDL-builder pages and violates the mutator-pattern rule | `mdl/executor/cmd_styling.go` (`execAlterStyling`, `execDescribeStyling`) + `mdl/backend/mpr/page_mutator.go` | Normalise container type with `strings.ToLower`. Route ALTER STYLING through `ctx.Backend.OpenPageForMutation(unitID)` like ALTER PAGE; check `mutator.FindWidget`. Add `SetDesignProperty`/`RemoveDesignProperty`/`ClearDesignProperties` to the `PageMutator` interface, writing the widget's `Appearance.DesignProperties` BSON array (`Forms$DesignPropertyValue` → `Toggle`/`Option`/`Custom` value), preserving an existing custom kind on option updates. When an ALTER uses a container-type discriminator, mirror the casing fix already done for ALTER PAGE (#402). Issue #631 |
| `declare $x list of T = empty;` (or any list-typed `declare`) passes `mxcli check` but Studio Pro rejects with CE0053 ("type not allowed") + CE0038 ("value required") | `declare` maps to a Create Variable activity, which Mendix forbids from producing a list — but the validator only flagged an empty list *used as a loop source* (MDL002), never the declaration itself | `mdl/executor/validate_microflow.go` → `walkBody` `*ast.DeclareStmt` case | Emit `MDL040` (SeverityError) for any `stmt.Type.Kind == ast.TypeListOf`, regardless of initializer. Lists must come from a microflow parameter, a `retrieve`, or `$x = create list of T;`. Also fix the synced skills that present declare-list as valid (`write-microflows.md`, `cheatsheet-variables.md`, `check-syntax.md`, `patterns-*`). Issue #607 |

---
Expand Down
2 changes: 1 addition & 1 deletion docs/01-project/MDL_FEATURE_MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ Document types that exist in Mendix but have no MDL support:
|---------|------|----------|--------|-----------|------|-------|----------|-------|---------|------|-----|--------|------|-----|------|--------|----------|-------|
| **Microflow activities** | - | - | P | - | - | - | 02 | Y | P | P | P | Y | Y | - | - | - | P | 60+ activities supported; some edge cases missing |
| **Building blocks** | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | Reusable page building blocks |
| **Styling** | P | P | P | N | N | N | N | N | N | N | N | P | N | P | N | N | N | Class/Style/DesignProperties on widgets; full theme system not yet |
| **Styling** | P | Y | P | N | N | Y | Y | Y | N | N | N | P | N | P | N | N | N | Class/Style/DesignProperties on widgets via ALTER STYLING (#631); full theme system not yet |
| **Extensions** | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | Mendix extensions / add-ons |
| **Custom JS actions** | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | JavaScript actions for nanoflows |
| **Custom widgets** | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | N | Pluggable widget packages |
Expand Down
67 changes: 67 additions & 0 deletions mdl-examples/bug-tests/631-alter-styling-design-properties.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
-- ============================================================================
-- Bug #631: ALTER STYLING cannot set design properties on widgets in pages
-- and snippets
-- ============================================================================
--
-- Symptom (before fix):
-- `ALTER STYLING ON PAGE ... WIDGET ... SET ...` always failed with
-- "unsupported container type: PAGE". The visitor stored ContainerType as
-- uppercase "PAGE"/"SNIPPET" but the executor compared against lowercase, so
-- it fell through to the unsupported-container error. The same casing bug
-- silently broke DESCRIBE STYLING. Even past the casing issue, ALTER STYLING
-- used a reflection walker (walkPageWidgets) that could not locate widgets in
-- pages created by the MDL builder, so there was no working write path for
-- design properties.
--
-- After fix:
-- ALTER STYLING normalises the container type and goes through the page
-- mutator (OpenPageForMutation), which works uniformly on pages/snippets
-- created in Studio Pro and by the MDL builder. The mutator gained
-- SetDesignProperty / RemoveDesignProperty / ClearDesignProperties writing the
-- widget's Appearance.DesignProperties BSON array.
--
-- Usage:
-- mxcli exec mdl-examples/bug-tests/631-alter-styling-design-properties.mdl -p app.mpr
-- Open in Studio Pro — the container shows the css class, inline style, and
-- Atlas design properties (Spacing bottom = Large, Full width toggle).
-- ============================================================================

create module BugTest631;

-- A builder-created page (the case the old reflection walker could not handle).
create page BugTest631.P_Styling
(
title: 'Styling Target',
layout: Atlas_Core.Atlas_Default
)
{
container ctnTarget (class: 'old-class') {
dynamictext txtHello (content: 'Hello')
}
}

-- Class + inline style
ALTER STYLING ON PAGE BugTest631.P_Styling WIDGET ctnTarget
SET Class = 'card card-bordered',
Style = 'padding: 24px;';

-- Option (dropdown) design property
ALTER STYLING ON PAGE BugTest631.P_Styling WIDGET ctnTarget
SET 'Spacing bottom' = 'Large';

-- Toggle design property ON
ALTER STYLING ON PAGE BugTest631.P_Styling WIDGET ctnTarget
SET 'Full width' = ON;

-- Read back — should report the class, style, and both design properties
DESCRIBE STYLING ON PAGE BugTest631.P_Styling WIDGET ctnTarget;

-- Toggle OFF removes the property again
ALTER STYLING ON PAGE BugTest631.P_Styling WIDGET ctnTarget
SET 'Full width' = OFF;

-- Clear all design properties (Class/Style remain)
ALTER STYLING ON PAGE BugTest631.P_Styling WIDGET ctnTarget
CLEAR DESIGN PROPERTIES;

DESCRIBE STYLING ON PAGE BugTest631.P_Styling WIDGET ctnTarget;
58 changes: 35 additions & 23 deletions mdl-examples/doctype-tests/12-styling-examples.mdl
Original file line number Diff line number Diff line change
Expand Up @@ -572,26 +572,38 @@ describe styling on snippet StyleTest.MySnippet widget ctnSnippet;
-- Change Class, Style, or DesignProperties on a single widget without
-- rewriting the entire page. This is a targeted, in-place update.

-- TODO: ALTER STYLING cannot find widgets in pages created by the MDL page builder
-- because walkPageWidgets traverses LayoutCall.Arguments but the page parser doesn't
-- fully reconstruct the widget tree when re-reading builder-created pages.
-- These commands work on pages originally created in Studio Pro.

-- ALTER STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader
-- SET Class = 'card card-bordered';
-- ALTER STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader
-- SET Style = 'padding: 24px; border-radius: 12px;';
-- ALTER STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader
-- SET 'Spacing bottom' = 'Large';
-- ALTER STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader
-- SET 'Full width' = ON;
-- ALTER STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader
-- SET 'Full width' = OFF;
-- ALTER STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader
-- SET Class = 'card card-bordered',
-- Style = 'padding: 24px;',
-- 'Spacing bottom' = 'Large',
-- 'Full width' = ON;
-- ALTER STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader
-- CLEAR DESIGN PROPERTIES;
-- DESCRIBE STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader;
-- ALTER STYLING goes through the page mutator (issue #631), so it works on
-- pages created by the MDL builder as well as those authored in Studio Pro.

-- Set a CSS class
ALTER STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader
SET Class = 'card card-bordered';

-- Set an inline style
ALTER STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader
SET Style = 'padding: 24px; border-radius: 12px;';

-- Set an option (dropdown) design property
ALTER STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader
SET 'Spacing bottom' = 'Large';

-- Turn a toggle design property ON
ALTER STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader
SET 'Full width' = ON;

-- Turn a toggle design property OFF (removes it)
ALTER STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader
SET 'Full width' = OFF;

-- Combine Class, Style, and design properties in one statement
ALTER STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader
SET Class = 'card card-bordered',
Style = 'padding: 24px;',
'Spacing bottom' = 'Large',
'Full width' = ON;

-- Remove all design properties from a widget
ALTER STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader
CLEAR DESIGN PROPERTIES;

DESCRIBE STYLING ON PAGE StyleTest.P006_Roundtrip WIDGET ctnHeader;
1 change: 1 addition & 0 deletions mdl/ast/ast_styling.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func (s *AlterStylingStmt) isStatement() {}
type StylingAssignment struct {
Property string // "Class", "Style", or design property key (e.g., "Spacing top")
Value string // Value string (CSS class, style, or option name)
IsCSS bool // true when set via the CLASS/STYLE keyword (appearance), false for a quoted design-property key
IsToggle bool // true for ON/OFF values
ToggleOn bool // true for ON, false for OFF (only meaningful when IsToggle is true)
}
60 changes: 42 additions & 18 deletions mdl/backend/mock/mock_page_mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,28 @@ var _ backend.PageMutator = (*MockPageMutator)(nil)
// nil error (never panics). ContainerType defaults to ContainerPage when unset;
// all other methods return zero values.
type MockPageMutator struct {
ContainerTypeFunc func() backend.ContainerKind
SetWidgetPropertyFunc func(widgetRef string, prop string, value any) error
SetWidgetDataSourceFunc func(widgetRef string, ds pages.DataSource) error
SetColumnPropertyFunc func(gridRef string, columnRef string, prop string, value any) error
InsertWidgetFunc func(widgetRef string, columnRef string, position backend.InsertPosition, widgets []pages.Widget) error
DropWidgetFunc func(refs []backend.WidgetRef) error
ReplaceWidgetFunc func(widgetRef string, columnRef string, widgets []pages.Widget) error
InsertColumnsFunc func(gridRef, afterColumnRef string, position backend.InsertPosition, columns []*backend.DataGridColumnSpec) error
ReplaceColumnFunc func(gridRef, columnRef string, columns []*backend.DataGridColumnSpec) error
FindWidgetFunc func(name string) bool
AddVariableFunc func(name, dataType, defaultValue string) error
DropVariableFunc func(name string) error
SetLayoutFunc func(newLayout string, paramMappings map[string]string) error
SetPluggablePropertyFunc func(widgetRef string, propKey string, op backend.PluggablePropertyOp, ctx backend.PluggablePropertyContext) error
EnclosingEntityFunc func(widgetRef string) string
ContainerTypeFunc func() backend.ContainerKind
SetWidgetPropertyFunc func(widgetRef string, prop string, value any) error
SetWidgetDataSourceFunc func(widgetRef string, ds pages.DataSource) error
SetColumnPropertyFunc func(gridRef string, columnRef string, prop string, value any) error
SetDesignPropertyFunc func(widgetRef string, key string, valueType string, option string) error
RemoveDesignPropertyFunc func(widgetRef string, key string) error
ClearDesignPropertiesFunc func(widgetRef string) error
InsertWidgetFunc func(widgetRef string, columnRef string, position backend.InsertPosition, widgets []pages.Widget) error
DropWidgetFunc func(refs []backend.WidgetRef) error
ReplaceWidgetFunc func(widgetRef string, columnRef string, widgets []pages.Widget) error
InsertColumnsFunc func(gridRef, afterColumnRef string, position backend.InsertPosition, columns []*backend.DataGridColumnSpec) error
ReplaceColumnFunc func(gridRef, columnRef string, columns []*backend.DataGridColumnSpec) error
FindWidgetFunc func(name string) bool
AddVariableFunc func(name, dataType, defaultValue string) error
DropVariableFunc func(name string) error
SetLayoutFunc func(newLayout string, paramMappings map[string]string) error
SetPluggablePropertyFunc func(widgetRef string, propKey string, op backend.PluggablePropertyOp, ctx backend.PluggablePropertyContext) error
EnclosingEntityFunc func(widgetRef string) string
EnclosingEntityForChildrenFunc func(widgetRef string) string
WidgetScopeFunc func() map[string]model.ID
ParamScopeFunc func() (map[string]model.ID, map[string]string)
SaveFunc func() error
WidgetScopeFunc func() map[string]model.ID
ParamScopeFunc func() (map[string]model.ID, map[string]string)
SaveFunc func() error
}

func (m *MockPageMutator) ContainerType() backend.ContainerKind {
Expand Down Expand Up @@ -66,6 +69,27 @@ func (m *MockPageMutator) SetColumnProperty(gridRef string, columnRef string, pr
return nil
}

func (m *MockPageMutator) SetDesignProperty(widgetRef string, key string, valueType string, option string) error {
if m.SetDesignPropertyFunc != nil {
return m.SetDesignPropertyFunc(widgetRef, key, valueType, option)
}
return nil
}

func (m *MockPageMutator) RemoveDesignProperty(widgetRef string, key string) error {
if m.RemoveDesignPropertyFunc != nil {
return m.RemoveDesignPropertyFunc(widgetRef, key)
}
return nil
}

func (m *MockPageMutator) ClearDesignProperties(widgetRef string) error {
if m.ClearDesignPropertiesFunc != nil {
return m.ClearDesignPropertiesFunc(widgetRef)
}
return nil
}

func (m *MockPageMutator) InsertWidget(widgetRef string, columnRef string, position backend.InsertPosition, widgets []pages.Widget) error {
if m.InsertWidgetFunc != nil {
return m.InsertWidgetFunc(widgetRef, columnRef, position, widgets)
Expand Down
Loading
Loading