Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f18d404
fix(alter page): refuse SET DataSource = DATABASE instead of wiping t…
claude Sep 20, 2026
3c1bc5b
fix(lint): report the legacy image widgets, and correct when CE0582 a…
claude Sep 20, 2026
9fb5b4c
fix(pages): stop a page rewrite moving state nobody asked to change
claude Sep 20, 2026
31bf717
fix(pages): stop writing 11.1/10.17 header keys into older projects
claude Sep 20, 2026
12f84a9
Merge pull request #544 from ako/claude/exciting-clarke-wx2lxm
ako Sep 20, 2026
afc5186
Merge remote-tracking branch 'origin/main' into claude/brave-faraday-…
claude Sep 20, 2026
d195eb9
Merge pull request #545 from ako/claude/brave-faraday-tuo7z1
ako Sep 20, 2026
e406ed1
Merge branch 'mendixlabs:main' into main
ako Sep 20, 2026
bc019e0
Merge pull request #546 from ako/claude/funny-keller-rrm17w
ako Sep 20, 2026
52f3c11
fix(pages): stop describe losing a password field, its validation and…
claude Sep 20, 2026
3ac6df5
fix(describe): read IsPasswordBox through a named accessor
claude Sep 20, 2026
c2755df
Revert "fix(describe): read IsPasswordBox through a named accessor"
claude Sep 20, 2026
1c7de40
Merge branch 'main' into claude/compassionate-ramanujan-tr79mp
claude Sep 20, 2026
5f0c218
Merge pull request #547 from ako/claude/compassionate-ramanujan-tr79mp
ako Sep 20, 2026
e739fd2
Merge pull request #551 from ako/claude/funny-keller-rrm17w
ako Sep 20, 2026
fa289cc
fix(pages): judge a list widget's own row action in the context it cr…
claude Sep 20, 2026
0ecde78
Merge pull request #577 from ako/claude/fix-552-list-widget-row-action
ako Sep 20, 2026
2f15c67
fix(domain model): drop cross-module associations with the entity the…
claude Sep 20, 2026
2455ee9
fix(security): write no member access for the audit associations
claude Sep 20, 2026
e0831fe
Merge pull request #578 from ako/claude/fix-553-drop-view-entity
ako Sep 20, 2026
dcd5dc6
Merge remote-tracking branch 'origin/main' into claude/fix-554-autoow…
claude Sep 20, 2026
5741e8c
Merge pull request #579 from ako/claude/fix-554-autoowner-access-rule
ako Sep 20, 2026
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/commands/mendix/lint.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ mxcli lint -p app.mpr --exclude System --exclude Administration
| MPR005 | quality | ImageSource - IMAGE widgets with no source configured |
| MPR006 | quality | EmptyContainer - Empty layout containers |
| MPR007 | security | PageNavigationSecurity - Navigation pages need allowed roles (CE0557) |
| MPR012 | correctness | LegacyImageWidget - staticimage/dynamicimage are unsupported by the React client (CE0582) |
| SEC001 | security | NoEntityAccessRules - Persistent entities need access rules |
| SEC002 | security | WeakPasswordPolicy - Password minimum length should be 8+ |
| SEC003 | security | DemoUsersActive - Demo users should be off at Production security |
Expand Down
4 changes: 4 additions & 0 deletions .claude/skills/fix-issue/findings/mdl-backend.jsonl

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions .claude/skills/fix-issue/findings/mdl-executor.jsonl

Large diffs are not rendered by default.

23 changes: 18 additions & 5 deletions .claude/skills/mendix/alter-page/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ set Action = SHOW_PAGE Module.DetailPage on btnEdit

-- Rebind a data-bound widget
set DataSource = $OrderParam on dvOrder
set DataSource = DATABASE Module.Order on dgOrders
set DataSource = microflow Module.MF_Get on dvOrder
```

**Prefer `set Action` over `replace` when only the action changes.** `replace`
Expand Down Expand Up @@ -198,7 +198,6 @@ from a microflow to a page parameter:
```sql
ALTER PAGE MyModule.OrderPage {
SET DataSource = $Order ON dvOrder; -- page/snippet parameter
SET DataSource = database MyModule.Order ON dgOrders; -- database
SET DataSource = microflow MyModule.MF_Get ON dvOrder; -- microflow
SET DataSource = nanoflow MyModule.NF_Get ON dvOrder; -- nanoflow
SET DataSource = selection dgOrders ON dvDetail; -- listen to widget
Expand All @@ -209,9 +208,23 @@ The parameter must exist on the page (or snippet) being altered — its entity i
read from the container's own parameter list, and an unknown name is refused
rather than written as an unresolved reference.

`association` sources are **not** supported by SET. Use REPLACE for those, which
rebuilds the widget through the CREATE PAGE path and handles every datasource
type; the error message says so.
`association` and `database` sources are **not** supported by SET. Use REPLACE
for those, which rebuilds the widget through the CREATE PAGE path and handles
every datasource type; the error message says so.

A `database` source has no single stored shape — the widget holding it decides
which element Mendix writes (a list view, a data grid and a pluggable widget
each store a different one), and SET writes the property directly rather than
rebuilding the widget, so it has nothing to choose from. This used to be
accepted and half-applied: the widget was left with a source that DESCRIBE read
back as absent and mxbuild rejected as **CE7007**, on a page `exec` had just
reported as altered (mendixlabs/mxcli#1032).

A **data view** is the one case REPLACE does not rescue: it binds to a single
object, so Mendix gives it no database form at all and the CREATE PAGE path
refuses one too. Point it at a context parameter, a microflow, a nanoflow or
`selection <widget>`, and use a list view or a data grid to show a query. The
refusal says which of the two situations you are in.

### INSERT - Add Widgets

Expand Down
7 changes: 7 additions & 0 deletions .claude/skills/mendix/create-page/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Both are optional and can be changed later with `alter page … { set Class = '
| Widget name | Required after type | `textbox txtName (...)` |
| Attribute binding | `attribute: AttrName` | `textbox txt (label: 'Name', attribute: Name)` |
| Attribute over an association | `attribute: Assoc/Attr` (bare association name, multi-hop OK) | `textbox txt (label: 'Rule', attribute: RuleAction_BusinessRule/Name)` |
| Password field | `Password: true` | `textbox tbPw (attribute: Secret, Password: true)` |
| Widget validation | `Validation: '<expr>'` + `ValidationMessage: '<text>'` | `Validation: 'length(toString($value)) > 0'` — quoted, not `[bracketed]` |
| Variable binding | `datasource: $Var` | `dataview dv (datasource: $Product) { ... }` |
| Action binding | `action: type` | `actionbutton btn (caption: 'Save', action: save_changes)` |
| Database source | `datasource: database entity` | `datagrid dg (datasource: database Module.Entity)` |
Expand Down Expand Up @@ -433,6 +435,11 @@ DATAVIEW dv (DataSource: $Issue) {

A bare association name is qualified with the module of the entity the widget
sits on. On a ComboBox that matters: its `DataSource:` is the *option list*, but
A text box that holds a secret needs `Password: true`. It is not cosmetic: without
it the field renders the value in plaintext, and before ako/mxcli#550 a
`describe page` → `exec` round trip silently turned every stored password field
into an ordinary one — so copying a login or change-password page lost it.

An input widget can also *traverse* an association to show a value from the
other side: `attribute: Assoc/Attr` binds the far attribute and stores the hops,
which is what Studio Pro does. It works on textbox, textarea, datepicker,
Expand Down
16 changes: 10 additions & 6 deletions .claude/skills/mendix/create-page/reference/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -698,10 +698,13 @@ staticimage imgAllSelected (Image: 'MyFirstModule.Images.gallery')
auto units and a responsive image — which `describe page` also omits, so a
round trip neither loses them nor invents them.

Mendix 11's React client reports **CE0582** for `staticimage` wherever it
appears — it is deprecated in favour of the pluggable `image` widget, which
takes the same `Image:`. mxcli still writes it, because round-tripping a model
that already contains one is the point; prefer `image` on a new page.
**CE0582** is reported for `staticimage` wherever it appears, by any app running
the React client — which Mendix added in **10.7** and which is the only client on
11, so this is not a Mendix 11 rule. The replacement is the pluggable `image`
widget, which takes the same `Image:`; Studio Pro offers the conversion from the
CE0582 error's context menu. mxcli still writes it, because round-tripping a
model that already contains one is the point — and `mxcli lint` reports it as
**MPR012** so a new page does not reach for it by accident.

#### `DataSource:` — which object a DYNAMICIMAGE shows

Expand Down Expand Up @@ -729,8 +732,9 @@ same three-part way as `staticimage`'s `Image:`. `WidthUnit:`/`HeightUnit:`,
written; leave them out for Mendix's defaults (auto, responsive, full size, no
enlarge), which `describe page` also omits.

CE0582 applies here too — `dynamicimage` is deprecated alongside `staticimage`,
and the pluggable `image` widget is the replacement for both.
CE0582 applies here too — the React client supports neither legacy image widget,
and the pluggable `image` widget is the replacement for both. `mxcli lint` reports
either as **MPR012**.

#### Setting Image Source (PLUGGABLEWIDGET syntax)

Expand Down
1 change: 1 addition & 0 deletions cmd/mxcli/cmd_lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ func builtinLintRules() []linter.Rule {
rules.NewDomainModelSizeRule(),
rules.NewValidationFeedbackRule(),
rules.NewImageSourceRule(),
rules.NewLegacyImageWidgetRule(),
rules.NewEmptyContainerRule(),
rules.NewGallerySelectionListenerRule(),
rules.NewDataViewLayoutGridRule(),
Expand Down
12 changes: 7 additions & 5 deletions cmd/mxcli/syntax/features_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func init() {
"page", "pages", "form", "UI", "user interface",
"widget", "layout", "screen",
},
Syntax: "CREATE PAGE Module.Name\n (\n Title: 'Page Title',\n Layout: Module.LayoutName\n [, Params: { $Param: Module.Entity }]\n [, Url: 'page-url']\n [, Folder: 'FolderPath']\n [, Variables: { $var: Boolean = 'true' }]\n [, PopupWidth: 800, PopupHeight: 480, PopupResizable: true]\n [, Class: 'css-class', Style: 'css: rule']\n )\n {\n -- widgets\n }",
Syntax: "CREATE PAGE Module.Name\n (\n Title: 'Page Title',\n Layout: Module.LayoutName\n [, Params: { $Param: Module.Entity }]\n [, Url: 'page-url']\n [, Folder: 'FolderPath']\n [, Variables: { $var: Boolean = 'true' }]\n [, PopupWidth: 800, PopupHeight: 480, PopupResizable: true]\n [, PopupCloseAction: cancelButton1]\n [, Class: 'css-class', Style: 'css: rule']\n )\n {\n -- widgets\n }",
Example: "CREATE PAGE MyModule.EditCustomer\n (\n Params: { $Customer: MyModule.Customer },\n Title: 'Edit Customer',\n Layout: Atlas_Core.PopupLayout,\n Class: 'container-fluid'\n )\n {\n DATAVIEW dvCustomer (DataSource: $Customer) {\n TEXTBOX txtName (Label: 'Name', Attribute: Name)\n FOOTER footer1 {\n ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)\n ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)\n }\n }\n }",
SeeAlso: []string{"page.create", "page.widgets", "page.alter", "snippet"},
})
Expand Down Expand Up @@ -166,9 +166,11 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) {
"-- so a stored one round-trips through DESCRIBE (mendixlabs/mxcli#1057). Without\n" +
"-- it the widget is written with no image and mxbuild reports CE0436:\n" +
"STATICIMAGE imgLogo (Image: 'MyModule.Images.logo', Width: 64, Height: 64)\n\n" +
"-- Deprecated in the Mendix 11 React client. These are written correctly by\n" +
"-- both engines, but mxbuild reports CE0582 (\"not supported in React client\")\n" +
"-- on each, so prefer the alternative:\n" +
"-- Not supported by the React client — added in Mendix 10.7, and the only\n" +
"-- client on 11, so this is not a Mendix 11 rule. These are written correctly,\n" +
"-- but mxbuild reports CE0582 (\"not supported in React client\") on each\n" +
"-- wherever that client is enabled, and `mxcli lint` reports them as MPR012.\n" +
"-- Prefer the alternative:\n" +
"-- STATICIMAGE -> IMAGE\n" +
"-- DYNAMICIMAGE -> IMAGE\n" +
"-- DROPDOWN -> COMBOBOX\n" +
Expand Down Expand Up @@ -275,7 +277,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) {
"popup width", "popup height", "popup resizable",
"drop template", "insert template", "list view template",
},
Syntax: "ALTER PAGE Module.Name {\n SET property = value ON widgetName; -- widget property names: any casing\n SET Action = MICROFLOW Module.MF ON btnSave; -- any CREATE PAGE action form\n SET DataSource = $Param ON dvOrder;\n SET (prop1 = val1, prop2 = val2) ON widgetName;\n SET Title = 'New Title'; -- page-level (case-sensitive)\n SET Documentation = 'What this page is for.';\n SET Class = 'css-class'; -- page-level CSS class / style\n SET Style = 'css: rule';\n SET PopupWidth = 800; -- page-level pop-up dimensions\n SET PopupHeight = 480;\n SET PopupResizable = true;\n INSERT AFTER widgetName { <widgets> };\n INSERT BEFORE widgetName { <widgets> };\n INSERT INTO containerName { <widgets> };\n DROP WIDGET name1, name2;\n DROP TEMPLATE FOR Module.Specialization IN listViewName;\n REPLACE widgetName WITH { <widgets> };\n};",
Syntax: "ALTER PAGE Module.Name {\n SET property = value ON widgetName; -- widget property names: any casing\n SET Action = MICROFLOW Module.MF ON btnSave; -- any CREATE PAGE action form\n SET DataSource = $Param ON dvOrder; -- parameter/microflow/nanoflow/selection;\n -- DATABASE and association are REPLACE-only,\n -- and a data view takes no database source\n SET (prop1 = val1, prop2 = val2) ON widgetName;\n SET Title = 'New Title'; -- page-level (case-sensitive)\n SET Documentation = 'What this page is for.';\n SET Class = 'css-class'; -- page-level CSS class / style\n SET Style = 'css: rule';\n SET PopupWidth = 800; -- page-level pop-up dimensions\n SET PopupHeight = 480;\n SET PopupResizable = true;\n INSERT AFTER widgetName { <widgets> };\n INSERT BEFORE widgetName { <widgets> };\n INSERT INTO containerName { <widgets> };\n DROP WIDGET name1, name2;\n DROP TEMPLATE FOR Module.Specialization IN listViewName;\n REPLACE widgetName WITH { <widgets> };\n};",
Example: "ALTER PAGE Module.EditPage {\n SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave;\n INSERT AFTER txtName {\n TEXTBOX txtMiddleName (Label: 'Middle Name', Attribute: MiddleName)\n };\n DROP WIDGET txtUnused;\n};",
SeeAlso: []string{"page.create", "page.show", "snippet.alter"},
})
Expand Down
4 changes: 4 additions & 0 deletions docs/01-project/MDL_QUICK_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1379,6 +1379,8 @@ MDL uses explicit property declarations for pages:
|---------|-----------|---------|
| Page properties | `(key: value, ...)` | `(title: 'Edit', layout: Atlas_Core.Atlas_Default)` |
| Pop-up dimensions | `PopupWidth: n, PopupHeight: n, PopupResizable: bool` | `(Layout: Atlas_Core.PopupLayout, PopupWidth: 800, PopupHeight: 480, PopupResizable: true)` — case-sensitive; default 600×600 |
| Pop-up close button | `PopupCloseAction: <widgetName>` | `(Layout: Atlas_Core.PopupLayout, PopupCloseAction: cancelButton1)` — names a widget on this page. Not carried from the stored document on a rewrite: the statement rebuilds the widget tree, so a carried name could dangle |
| DataView read-only style | `ReadOnlyStyle: Inherit\|Control\|Text` | `dataview dv (datasource: $O, ReadOnlyStyle: Text)` — a DataView's own, distinct from a checkbox's. **Control** is Studio Pro's default here, not Inherit |
| Page CSS class / style | `Class: 'css-class', Style: 'css: rule'` | `(Title: 'Home', Class: 'container-fluid bg-light', Style: 'min-height: 100vh')` — the page's Appearance |
| Page variables | `variables: { $name: type = 'expr' }` | `variables: { $show: boolean = 'true' }` |
| Repeated widget entries | `<container> <name> ( … )` **in the widget body** | A repeatable property (FileUploader `allowedFileFormats`, HTML Element `attributes`, a chart's `series`) is a block, never a property value. `attributes: [(attributeName: 'x')]` is **MDL-WIDGET27** — it used to check clean, exec, and vanish from storage. `describe widget <name> -p app.mpr` lists the container keywords |
Expand All @@ -1388,6 +1390,8 @@ MDL uses explicit property declarations for pages:
| Widget name | Required after type | `textbox txtName (...)` |
| Attribute binding | `attribute: AttrName` | `textbox txt (label: 'Name', attribute: Name)` |
| Attribute over an association | `attribute: Assoc/Attr` (bare association name, multi-hop OK) | `textbox txt (label: 'Rule', attribute: RuleAction_BusinessRule/Name)` — works on textbox, textarea, datepicker, dropdown, checkbox and radiobuttons, the same as on a data grid column |
| Password field | `Password: true` on a textbox | `textbox tbPw (attribute: Secret, Password: true)` — omitted when false. Without it a describe → exec round trip turns a password field into a plaintext one |
| Widget validation | `Validation: '<expression>'`, `ValidationMessage: '<text>'` | `Validation: 'length(toString($value)) > 0'` — a Mendix expression over `$value`, QUOTED not bracketed (`[...]` is the XPath spelling and parses as an array) |
| Variable binding | `datasource: $Var` | `dataview dv (datasource: $Product) { ... }` |
| Action binding | `action: type` | `actionbutton btn (caption: 'Save', action: save_changes)` — the forms are a closed set (`mxcli syntax page.action`); anything else is **MDL-WIDGET28** |
| No action | `action: nothing` | `actionbutton btn (caption: 'Decorative', action: nothing)` — an explicitly inert control. Write it deliberately: an action keyword **short its argument** (`action: open_link` with no URL) is now an error rather than a widget silently written with no action at all |
Expand Down
106 changes: 106 additions & 0 deletions mdl-examples/bug-tests/1032-alter-page-set-database-datasource.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
-- mendixlabs/mxcli#1032 — `SET DataSource = DATABASE …` silently wiped a
-- DataView's datasource
--
-- The reported statement
--
-- ALTER PAGE Test.MyPage { SET DataSource = DATABASE Test.Customer ON dvCust; };
--
-- passed `check`, ran to `Altered page …` with exit 0, and left the DataView
-- with no usable datasource at all. `DESCRIBE PAGE` read the widget back as
-- `dataview dvCust {` with the property gone, and the only other signal was
-- CE7007 at build time — naming the widget, never the statement that broke it.
--
-- Cause: one mapping standing in for several. A DATABASE source has no single
-- stored shape; the widget holding it decides which element Studio Pro writes —
-- Forms$ListViewXPathSource on a list view, CustomWidgets$CustomWidgetXPathSource
-- on a pluggable widget, Forms$GridXPathSource on a grid. A DATA VIEW has no
-- database form at all: it binds to ONE object, which is why CREATE PAGE's own
-- builder refuses that pairing. The mutator instead wrote a Forms$DataViewSource
-- — the "data from context" source — with the entity in EntityRef and
-- SourceVariable left null, which is neither shape and is why DESCRIBE (whose
-- context reader needs a SourceVariable) rendered nothing.
--
-- Measured on a real Mendix 11.13.0 app, two copies of the same project:
--
-- with the fault mxcli check --references → "Check passed!"
-- mxcli exec → "Altered page …", exit 0
-- describe page → dataview dvCust { … } (no source)
-- mx check → 1 error, CE7007 at Data view 'dvCust'
--
-- with the fix mxcli check --references → refused, exit 1
-- mxcli exec → refused, exit 1
-- describe page → datasource unchanged
-- mx check → 0 errors
--
-- The refusal lives once, in the mutator, so `check` (which dry-runs the SET
-- against a throwaway copy of the stored document) and `exec` cannot disagree.
-- It needs the document to know the widget's type, so it fires under `-p`; this
-- file is therefore the PASSING shape, with the refused statements commented.
--
-- Run: mxcli check mdl-examples/bug-tests/1032-alter-page-set-database-datasource.mdl
-- mxcli exec mdl-examples/bug-tests/1032-alter-page-set-database-datasource.mdl -p app.mpr

create module P1032;
create persistent entity P1032.Customer (Name: String(200));

create microflow P1032.MF_GetCustomer () returns P1032.Customer as $Result
begin
retrieve $Result from P1032.Customer limit 1;
end;

create page P1032.CustomerPage (
params: { $Customer: P1032.Customer },
title: 'Customer',
layout: Atlas_Core.Atlas_Default
) {
layoutgrid g { row r { column c (desktopwidth: 12) {
-- A LIST VIEW is where a database query belongs. Authored here through
-- CREATE PAGE, which builds the Forms$ListViewXPathSource the widget needs.
listview lvCustomers (datasource: database P1032.Customer) {
textbox tbListName (attribute: Name, label: 'Name')
}
-- The DataView from the report, on a microflow source.
dataview dvCust (datasource: microflow P1032.MF_GetCustomer) {
textbox tbName (attribute: Name, label: 'Name')
}
} } }
}

-- The sources a DataView really takes still retype through SET, unchanged by
-- the refusal — the control that says this fixes #1032 rather than replacing it
-- with a blanket "no".
alter page P1032.CustomerPage {
set DataSource = $Customer on dvCust;
}

alter page P1032.CustomerPage {
set DataSource = microflow P1032.MF_GetCustomer on dvCust;
}

alter page P1032.CustomerPage {
set DataSource = selection lvCustomers on dvCust;
}

-- REFUSED, both by `check -p --references` and by `exec` (uncomment to see):
--
-- alter page P1032.CustomerPage {
-- set DataSource = database P1032.Customer on dvCust;
-- }
--
-- a data view cannot take a database datasource — a data view binds to a
-- single object, so its source is a context parameter (`$Param`), a
-- microflow, a nanoflow or `selection <widget>`; to show the result of a
-- database query, use a list view or a data grid instead
--
-- The same statement against the LIST VIEW is refused too, but for the other
-- reason and with the other remedy — a list view can hold a database source,
-- SET just cannot build one, so the message names the path that can:
--
-- alter page P1032.CustomerPage {
-- set DataSource = database P1032.Customer on lvCustomers;
-- }
--
-- setting a database datasource on "lvCustomers" (Forms$ListView) is not
-- supported by `set` — its stored shape depends on the widget and is built
-- by the CREATE PAGE path; use `replace <widget> with …` instead, which
-- rebuilds the widget through that path
Loading
Loading