Skip to content

Commit 57db4ad

Browse files
committed
feat(pages): describe, create and alter button confirmation dialogs
A microflow or nanoflow button's "are you sure?" dialog (Forms$ConfirmationInfo) was invisible to DESCRIBE PAGE, so describe -> exec silently removed it. Buttons now take Confirmation / ConfirmProceed / ConfirmCancel (captions default to Proceed / Cancel and are omitted by describe when default), in CREATE PAGE and ALTER PAGE SET. The dialog lives on the action (MicroflowSettings.ConfirmationInfo, or CallNanoflowClientAction.ConfirmationInfo), so any other action, a caption without a question, or a non-button widget is refused as MDL-WIDGET32 by check and exec alike. ALTER edits only the authoring-language translation; Confirmation = '' removes the dialog. The MCP backend refuses rather than drops it. Verified on Mendix 11.12.2: describe -> exec reports Unchanged, round trip is byte-identical, mx check 0 errors. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 57259e8 commit 57db4ad

22 files changed

Lines changed: 999 additions & 6 deletions

‎.claude/skills/mendix/alter-page/SKILL.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ set caption = 'New Caption' on widgetName
109109
-- Multiple properties
110110
set (caption = 'Save & Close', buttonstyle = success) on btnSave
111111

112+
-- Confirmation dialog on a button with a microflow/nanoflow action.
113+
-- Captions default to Proceed / Cancel; only the authoring-language text
114+
-- changes, other translations are kept. Confirmation = '' removes it.
115+
set (Confirmation = 'Delete all archived orders?', ConfirmProceed = 'Purge') on btnPurge
116+
set Confirmation = '' on btnPurge
117+
112118
-- Page-level property (no ON clause). Page-level property names are
113119
-- case-sensitive and must match the Mendix property exactly.
114120
set Title = 'New Page Title'

‎.claude/skills/mendix/create-page/SKILL.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,19 @@ image imgRemote (ImageType: imageUrl, ImageUrl: 'https://example.com/logo.svg')
383383
image imgIcon (ImageType: icon)
384384
```
385385

386+
### Asking before a microflow or nanoflow runs
387+
388+
A button whose action is `microflow` or `nanoflow` can show Mendix's confirmation dialog:
389+
390+
```sql
391+
actionbutton btnPurge (Caption: 'Purge', Action: microflow Sales.ACT_Purge,
392+
Confirmation: 'Delete all archived orders?', ConfirmProceed: 'Purge', ConfirmCancel: 'Keep')
393+
```
394+
395+
- `ConfirmProceed` / `ConfirmCancel` default to `Proceed` / `Cancel`; `describe` omits them when they match.
396+
- The dialog is stored on the **action** (`Forms$ConfirmationInfo` under the microflow settings, or on the nanoflow action), so any other action (`delete`, `save_changes`, `show_page`, …) with `Confirmation:` is refused as **MDL-WIDGET32** — Mendix has no slot for it there. A caption without a question is refused too.
397+
- Changing the button's `Action:` later with `alter page … set Action = …` builds a fresh action, so re-state `Confirmation` in the same `set`.
398+
386399
### A button's icon is one of three elements
387400

388401
Mendix stores **three different icon elements**, and the keyword picks which:

‎cmd/mxcli/syntax/features_page.go‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,10 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) {
258258
"button style", "primary", "danger", "success",
259259
"icon", "linkbutton", "link button",
260260
"nothing", "no action", "inert", "dead button",
261+
"confirmation", "confirm", "are you sure", "dialog",
261262
},
262-
Syntax: "Action: NOTHING -- deliberately no action (Forms$NoAction)\nAction: SAVE_CHANGES\nAction: SAVE_CHANGES CLOSE_PAGE -- save, then close the pop-up\nAction: CANCEL_CHANGES\nAction: CANCEL_CHANGES CLOSE_PAGE\nAction: CLOSE_PAGE\nAction: DELETE\nAction: DELETE CLOSE_PAGE\nAction: DELETE_OBJECT\nAction: NANOFLOW Module.NF\nAction: NANOFLOW Module.NF(Param: $val)\nAction: OPEN_LINK 'https://example.com'\nAction: SIGN_OUT\nAction: COMPLETE_TASK 'OutcomeName'\nAction: SHOW_PAGE Module.Page\nAction: SHOW_PAGE Module.Page(Param: $currentObject)\nAction: MICROFLOW Module.MF\nAction: MICROFLOW Module.MF(Param: $val)\nAction: CREATE_OBJECT Module.Entity THEN SHOW_PAGE Module.Page\n\nThe list above is exhaustive. Anything else in an action slot is an\nERROR (MDL-WIDGET28), including a real keyword short its argument --\n`Action: OPEN_LINK` without a URL, `Action: SHOW_PAGE` without a page.\nSuch a widget used to be written with NO action at all and rendered as a\ndead control, with check, exec and mxbuild all clean, because a\nno-action widget is legal Mendix (mendixlabs/mxcli#1062). Write NOTHING\nwhen a control really is meant to be inert.\n\nThe same forms serve `OnClick:` (an alias of `Action:`) and `OnChange:`.\n\nA microflow or nanoflow action is a CALL: it needs an argument for every\nparameter the flow declares, or Mendix rejects the page with CE1571. The\nargument list is the same on every widget that takes an action -- a\nCONTAINER (which is clickable) as much as an ACTIONBUTTON. An enclosing\ndata container of the right type supplies it without an argument; a data\ngrid's CONTROL BAR does not, because it is not row-scoped -- pass the\ngrid's selection there (`$dgOrders`).\n\nA SHOW_PAGE argument must be the enclosing widget's context object --\neither $currentObject or the name of the variable the enclosing data\nwidget is bound to. Mendix infers it from that widget, so naming any\nother variable is refused (MDL-PAGEARG01); call a microflow instead.\nOutside any data widget there is no context object to infer, so such a\nbutton takes NO argument at all -- not a page parameter, not\n$currentObject, not a literal. mxcli used to drop it in silence and\nmxbuild then reported CE1571 per parameter of the target page\n(mendixlabs/mxcli#1029). Route that navigation through a microflow.\n\nOPEN_LINK takes a static web address and stores it as a\nForms$StaticOrDynamicString. Mendix also supports a DYNAMIC address, read\nfrom an attribute at runtime; MDL cannot author that one, and DESCRIBE\nflags such a button rather than printing its address as a literal.\n\nButton styles: Default, Primary, Success, Info, Warning, Danger\n\nMendix has THREE icon elements and the keyword picks which one:\n\nIcon: 'Atlas_Core.Atlas_Filled.pencil' -- an icon collection\nIcon: image MyModule.Images.logo -- an IMAGE collection\nIcon: glyph 57377 -- a font code point\n\nThe bare form is the icon-collection icon and any collection in the\nproject works, third-party ones included. The image form points into a\ndifferent document, and is spelled the same way apart from the keyword\n-- write it without `image` and mxcli stores a custom-icon reference,\nwhich fails the build with CE1613 (mendixlabs/mxcli#1059).\n`mxcli check -p … --references` resolves each kind against its own\ncollection and names the remedy when the kind is wrong.\n\nA glyph carries a code and no name. Codes are sparse, and an undefined\none fails only at `mxbuild --target=deploy`, naming the PAGE rather\nthan the icon -- so MDL078 checks it against the font's own table.\nList them with `show glyphs`.\n\nA name may be quoted or bare; a hyphenated segment is double-quoted on\nits own: Atlas_Core.Atlas.\"align-center\".\n\nUse `linkbutton` instead of `actionbutton` for link render mode (same properties).",
263-
Example: "ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)\nACTIONBUTTON btnEdit (Caption: 'Edit',\n Action: SHOW_PAGE Module.EditPage(Item: $currentObject))\nLINKBUTTON btnDelete (Caption: 'Delete', Action: DELETE,\n Icon: 'Atlas_Core.Atlas_Filled.pencil')\n\n-- A clickable CONTAINER in a data grid's control bar, calling a nanoflow\n-- with the grid's selection as its argument.\nDATAGRID dgOrders (DataSource: DATABASE FROM Sales.Order, Selection: Single) {\n COLUMN colNr (Attribute: Number, Caption: 'Order #')\n CONTROLBAR cb {\n CONTAINER cShip (Class: 'command',\n Action: NANOFLOW Sales.ACT_Ship($Order = $dgOrders)) {\n ACTIONBUTTON btnShip (Caption: 'Ship')\n }\n }\n}",
263+
Syntax: "Action: NOTHING -- deliberately no action (Forms$NoAction)\nAction: SAVE_CHANGES\nAction: SAVE_CHANGES CLOSE_PAGE -- save, then close the pop-up\nAction: CANCEL_CHANGES\nAction: CANCEL_CHANGES CLOSE_PAGE\nAction: CLOSE_PAGE\nAction: DELETE\nAction: DELETE CLOSE_PAGE\nAction: DELETE_OBJECT\nAction: NANOFLOW Module.NF\nAction: NANOFLOW Module.NF(Param: $val)\nAction: OPEN_LINK 'https://example.com'\nAction: SIGN_OUT\nAction: COMPLETE_TASK 'OutcomeName'\nAction: SHOW_PAGE Module.Page\nAction: SHOW_PAGE Module.Page(Param: $currentObject)\nAction: MICROFLOW Module.MF\nAction: MICROFLOW Module.MF(Param: $val)\nAction: CREATE_OBJECT Module.Entity THEN SHOW_PAGE Module.Page\n\nThe list above is exhaustive. Anything else in an action slot is an\nERROR (MDL-WIDGET28), including a real keyword short its argument --\n`Action: OPEN_LINK` without a URL, `Action: SHOW_PAGE` without a page.\nSuch a widget used to be written with NO action at all and rendered as a\ndead control, with check, exec and mxbuild all clean, because a\nno-action widget is legal Mendix (mendixlabs/mxcli#1062). Write NOTHING\nwhen a control really is meant to be inert.\n\nThe same forms serve `OnClick:` (an alias of `Action:`) and `OnChange:`.\n\nA microflow or nanoflow action is a CALL: it needs an argument for every\nparameter the flow declares, or Mendix rejects the page with CE1571. The\nargument list is the same on every widget that takes an action -- a\nCONTAINER (which is clickable) as much as an ACTIONBUTTON. An enclosing\ndata container of the right type supplies it without an argument; a data\ngrid's CONTROL BAR does not, because it is not row-scoped -- pass the\ngrid's selection there (`$dgOrders`).\n\nA SHOW_PAGE argument must be the enclosing widget's context object --\neither $currentObject or the name of the variable the enclosing data\nwidget is bound to. Mendix infers it from that widget, so naming any\nother variable is refused (MDL-PAGEARG01); call a microflow instead.\nOutside any data widget there is no context object to infer, so such a\nbutton takes NO argument at all -- not a page parameter, not\n$currentObject, not a literal. mxcli used to drop it in silence and\nmxbuild then reported CE1571 per parameter of the target page\n(mendixlabs/mxcli#1029). Route that navigation through a microflow.\n\nOPEN_LINK takes a static web address and stores it as a\nForms$StaticOrDynamicString. Mendix also supports a DYNAMIC address, read\nfrom an attribute at runtime; MDL cannot author that one, and DESCRIBE\nflags such a button rather than printing its address as a literal.\n\nButton styles: Default, Primary, Success, Info, Warning, Danger\n\nMendix has THREE icon elements and the keyword picks which one:\n\nIcon: 'Atlas_Core.Atlas_Filled.pencil' -- an icon collection\nIcon: image MyModule.Images.logo -- an IMAGE collection\nIcon: glyph 57377 -- a font code point\n\nThe bare form is the icon-collection icon and any collection in the\nproject works, third-party ones included. The image form points into a\ndifferent document, and is spelled the same way apart from the keyword\n-- write it without `image` and mxcli stores a custom-icon reference,\nwhich fails the build with CE1613 (mendixlabs/mxcli#1059).\n`mxcli check -p … --references` resolves each kind against its own\ncollection and names the remedy when the kind is wrong.\n\nA glyph carries a code and no name. Codes are sparse, and an undefined\none fails only at `mxbuild --target=deploy`, naming the PAGE rather\nthan the icon -- so MDL078 checks it against the font's own table.\nList them with `show glyphs`.\n\nA name may be quoted or bare; a hyphenated segment is double-quoted on\nits own: Atlas_Core.Atlas.\"align-center\".\n\nA MICROFLOW or NANOFLOW action can ask first. Confirmation: is the\nquestion; ConfirmProceed: / ConfirmCancel: are the dialog's button\ncaptions and default to Proceed / Cancel. Mendix stores the dialog on the\naction, so any other action with Confirmation: is refused (MDL-WIDGET32),\nas is a caption without a question. ALTER PAGE sets the same three\nproperties; `set Confirmation = '' on btn` removes the dialog.\n\nUse `linkbutton` instead of `actionbutton` for link render mode (same properties).",
264+
Example: "ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)\nACTIONBUTTON btnEdit (Caption: 'Edit',\n Action: SHOW_PAGE Module.EditPage(Item: $currentObject))\nLINKBUTTON btnDelete (Caption: 'Delete', Action: DELETE,\n Icon: 'Atlas_Core.Atlas_Filled.pencil')\nACTIONBUTTON btnPurge (Caption: 'Purge', Action: MICROFLOW Module.ACT_Purge,\n Confirmation: 'Delete all archived orders?', ConfirmProceed: 'Purge')\n\n-- A clickable CONTAINER in a data grid's control bar, calling a nanoflow\n-- with the grid's selection as its argument.\nDATAGRID dgOrders (DataSource: DATABASE FROM Sales.Order, Selection: Single) {\n COLUMN colNr (Attribute: Number, Caption: 'Order #')\n CONTROLBAR cb {\n CONTAINER cShip (Class: 'command',\n Action: NANOFLOW Sales.ACT_Ship($Order = $dgOrders)) {\n ACTIONBUTTON btnShip (Caption: 'Ship')\n }\n }\n}",
264265
SeeAlso: []string{"page.widgets"},
265266
})
266267

‎docs/01-project/MDL_QUICK_REFERENCE.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,7 @@ MDL uses explicit property declarations for pages:
14381438
| Variable binding | `datasource: $Var` | `dataview dv (datasource: $Product) { ... }` |
14391439
| 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** |
14401440
| 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 |
1441+
| Confirmation dialog | `confirmation: 'q' [, confirmproceed: 'c', confirmcancel: 'c']` | `actionbutton btn (caption: 'Purge', action: microflow M.ACT_Purge, confirmation: 'Delete all archived orders?', confirmproceed: 'Purge')` — asks before the call. Stored on the **action** (`Forms$ConfirmationInfo`), so only a `microflow` / `nanoflow` action takes it; anything else is **MDL-WIDGET32**. Captions default to `Proceed` / `Cancel` and `describe` omits them when they match |
14411442
| Microflow action | `action: microflow Name(Param: val)` | `action: microflow Mod.ACT_Process(Order: $Order)` |
14421443
| Button icon | `icon: 'Module.IconCollection.IconName'` | `linkbutton btn (caption: 'Edit', action: nothing, icon: 'Atlas_Core.Atlas_Filled.pencil')` — the **icon-collection** icon; MxBuild rejects an unknown name (CE1613) |
14431444
| Image icon | `icon: image Module.ImageCollection.Name` | `actionbutton btn (caption: 'Logo', action: nothing, icon: image MyMod.Images.logo)` — an **image** collection is a different document from an icon collection, and the names are spelled the same, so the keyword is what separates them. Written without `image` it is stored as a custom-icon reference and the build fails **CE1613** |
@@ -1656,7 +1657,7 @@ Modify an existing page or snippet's widget tree in-place without full `create o
16561657
| Set layout | `set layout = Module.LayoutName` | Change page layout, auto-maps placeholders |
16571658
| Set layout + map | `set layout = Module.Layout map (Old as New)` | Explicit placeholder mapping |
16581659

1659-
**Supported SET properties:** Caption, Label, ButtonStyle, Class, Style, DynamicClasses, Editable, Visible, Name, Title (page-level), Documentation (page-level), Layout (page-level), PopupWidth / PopupHeight / PopupResizable (page-level), and quoted pluggable widget properties.
1660+
**Supported SET properties:** Caption, Label, ButtonStyle, Confirmation / ConfirmProceed / ConfirmCancel (buttons with a microflow or nanoflow action; `Confirmation = ''` removes the dialog), Class, Style, DynamicClasses, Editable, Visible, Name, Title (page-level), Documentation (page-level), Layout (page-level), PopupWidth / PopupHeight / PopupResizable (page-level), and quoted pluggable widget properties.
16601661

16611662
**Example:**
16621663
```sql
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
-- ============================================================================
2+
-- Button Confirmation Examples - Confirmation / ConfirmProceed / ConfirmCancel
3+
-- ============================================================================
4+
--
5+
-- A button that calls a microflow or nanoflow can ask "are you sure?" first.
6+
-- Mendix stores the dialog on the ACTION (Forms$ConfirmationInfo: Question,
7+
-- ProceedButtonCaption, CancelButtonCaption), so only those two action kinds
8+
-- take it; anything else is refused as MDL-WIDGET32.
9+
--
10+
-- The captions default to Proceed / Cancel. DESCRIBE PAGE omits a caption
11+
-- equal to its default, so describe -> exec -> describe is stable.
12+
--
13+
-- Run: mxcli exec mdl-examples/doctype-tests/button-confirmation.mdl -p app.mpr
14+
-- ============================================================================
15+
16+
create module ConfirmTest;
17+
18+
create microflow ConfirmTest.ACT_Purge ()
19+
begin
20+
log info 'purge';
21+
end;
22+
/
23+
24+
create nanoflow ConfirmTest.NAV_Reset ()
25+
begin
26+
log info 'reset';
27+
end;
28+
/
29+
30+
-- ----------------------------------------------------------------------------
31+
-- CREATE PAGE: default captions, custom captions, nanoflow action, link button
32+
-- ----------------------------------------------------------------------------
33+
34+
create or replace page ConfirmTest.Maintenance (
35+
Title: 'Maintenance',
36+
Layout: Atlas_Core.Atlas_Default
37+
) {
38+
container cMain {
39+
actionbutton btnPurge (Caption: 'Purge', Action: microflow ConfirmTest.ACT_Purge,
40+
Confirmation: 'Delete all archived orders?', ButtonStyle: Danger)
41+
actionbutton btnPurgeCustom (Caption: 'Purge (custom)', Action: microflow ConfirmTest.ACT_Purge,
42+
Confirmation: 'Really purge?', ConfirmProceed: 'Purge', ConfirmCancel: 'Keep')
43+
actionbutton btnReset (Caption: 'Reset', Action: nanoflow ConfirmTest.NAV_Reset,
44+
Confirmation: 'Reset the filters?')
45+
linkbutton lnkPurge (Caption: 'Purge', Action: microflow ConfirmTest.ACT_Purge,
46+
Confirmation: 'Delete all archived orders?', ConfirmProceed: 'Yes')
47+
actionbutton btnPlain (Caption: 'No dialog', Action: microflow ConfirmTest.ACT_Purge)
48+
}
49+
}
50+
51+
describe page ConfirmTest.Maintenance;
52+
53+
-- ----------------------------------------------------------------------------
54+
-- ALTER PAGE: add, edit, remove
55+
-- ----------------------------------------------------------------------------
56+
57+
alter page ConfirmTest.Maintenance {
58+
-- add a dialog to a button that had none
59+
set (Confirmation = 'Run it now?', ConfirmCancel = 'Not now') on btnPlain;
60+
-- edit one caption; the question and the other caption are kept
61+
set ConfirmProceed = 'Go' on btnPurgeCustom;
62+
-- remove the dialog
63+
set Confirmation = '' on btnReset;
64+
};
65+
66+
describe page ConfirmTest.Maintenance;
67+
68+
-- ----------------------------------------------------------------------------
69+
-- Refused (MDL-WIDGET32) — uncomment to see the errors:
70+
--
71+
-- actionbutton b1 (Caption: 'Save', Action: save_changes, Confirmation: 'Sure?')
72+
-- -- the dialog lives on a microflow/nanoflow action; save has no slot
73+
-- actionbutton b2 (Caption: 'Go', Action: microflow ConfirmTest.ACT_Purge, ConfirmProceed: 'Yes')
74+
-- -- a caption without a question
75+
-- ----------------------------------------------------------------------------

‎mdl/backend/mcp/page_widgets.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,10 @@ func mapClientAction(a pages.ClientAction) (map[string]any, error) {
391391
case nil, *pages.NoClientAction:
392392
return map[string]any{"$Type": "Pages$NoClientAction"}, nil
393393
case *pages.MicroflowClientAction:
394+
// Refuse rather than drop: the dialog is not mapped over MCP yet.
395+
if act.Confirmation != nil {
396+
return nil, fmt.Errorf("a microflow action's confirmation dialog is not yet supported over MCP")
397+
}
394398
return map[string]any{
395399
"$Type": "Pages$MicroflowClientAction",
396400
"microflow": act.MicroflowName,

‎mdl/backend/mcp/widget.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,9 @@ func customWidgetClientAction(a pages.ClientAction) (map[string]any, error) {
418418
if len(act.ParameterMappings) > 0 {
419419
return nil, fmt.Errorf("microflow action parameter mappings are not yet supported over MCP")
420420
}
421+
if act.Confirmation != nil {
422+
return nil, fmt.Errorf("a microflow action's confirmation dialog is not yet supported over MCP")
423+
}
421424
return map[string]any{
422425
"$Type": "Pages$MicroflowClientAction",
423426
"microflowSettings": map[string]any{

0 commit comments

Comments
 (0)