Skip to content

Commit f0b4547

Browse files
committed
fix(widgets): write and describe a Gallery's OnClick action
The hand-written Gallery definition mapped onClickTrigger but not the onClick action slot, so OnClick: on a gallery was accepted and stored as NoAction. Map it like DataGrid2's row click, and read it back in describe. Field run: ProcureFlow (Mendix 11.14.0) scratch copy, gallery over System.WorkflowUserTask with OnClick: microflow InvoiceProcessing.ACT_UserTask_Open(WorkflowUserTask: $currentObject): stored as Forms$MicroflowAction with the parameter mapping, describe prints it, mx check 0 errors. A click on an item also selects it, and Mendix rejects an item click action beside a selection: on a blank Mendix 11.14.0 app, OnClick with the default Selection (Single) wrote the page and mx check reported "The item click action is ambiguous". Refuse that at check time (MDL-WIDGET35, remedy selection: none). describe omitted a stored Selection None — the builder's default is Single — so a describe -> exec round trip of such a gallery turned None into Single and was then refused; it now prints Selection: None. Measured on a blank 11.14.0 app: gallery with selection: none + OnClick -> exec, mx check 0 errors, describe -> exec "Unchanged page"; without selection: none -> MDL-WIDGET35 at check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5dJ4raEjbm8bFiKeYtchS
1 parent 9509176 commit f0b4547

13 files changed

Lines changed: 231 additions & 0 deletions

File tree

‎.claude/skills/fix-issue/findings/mdl-executor.jsonl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,3 +713,4 @@
713713
{"area": "mdl/executor", "date": "2026-09-25", "symptom": "`textbox t (Attribute: FullName)` at the top of a page (CREATE PAGE/SNIPPET, a plain container, or ALTER PAGE … INSERT at page level) passed plain `mxcli check`, `exec --no-check`/ALTER reported success, and `bson dump` showed `AttributeRef: null` — mxbuild 11.13.0: CE0544 \"This widget can only function inside a data context\" + CE7005 (textbox/textarea/datepicker/checkbox/radiobuttons/dropdown), CE0402 (dynamictext Attribute:), CE0642 (combobox). Qualified `Mod.Ent.Attr` there is stored and fails CE0544/CE2421/CE1365/CE7247 \"Move this widget into a data container\" + CE7006. `Attribute: $P/Attr` / `$currentObject/Attr` dropped even INSIDE a data view.", "cause": "resolveAttributePath returns the bare name when entityContext is \"\", and attributeRefToGen (and widgetobj setAttributeRefField) write nil for any path with < 2 dots, so the binding vanished between builder and writer; refuseBareAttributeRefs never sees it because no Attribute string is emitted. The only refusal (validatePageContextTree) runs in the --references phase for CREATE PAGE/SNIPPET, so plain check, --no-check and ALTER were unguarded. `$x/Attr` parses via the generic property rule as an *ast.DataSourceV3, so GetAttribute() returns \"\" and every builder skipped it.", "file": "mdl/executor/cmd_pages_input_binding_context.go (inputBindingProblem, checkInputBinding, validateInputBindingContext = MDL-WIDGET34), wired in cmd_pages_builder_v3_widgets.go (6 input builders + buildDynamicTextV3), widget_engine.go (primary Attribute mapping), validate_widgets.go (validateWidgetTreeIn); tests cmd_pages_input_binding_context_test.go; bug-tests input-binding-without-context{,.fail}.mdl", "insight": "Reuse the MDL-PAGEARG01 three-state context (pageArgContext known/present) rather than entityContext==\"\" as the 'outside a data container' signal: entityContext is also empty INSIDE a container whose flow cannot be resolved (excluded ShareFeedback_Logo), where DESCRIBE writes qualified names that must keep building — refusing qualified-on-empty-entity would have broken that round trip. So known-absent context refuses bare AND qualified; unknown context (ALTER) refuses only the bare name the writer provably nulls. Two existing unit tests (OnChangeSurvivesBuilder, DynamicTextV3_AttributeBinds) built inputs with NO entity and passed — the second asserted a bare `Title` AttributeRef counted as 'bound', i.e. it pinned the bug: when a fixture has no entity context, ask what the writer does with its output. The `$P/Attr` drop was found only by dumping the control page, not from the report — print the AST value type with a probe test before assuming a spelling reaches the builder. Evidence: 22 mxbuild errors before on the probe matrix; after, every case refused with nothing written, controls (dataview/listview/gallery/datagrid/snippet dataview/ALTER into dataview) 0 errors, 17/17 stock pages + 4/4 snippets describe→exec round trip.", "refs": ["MDL-WIDGET34"], "ce": ["CE0544", "CE7005", "CE0402", "CE0642", "CE2421", "CE1365", "CE7247", "CE7006"]}
714714
{"area":"mdl/executor","date":"2026-09-25","symptom":"`alter page FeedbackModule.ShareFeedback_Logo { insert after textBox1 { image zzImg (ImageType: imageUrl, ImageUrl: '{1}', ImageUrlParams: [{1} = ImageB64]) } }` passed `check --references`; exec wrote a bare AttributeRef and `mx check` could not LOAD the project (ArgumentNullException setting 'Attribute'). Same at page top level outside any data container; a text box's `Attribute:` there is silently dropped (CE7005).","cause":"ALTER's entity context comes from the STORED document (nearest enclosing data source, or a flow source's return type via resolveDataSourceFlowEntity). With the flow missing (Feedback v4.0.2 ships no DS_FeedbackForm) or no container at all, entityContext is \"\" and resolveAttributePath returns the bare name. CREATE PAGE refused this at check time (relaxExcludedWidgetRefs/unscopedBindings, #678); ALTER's check never opened the document, so nothing could know the scope.","file":"mdl/executor/validate_alter_unscoped.go, mdl/executor/cmd_alter_page.go (alterEntityContext), mdl/executor/validate.go (bindingsWithoutScope)","insight":"For ALTER, scope is a property of the stored document, not the statement: a check-time question about it must open the document (OpenPageForMutation, never Save; validate_alter_set.go already does this) and ask through the SAME function exec uses, so the INSERT/REPLACE entity resolution was lifted into alterEntityContext rather than restated, and the binding walk lifted out of unscopedBindings (bindingsWithoutScope) rather than copied. Controls that keep it from blocking working scripts: skip a target the stored doc lacks (added earlier in the script), a flow the script declares with an entity return (sc.flowParams), DataGrid2 column and list-view-template paths, and documents the script creates. Reproduce with a Studio Pro-authored page whose flow is genuinely absent.","refs":["#678","#685"]}
715715
{"area": "mdl/executor", "date": "2026-09-25", "symptom": "`describe page` on a File Uploader (files mode) emits `DataSource: association …`, and exec of that output fails: \"widget `upFiles` (fileuploader) exposes 2 datasources, so a generic `datasource:` clause is ambiguous — name the one you mean: associatedFiles, associatedImages\"", "cause": "DESCRIBE chose generic vs named by counting CONFIGURED datasources (namedCustomWidgetDataSources drops unset ones, so files mode = 1), while the builder's refuseAmbiguousGenericDataSource counts DECLARED datasource mappings (a generated def maps every top-level datasource = 2). Two sides of one round trip deciding the same question from different evidence.", "file": "mdl/executor/cmd_pages_describe_parse.go", "insight": "When describe and build each decide 'is this ambiguous?', they must count the same set. Fix read the DECLARED count from the stored schema (PropertyTypes with ValueType.Type=DataSource, excluding IsLinked) and excluded widgets with an embedded .def.json — those are hand-written and pick one datasource mapping per mode, so a database-mode ComboBox (two declared) must keep the generic clause. The #956 bug-test script itself authored the refused generic clause on a File Uploader: a bug-test that only runs `mxcli check` without a project can't see an exec-time refusal, so grep bug-tests for the old spelling whenever a builder starts refusing one.", "refs": ["mendixlabs/mxcli#1199", "mendixlabs/mxcli#956", "mendixlabs/mxcli#1109"], "rules": []}
716+
{"area": "mdl/executor", "date": "2026-09-26", "symptom": "`gallery g (…, OnClick: microflow M.Open(Item: $currentObject))`: check clean, exec 'Created page', stored gallery `onClick` is Forms$NoAction; describe shows no OnClick", "cause": "The Gallery definition in sdk/widgets/definitions is hand-written and never regenerated per project; it mapped `onClickTrigger` but not the `onClick` action, and `OnClick` is a built-in MDL property name so MDL-WIDGET01/06 never inspect it. Describe had no read for the gallery click slot either.", "file": "sdk/widgets/definitions/gallery.def.json", "insight": "Generated defs gained action mappings in generator version 13, but the nine hand-written definitions (combobox, gallery, image, filters, …) do not pass through the generator, so every generator-side fix skips them. When a generated-def fix lands, diff the hand-written defs against what the generator would emit for the same .mpk. Settled by reading back the stored WidgetValue.Action of the gallery's onClick property.", "rules": []}

‎.claude/skills/mendix/create-page/reference/widgets.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,24 @@ gallery productGallery (datasource: database Module.Product, selection: single)
571571
}
572572
```
573573

574+
**With a click action** — `onclick:` on the gallery runs when an item is clicked,
575+
with the clicked item as `$currentObject` (the gallery's "On click action", the
576+
same slot as a Data grid 2 row click). Any action form works. It needs
577+
`selection: none`: the click that runs the action is the click that would select
578+
the item, and Mendix rejects the pair as ambiguous — the default selection is
579+
`Single`, so leaving it out is **MDL-WIDGET35**:
580+
```sql
581+
gallery orderGallery (
582+
datasource: database Module.Order,
583+
selection: none,
584+
onclick: microflow Module.ACT_Order_Open(Order: $currentObject)
585+
) {
586+
template template1 {
587+
dynamictext orderNr (content: '{1}', contentparams: [{1} = Number])
588+
}
589+
}
590+
```
591+
574592
### Filter Widgets
575593

576594
A filter widget lives in one of two places: **inside a DATA GRID column's own braces**,

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1414

1515
### Fixed
1616

17+
- **A Gallery's `OnClick:` was dropped without a word** — Gallery declares an `onClick` action for the clicked item, the slot a Data grid 2 row click lives in, but the hand-written Gallery definition mapped only `onClickTrigger`. `gallery g (…, OnClick: microflow M.Open(Item: $currentObject))` passed `check` (OnClick is a built-in property name, so nothing flagged it), `exec` said `Created page`, and the widget was stored with `Forms$NoAction`. It is now written as a `Forms$MicroflowAction` (or any other action) on the gallery's `onClick`, and `describe` prints it back. A click on an item also selects it, and Mendix rejects an item click action beside a selection (mx check: "The item click action is ambiguous"); Selection defaults to Single, so `OnClick:` on a gallery without `selection: none` is refused at check time (**MDL-WIDGET35**) instead of failing the build. `describe` now prints a gallery's stored `Selection: None`, which it used to omit — the default is Single, so a describe → exec round trip turned None into Single.
1718
- **Keyword operators were fused in some stored expressions** — a page action's microflow argument `Flag: $a and $b` was stored as `$aand$b`, `if $x then 'a' else 'b'` as `if$xthen'a'else'b'`; the same in `contentparams` values, `send rest request … with (…)` parameters and a dynamic `execute database query`. Those four places took the expression's text without its whitespace; literals, `+` and `$currentObject` were unaffected, which is why it went unnoticed, and `check --references` passed. Measured by decoding the stored units on a Mendix 11.14.0 project. The expression is now stored as written, with MDL comments removed, as the microflow expression sites already did.
1819
- **An input bound to an attribute outside any data container was written with no binding** — `textbox t (Attribute: FullName)` at the top of a page (or of a snippet, or inside a plain container) has no entity to qualify the name with, and the writer stored it as `AttributeRef: null`. Plain `check` passed, `exec --no-check` and `alter page … insert` at page level said success, and mxbuild 11.13.0 failed the page with CE0544 "This widget can only function inside a data context" + CE7005 (text box, text area, date picker, check box, radio buttons, drop-down), CE0402 (dynamic text `Attribute:`) or CE0642 (combo box). A qualified attribute there is stored and fails the same way (CE0544 / CE2421 / CE1365 / CE7247 "Move this widget into a data container"). `Attribute: $P/Name` and `Attribute: $currentObject/Name` never parsed as an attribute path and were dropped even inside a data view. `check` now reports all three as **MDL-WIDGET34** (no project needed), and the page builder refuses them with the widget named, so nothing is written; `alter page` refuses a bare name it has no entity for. Place the widget in a data view, list view, gallery or data grid and bind the attribute by name.
1920
- **An expression property written in brackets was silently dropped** (mendixlabs/mxcli#750) — `dynamicclasses: [ if $currentObject/Featured then 'a' else 'b' ]`, the spelling #750 proposes, parsed as a list that no writer reads: `check` was clean, `exec` said `Created page`, and the widget was stored with no dynamic class. `alter page … set DynamicClasses = [ … ]` said `Altered page` and changed nothing, and a column's `DynamicCellClass` stored the list's text — tokens fused, `[if$x/Ythen'a'else'b']` — as its expression. Measured on a copy of a Mendix 11.14.0 project with the pre-fix binary. `mxcli check` now reports **MDL-WIDGET32** for `DynamicClasses` and `DynamicCellClass` written as a list (no project needed), and ALTER refuses it, so `check -p` reports that too. Write the expression quoted.

‎docs-site/src/language/widget-types.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,22 @@ GALLERY galBoard (
224224
}
225225
```
226226

227+
**Click action** — `OnClick:` runs when an item is clicked, with the clicked item
228+
as `$currentObject`. It takes any action form, as a button does. A click also
229+
selects an item, so Mendix accepts an item click action only on a gallery with
230+
`Selection: None`. Selection defaults to `Single`, so an `OnClick:` without
231+
`Selection: None` is refused (**MDL-WIDGET35**):
232+
233+
```sql
234+
GALLERY galOrders (
235+
DataSource: DATABASE MyModule.Order,
236+
Selection: None,
237+
OnClick: MICROFLOW MyModule.ACT_Order_Open(Order: $currentObject)
238+
) {
239+
DYNAMICTEXT txtNr (Content: '{1}', Attribute: Number)
240+
}
241+
```
242+
227243
## Input Widgets
228244

229245
All input widgets share common properties:

‎mdl/executor/cmd_pages_describe_output.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,9 @@ func outputWidgetMDLV3(ctx *ExecContext, w rawWidget, indent int) {
717717
if w.Selection != "" {
718718
props = append(props, fmt.Sprintf("Selection: %s", w.Selection))
719719
}
720+
if w.OnClick != "" {
721+
props = append(props, actionProp("OnClick", w.OnClick))
722+
}
720723
props = appendAppearanceProps(props, w)
721724
// Output filter and content widgets
722725
hasContent := len(w.Children) > 0 || len(w.FilterWidgets) > 0

‎mdl/executor/cmd_pages_describe_parse.go‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,12 @@ func parseRawWidget(ctx *ExecContext, w map[string]any, parentEntityContext ...s
471471
if widget.RenderMode == "gallery" {
472472
widget.DataSource = extractGalleryDataSource(ctx, w)
473473
widget.Selection = extractGallerySelection(ctx, w)
474+
// A stored None must be printed: the Gallery definition's default is
475+
// Single, so leaving it out turned None into Single on describe ->
476+
// exec — and a gallery with OnClick needs None (MDL-WIDGET35).
477+
if widget.Selection == "" && customWidgetPropertySelection(w, "itemSelection") == "None" {
478+
widget.Selection = "None"
479+
}
474480
widget.DesktopColumns = extractCustomWidgetPropertyString(ctx, w, "desktopItems")
475481
widget.TabletColumns = extractCustomWidgetPropertyString(ctx, w, "tabletItems")
476482
widget.PhoneColumns = extractCustomWidgetPropertyString(ctx, w, "phoneItems")
@@ -483,6 +489,9 @@ func parseRawWidget(ctx *ExecContext, w map[string]any, parentEntityContext ...s
483489
return extractGalleryContent(ctx, w, widget.EntityContext)
484490
})
485491
widget.FilterWidgets = extractGalleryFilters(ctx, w)
492+
// The gallery's own click action — written from `OnClick:` since the
493+
// definition maps it; read back by source like DataGrid2's.
494+
widget.OnClick = renderClientActionMDL(ctx, customWidgetActionForSource(ctx, w, "OnClick"))
486495
}
487496
// For filter widgets, extract filter attributes and expression
488497
if widget.RenderMode == "textfilter" || widget.RenderMode == "numberfilter" || widget.RenderMode == "dropdownfilter" || widget.RenderMode == "datefilter" {

‎mdl/executor/cmd_pages_describe_pluggable.go‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,29 @@ func extractGallerySelection(ctx *ExecContext, w map[string]any) string {
856856
return ""
857857
}
858858

859+
// customWidgetPropertySelection returns the Selection stored on the named
860+
// CustomWidget property ("None", "Single", "Multiple"), or "" when the widget
861+
// has no such property. Every property value carries a Selection field, None on
862+
// all but the selection property itself, so it must be looked up by key.
863+
func customWidgetPropertySelection(w map[string]any, propertyKey string) string {
864+
obj, ok := w["Object"].(map[string]any)
865+
if !ok {
866+
return ""
867+
}
868+
propTypeKeyMap := buildPropertyTypeKeyMap(w, true)
869+
for _, prop := range getBsonArrayElements(obj["Properties"]) {
870+
propMap, ok := prop.(map[string]any)
871+
if !ok || propTypeKeyMap[extractBinaryID(propMap["TypePointer"])] != propertyKey {
872+
continue
873+
}
874+
if value, ok := propMap["Value"].(map[string]any); ok {
875+
sel, _ := value["Selection"].(string)
876+
return sel
877+
}
878+
}
879+
return ""
880+
}
881+
859882
// extractFilterAttributes extracts the filter attributes from a TextFilter/NumberFilter widget.
860883
func extractFilterAttributes(ctx *ExecContext, w map[string]any) []string {
861884
// Use the generic property extraction helper
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package executor
4+
5+
import (
6+
"fmt"
7+
"strings"
8+
9+
"github.com/mendixlabs/mxcli/mdl/ast"
10+
"github.com/mendixlabs/mxcli/mdl/linter"
11+
)
12+
13+
// validateGalleryOnClickSelection (MDL-WIDGET35) reports a Gallery whose item
14+
// click action sits beside an item selection.
15+
//
16+
// A Gallery triggers its `onClick` on a single click (the definition fixes
17+
// onClickTrigger to "single"), and a single click on an item is also what
18+
// selects it. Mendix refuses that combination: with `OnClick:` and the
19+
// definition's default `Selection: Single`, exec wrote the page and mx check
20+
// (Mendix 11.14.0) reported "The item click action is ambiguous. Change "On
21+
// click trigger" to "Double click" or set "Selection" to "None"." MDL has no
22+
// spelling for the trigger, so the remedy is `selection: none`, which checks
23+
// clean.
24+
func validateGalleryOnClickSelection(w *ast.WidgetV3, locationPrefix string) []linter.Violation {
25+
if w == nil || !strings.EqualFold(w.Type, "gallery") || w.GetAction() == nil {
26+
return nil
27+
}
28+
selection := "Single" // the Gallery definition's default for itemSelection
29+
if v, ok := lookupPropCI(w, "Selection"); ok {
30+
selection = strings.TrimSpace(fmt.Sprint(v))
31+
}
32+
if strings.EqualFold(selection, "none") {
33+
return nil
34+
}
35+
return []linter.Violation{{
36+
RuleID: "MDL-WIDGET35",
37+
Severity: linter.SeverityError,
38+
Message: fmt.Sprintf(
39+
"%s: gallery `%s` has an OnClick action and Selection %s — a single click on an item would both select it and run the action, which Mendix rejects as ambiguous",
40+
locationPrefix, w.Name, selection),
41+
Suggestion: "Add `selection: none` to the gallery, or drop OnClick and put the action on a clickable `container` inside the template",
42+
}}
43+
}

0 commit comments

Comments
 (0)