Skip to content

Commit a308118

Browse files
committed
feat(pages): VisibleForRoles — role-based widget visibility
Studio Pro's "Visible: based on module roles" (ConditionalVisibilitySettings. ModuleRoles) had no MDL: the key was MDL-WIDGET07 and the list was always written empty. Add VisibleForRoles: [Module.Role, ...] on built-in widgets, written in Studio Pro's shape (pinned against WorkflowCommons. WorkflowAttachment_New), combined with Visible: [expr], and described back. Field run: ProcureFlow (Mendix 11.14.0) scratch copy — Delete/Edit buttons and a container gated to Procurement.Admin / ProcurementOfficer: stored ModuleRoles [1, ...], describe round-trips, mx check 0 errors; control with Procurement.NoSuchRole -> mx check CE1613. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5dJ4raEjbm8bFiKeYtchS
1 parent 9509176 commit a308118

17 files changed

Lines changed: 371 additions & 1 deletion

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,14 @@ textbox txtEmail (label: 'Email', attribute: Email,
280280
textbox txtReadOnly (label: 'Read Only', attribute: Name, editable: Never)
281281
textbox txtHidden (label: 'Hidden', attribute: Name, visible: false)
282282

283+
-- Visible to some module roles only (Studio Pro's "based on module roles").
284+
-- Any built-in widget; combines with visible: — both must hold.
285+
actionbutton btnDelete (caption: 'Delete', action: microflow MES.ACT_Delete(Item: $currentObject),
286+
visibleforroles: [MES.Administrator])
287+
container ctnAdmin (visibleforroles: [MES.Administrator, MES.Supervisor], visible: [IsActive]) {
288+
dynamictext t (content: 'Admin tools')
289+
}
290+
283291
-- A quoted-string expression is also accepted (CREATE and ALTER). Unlike the
284292
-- bracket form, it is NOT auto-rooted — write $currentObject/ yourself.
285293
dynamictext ovChip (content: 'chip', visible: '$currentObject/Name != empty')

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,24 @@ actionbutton btnSubmit (
849849
)
850850
```
851851

852+
To show a button to some **module roles** only — Studio Pro's "Visible: based on
853+
module roles" — list them with `visibleforroles:`. It works on every built-in
854+
widget (buttons, containers, inputs, text, data views) and combines with
855+
`visible:`: the widget shows when the user has one of the roles **and** the
856+
expression holds.
857+
858+
```sql
859+
actionbutton btnDelete (
860+
caption: 'Delete', action: microflow Mod.ACT_Delete(Item: $currentObject),
861+
visibleforroles: [Mod.Admin, Mod.Manager]
862+
)
863+
```
864+
865+
Write each role qualified, `Module.Role`; an unknown one is CE1613 at `mx check`.
866+
Pluggable widgets (Data grid 2, Gallery, …) take no conditional visibility from
867+
mxcli — wrap one in a `container` and put the roles there. Hiding a button is not
868+
security: grant execute on the microflow to the same roles.
869+
852870
### Inputs in a List View Need `editable: true` on the List View
853871

854872
A list view has an `Editable` of its own, default **false** (Mendix's default),

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- **`VisibleForRoles: [Module.Role, …]` — Studio Pro's "Visible: based on module roles"** on any built-in widget (buttons, containers, inputs, …). There was no MDL for it: the key was MDL-WIDGET07 "silently dropped", so hiding a Delete button or a menu from a role meant Studio Pro. It is stored in the widget's `ConditionalVisibilitySettings.ModuleRoles`, the shape Studio Pro writes (WorkflowCommons.WorkflowAttachment_New), combines with `Visible: [expression]` as it does there, and `describe` prints it back. An unknown role is reported by mx check (CE1613), as for any by-name reference. Pluggable widgets still get no conditional visibility from mxcli; wrap one in a container.
12+
913
### Changed
1014

1115
- **`DynamicClasses` and a column's `DynamicCellClass` are written as Mendix expressions** (mendixlabs/mxcli#750) — the expression is written as-is, so the doubled-quote spelling is gone: `dynamicclasses: if $currentObject/Featured then 'is-featured' else ''`, and `dynamicclasses: 'is-featured'` is the string — the class `is-featured`. The same rule as the OData client's credentials. `create page`, `alter page … set` and `describe` all use it, and a describe → exec round trip stores identical values (measured on a Mendix 11.14.0 project). **Migrating a script:** the old spelling, the expression's text in quotes (`'if … then ''a'' else '''''`), still parses but would now store that text as a class name, so `check` and `exec` refuse it as **MDL-WIDGET33** and give the unquoted expression. An expression in any other widget property is an error rather than an empty value; a pluggable property whose schema kind is Expression (a column's `Visible`, for one) keeps the quoted form until a following change.

‎cmd/mxcli/syntax/features_page.go‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,10 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) {
144144
"-- container the grid declares — it used to be dropped on write with no\n" +
145145
"-- diagnostic, and is now refused (MDL-WIDGET30).\n\n" +
146146
"-- Inputs\nTEXTBOX name (Label: 'L', Attribute: Attr)\nTEXTAREA | DATEPICKER | COMBOBOX | CHECKBOX | RADIOBUTTONS\n\n" +
147-
"-- Actions\nACTIONBUTTON name (Caption: 'C', Action: SAVE_CHANGES, ButtonStyle: Primary)\nLINKBUTTON name (Caption: 'C', Action: ...)\n\n" +
147+
"-- Actions\nACTIONBUTTON name (Caption: 'C', Action: SAVE_CHANGES, ButtonStyle: Primary)\nLINKBUTTON name (Caption: 'C', Action: ...)\n" +
148+
"-- Studio Pro's \"Visible: based on module roles\" on any built-in widget —\n" +
149+
"-- combines with a Visible: [expression] exactly as in Studio Pro:\n" +
150+
"ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE_OBJECT, VisibleForRoles: [MyModule.Admin])\n\n" +
148151
"-- Display\nDYNAMICTEXT name (Content: 'Hello, {1}!', ContentParams: [{1} = Name])\nTITLE name (Content: 'Heading')\nIMAGE name (Image: 'Module.Collection.ImageName')\nIMAGE name (ImageType: imageUrl, ImageUrl: 'https://…')\n" +
149152
"-- IMAGE needs a source. Its default, `ImageType: image`, shows an entry from an\n" +
150153
"-- image collection, named as three parts: Module.Collection.ImageName.\n" +

‎docs-site/src/language/pages.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,22 @@ mxcli writes one condition per value of the attribute, as Studio Pro does, so a
114114
value not listed hides the widget. `describe page` emits this form for widgets
115115
set up that way in Studio Pro.
116116

117+
Studio Pro's **"based on module roles"** form shows a widget to the listed
118+
module roles only:
119+
120+
```sql
121+
ACTIONBUTTON btnDelete (Caption: 'Delete', Action: MICROFLOW Sales.ACT_Delete(Order: $Order),
122+
VisibleForRoles: [Sales.Administrator, Sales.Manager])
123+
```
124+
125+
Every built-in widget takes it. It combines with an expression the way Studio
126+
Pro combines them: with `Visible: [IsOpen]` added as well, the widget shows only
127+
to those roles and only while the expression holds. Write each role qualified,
128+
`Module.Role`; `mx check` reports an unknown one (CE1613). Pluggable widgets get
129+
no conditional visibility from mxcli, so wrap one in a `CONTAINER` and put the
130+
roles there. Hiding a widget is not access control: grant execute on the
131+
microflow it calls to the same roles.
132+
117133
### Conditional Editability
118134

119135
Input widgets can be conditionally editable:

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ All input widgets share common properties:
234234
| `Attribute` | Entity attribute to bind to | `Attribute: Name` |
235235
| `Editable` | Editability mode | `Editable: ReadOnly` |
236236
| `Visible` | Visibility expression | `Visible: '$showField'` |
237+
| `VisibleForRoles` | Module roles that see the widget (with `Visible`, both must hold) | `VisibleForRoles: [Sales.Administrator]` |
237238

238239
### TEXTBOX
239240

@@ -569,6 +570,7 @@ These properties are shared across many widget types:
569570
| `DynamicClasses` | Runtime-computed CSS classes (expression; stacks on `Class`) | `DynamicClasses: if $currentObject/IsActive then 'is-active' else ''` |
570571
| `DesignProperties` | Atlas design properties | `DesignProperties: ['Spacing top': 'Large', 'Full width': ON]` |
571572
| `Visible` | Visibility expression | `Visible: '$showSection'` |
573+
| `VisibleForRoles` | Module roles that see the widget (with `Visible`, both must hold) | `VisibleForRoles: [Sales.Administrator]` |
572574
| `Editable` | Editability mode | `Editable: ReadOnly` |
573575

574576
## See Also

‎docs-site/src/reference/page/create-page.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ These properties are available on most widget types:
193193
| `DynamicClasses` | Runtime-computed CSS classes (expression; stacks on `Class`) | `DynamicClasses: if $currentObject/IsActive then 'is-active' else ''` |
194194
| `Editable` | Edit control | `Editable: NEVER` or `Editable: ALWAYS` |
195195
| `Visible` | Visibility expression | `Visible: '$showField'` |
196+
| `VisibleForRoles` | Module roles that see the widget (with `Visible`, both must hold) | `VisibleForRoles: [Sales.Administrator]` |
196197
| `DesignProperties` | Atlas design properties | `DesignProperties: ['Spacing top': 'Large']` |
197198

198199
## Parameters

‎docs/01-project/MDL_QUICK_REFERENCE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,6 +1554,7 @@ create page MyModule.Customer_Edit
15541554
| TabletWidth | `column col (tabletwidth: 6)` | 1-12 or AutoFill (default: auto) |
15551555
| PhoneWidth | `column col (phonewidth: 12)` | 1-12 or AutoFill (default: auto) |
15561556
| Visible | `textbox txt (visible: [IsActive])` | Conditional visibility (XPath expression) |
1557+
| VisibleForRoles | `actionbutton btn (visibleforroles: [Mod.Admin, Mod.Editor])` | Studio Pro's "Visible: based on module roles" — any built-in widget; combines with `visible:` (both must hold). Qualified `Module.Role` names; an unknown role is CE1613 at mx check. Not on pluggable widgets — wrap one in a container |
15571558
| Editable | `textbox txt (editable: [status != 'Closed'])` | Conditional editability (XPath expression) |
15581559
| Image | `staticimage img (Image: 'Mod.Images.logo')` | Image-collection entry, `Module.Collection.Image`. Omitted → CE0436 "No image selected." |
15591560
| DataSource (dynamicimage) | `dynamicimage img (DataSource: database from Mod.Photo)` | The entity holding the image. Omitted → CE0489 "Select an entity for the data source of this dynamic image." |
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
-- ============================================================================
2+
-- Role-based widget visibility — Studio Pro's "Visible: based on module roles"
3+
-- ============================================================================
4+
--
5+
-- A widget can be shown to some module roles only:
6+
--
7+
-- VisibleForRoles: [Module.Role, Module.OtherRole]
8+
--
9+
-- It is stored in the widget's ConditionalVisibilitySettings.ModuleRoles, the
10+
-- same element that holds a `Visible: [expression]` condition, and the two
11+
-- combine as they do in Studio Pro: the widget shows when the user has one of
12+
-- the roles AND the expression holds. `describe` prints the list back.
13+
--
14+
-- Works on the built-in widgets — buttons, containers, inputs, text, data
15+
-- views, … Each name is a by-name reference written Module.Role; an unknown
16+
-- role is reported by mx check (CE1613), like any other by-name reference.
17+
-- Pluggable widgets (Data grid 2, Gallery, …) get no conditional visibility
18+
-- from mxcli: wrap one in a container and put the roles on the container.
19+
--
20+
-- Hiding a widget is not security: a hidden button's microflow can still be
21+
-- called. Grant execute on the microflow to the same roles.
22+
--
23+
-- ============================================================================
24+
25+
create module RoleVisTest;
26+
27+
create module role RoleVisTest.Admin;
28+
create module role RoleVisTest.Editor;
29+
create module role RoleVisTest.Viewer;
30+
31+
create persistent entity RoleVisTest.Document (
32+
Title: string(200) not null,
33+
IsLocked: boolean default false
34+
);
35+
36+
/**
37+
* Deletes one document.
38+
*
39+
* @param $Document The document to delete
40+
*/
41+
create microflow RoleVisTest.ACT_Document_Delete (
42+
$Document: RoleVisTest.Document
43+
)
44+
begin
45+
delete $Document;
46+
end;
47+
/
48+
49+
grant execute on microflow RoleVisTest.ACT_Document_Delete to RoleVisTest.Admin;
50+
51+
/**
52+
* - btnEdit: shown to editors and admins.
53+
* - btnDelete: shown to admins only.
54+
* - btnUnlock: roles AND an expression — shown to admins, and only while the
55+
* document is locked.
56+
* - txtTitle / cntAdmin: any built-in widget takes the property, a container
57+
* included, which also hides everything inside it.
58+
*
59+
* @param $Document The document shown on the page
60+
*/
61+
create page RoleVisTest.Document_Detail
62+
(
63+
params: { $Document: RoleVisTest.Document },
64+
title: 'Document',
65+
layout: Atlas_Core.PopupLayout
66+
)
67+
{
68+
layoutgrid lgDocument {
69+
row row1 {
70+
column col1 (desktopwidth: autofill) {
71+
dataview dvDocument (datasource: $Document) {
72+
textbox txtTitle (
73+
label: 'Title',
74+
attribute: Title,
75+
visibleforroles: [RoleVisTest.Admin, RoleVisTest.Editor]
76+
)
77+
container cntAdmin (class: 'card', visibleforroles: [RoleVisTest.Admin]) {
78+
dynamictext txtAdminNote (content: 'Visible to administrators only')
79+
}
80+
footer footer1 {
81+
actionbutton btnEdit (
82+
caption: 'Save',
83+
action: save_changes,
84+
visibleforroles: [RoleVisTest.Admin, RoleVisTest.Editor]
85+
)
86+
actionbutton btnDelete (
87+
caption: 'Delete',
88+
action: microflow RoleVisTest.ACT_Document_Delete(Document: $Document),
89+
buttonstyle: danger,
90+
visibleforroles: [RoleVisTest.Admin]
91+
)
92+
actionbutton btnUnlock (
93+
caption: 'Unlock',
94+
action: save_changes,
95+
visible: [IsLocked],
96+
visibleforroles: [RoleVisTest.Admin]
97+
)
98+
actionbutton btnClose (caption: 'Close', action: close_page)
99+
}
100+
}
101+
}
102+
}
103+
}
104+
}
105+
106+
-- Prints the VisibleForRoles lists back.
107+
describe page RoleVisTest.Document_Detail;

‎mdl/backend/modelsdk/widget_write.go‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,6 +1020,15 @@ func conditionalVisibilityToGen(cvs *pages.ConditionalVisibilitySettings) elemen
10201020
assignID(g)
10211021
g.SetExpression(cvs.Expression)
10221022
g.SetIgnoreSecurity(false)
1023+
// "Visible: based on module roles" — by-name Module.Role references, the
1024+
// marker-1 list Studio Pro writes (WorkflowCommons.WorkflowAttachment_New).
1025+
if len(cvs.ModuleRoles) > 0 {
1026+
roles := make([]string, 0, len(cvs.ModuleRoles))
1027+
for _, r := range cvs.ModuleRoles {
1028+
roles = append(roles, string(r))
1029+
}
1030+
g.SetModuleRolesQualifiedNames(roles)
1031+
}
10231032
// "Visible: based on attribute value": the attribute plus one condition per
10241033
// value it can hold (ako/mxcli attribute-condition visibility).
10251034
if cvs.Attribute != "" {

0 commit comments

Comments
 (0)