Skip to content
Merged
152 changes: 85 additions & 67 deletions .claude/skills/mendix/alter-page/SKILL.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Changed

- **`alter page`, `alter snippet` and `alter layout` are the first document types on the generic ALTER** (ako/mxcli#712, ADR-0012) — one grammar, `alter <type> Module.Name { set (Key: value) on <target>; insert before|after|into <target> { … } replace <target> with { … } drop <target>; }`, whose target is resolved by the document type (a new `backend.AlterTargetResolver`, implemented by the page mutator on the modelsdk and `--mcp` backends). Properties are written as in `create`: `set (Caption: 'Save') on btnSave`, `set (Title: 'Edit')` for the page itself. A target is a widget name, `grid.Column` or `layoutContainer.top`; a quoted-caption target or `@n` is refused on a page, whose elements have names — by `check` too, as **MDL-ALTER01**. **Migrating a script:** nothing breaks — `set Key = value`, `set Key: value` without parentheses and `drop widget a` still run and build the identical change, and `check` / `exec` warn with **MDL-DEPR101**, **MDL-DEPR102** and **MDL-DEPR103** naming the rewrite.

- **`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.
- **An OData client's credentials and header values are written as Mendix expressions** (mendixlabs/mxcli#750) — `HttpUsername`, `HttpPassword`, `ClientCertificate` and every `headers (…)` value hold an expression, and MDL now writes it as-is: `HttpUsername: 'admin'` is the string `'admin'`, `@Module.Const` reads a constant, and `'Bearer ' + @Module.Token` concatenates. Before, a quoted value was the expression's *text*, so `'admin'` stored the identifier `admin` and a string needed `'''admin'''`. `describe` prints the stored expression as-is, so Studio Pro's `'abc'` now reads `HttpUsername: 'abc'`; measured against a Studio Pro-authored client, and a describe → exec round trip stores identical values. **Migrating a script:** `'''admin'''` becomes `'admin'`, and a quoted constant `'@Module.Const'` becomes `@Module.Const` — both old forms still parse but would now store something else, so `check` and `exec` refuse them as **MDL-ODATA07**. A compound expression in any other OData property (`Path: 'a' + 'b'`) is an error rather than an empty value. `ServiceUrl` is a constant reference, not an expression — see the next entry.
- **An OData client's `ServiceUrl` names a constant, like `ProxyHost`** (mendixlabs/mxcli#750) — Studio Pro picks the service URL as a constant and stores it as `@Module.Name`. `ServiceUrl: Module.Location` is now accepted alongside `@Module.Location` and `'@Module.Location'` (the bare name used to be refused as "not a constant reference"); all three store the same value, and `describe` prints the bare name, as it does for the proxy references. A literal URL is still refused (CE6825).
Expand Down
12 changes: 6 additions & 6 deletions cmd/mxcli/syntax/features_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,8 @@ 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 'Row size' = 'Small' ON lvOrders; -- an Atlas DESIGN property of that widget's\n -- type; quoted and case-sensitive.\n -- `show design properties for <type>` lists\n -- them. ON/OFF for a toggle, where OFF\n -- REMOVES the entry.\n -- A multi-select ('Hide on') or compound\n -- ('Spacing') one needs the inline\n -- DesignProperties: [...] form, because a\n -- SET assignment carries one value.\n SET Action = MICROFLOW Module.MF ON btnSave; -- any CREATE PAGE action form\n SET 'createFileAction' = MICROFLOW Module.MF ON fileUploader1;\n -- a pluggable widget's NAMED action slot,\n -- by the widget's own key; refused on a\n -- key that is not action-typed\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};\n\n-- The BULK form: one design property on every widget of a TYPE.\nALTER PAGES [IN Module]\n SET 'Compact' = ON, 'Striped' = ON\n WHERE WIDGETTYPE = datagrid -- the MDL keyword, which resolves to\n -- exactly one widget id. A full id in\n -- quotes works too. NOT a name: a widget\n -- name is unique only within its page.\n [DRY RUN]; -- run this FIRST. It reports the matches\n -- against a discardable copy and writes\n -- nothing.",
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};",
Syntax: "ALTER PAGE Module.Name { -- the generic ALTER: set / insert / replace / drop\n SET (property: value) ON widgetName; -- widget property names: any casing\n SET ('Row size': 'Small') ON lvOrders; -- an Atlas DESIGN property of that widget's\n -- type; quoted and case-sensitive.\n -- `show design properties for <type>` lists\n -- them. ON/OFF for a toggle, where OFF\n -- REMOVES the entry.\n -- A multi-select ('Hide on') or compound\n -- ('Spacing') one needs the inline\n -- DesignProperties: [...] form, because a\n -- SET assignment carries one value.\n SET (Action: MICROFLOW Module.MF) ON btnSave; -- any CREATE PAGE action form\n SET ('createFileAction': MICROFLOW Module.MF) ON fileUploader1;\n -- a pluggable widget's NAMED action slot,\n -- by the widget's own key; refused on a\n -- key that is not action-typed\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): no ON\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, PopupHeight: 480, PopupResizable: true); -- page-level pop-up\n INSERT AFTER widgetName { <widgets> };\n INSERT BEFORE widgetName { <widgets> };\n INSERT INTO containerName { <widgets> };\n DROP name1, name2;\n DROP TEMPLATE FOR Module.Specialization IN listViewName;\n REPLACE widgetName WITH { <widgets> };\n};\n-- A target is a widget name, or grid.Column. The old spellings `SET p = v`,\n-- `SET p: v` (no parentheses) and `DROP WIDGET a` still run and warn\n-- (MDL-DEPR101..103).\n\n-- The BULK form: one design property on every widget of a TYPE.\nALTER PAGES [IN Module]\n SET 'Compact' = ON, 'Striped' = ON\n WHERE WIDGETTYPE = datagrid -- the MDL keyword, which resolves to\n -- exactly one widget id. A full id in\n -- quotes works too. NOT a name: a widget\n -- name is unique only within its page.\n [DRY RUN]; -- run this FIRST. It reports the matches\n -- against a discardable copy and writes\n -- nothing.",
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 txtUnused;\n};",
SeeAlso: []string{"page.create", "page.show", "snippet.alter"},
})

Expand Down Expand Up @@ -423,8 +423,8 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) {
Keywords: []string{
"alter snippet", "modify snippet", "update snippet",
},
Syntax: "ALTER SNIPPET Module.Name {\n SET property = value ON widgetName;\n INSERT AFTER widgetName { <widgets> };\n INSERT BEFORE widgetName { <widgets> };\n INSERT INTO containerName { <widgets> };\n DROP WIDGET name1, name2;\n REPLACE widgetName WITH { <widgets> };\n};",
Example: "ALTER SNIPPET Module.NavSnippet {\n REPLACE navItem1 WITH {\n ACTIONBUTTON btnHome (Caption: 'Home', Action: SHOW_PAGE Module.HomePage)\n };\n DROP WIDGET txtOldField;\n INSERT AFTER txtName {\n TEXTBOX txtNewField (Label: 'New Field', Attribute: NewAttr)\n };\n};",
Syntax: "ALTER SNIPPET Module.Name {\n SET (property: value) ON widgetName;\n INSERT AFTER widgetName { <widgets> };\n INSERT BEFORE widgetName { <widgets> };\n INSERT INTO containerName { <widgets> };\n DROP name1, name2;\n REPLACE widgetName WITH { <widgets> };\n};",
Example: "ALTER SNIPPET Module.NavSnippet {\n REPLACE navItem1 WITH {\n ACTIONBUTTON btnHome (Caption: 'Home', Action: SHOW_PAGE Module.HomePage)\n };\n DROP txtOldField;\n INSERT AFTER txtName {\n TEXTBOX txtNewField (Label: 'New Field', Attribute: NewAttr)\n };\n};",
SeeAlso: []string{"snippet", "page.alter"},
})

Expand Down Expand Up @@ -559,8 +559,8 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) {
"ALTER LAYOUT Module.Name {\n" +
" INSERT INTO <scrollContainer>.<top|right|bottom|left|center> { <widgets> };\n" +
" INSERT BEFORE|AFTER <widgetName> { <widgets> };\n" +
" SET <property> = <value> ON <widgetName>;\n" +
" DROP WIDGET <name1>, <name2>;\n" +
" SET (<property>: <value>) ON <widgetName>;\n" +
" DROP <name1>, <name2>;\n" +
" REPLACE <widgetName> WITH { <widgets> };\n" +
"};\n\n" +
"-- Point one page at a different layout:\n" +
Expand Down
48 changes: 24 additions & 24 deletions docs-site/src/language/alter-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,28 @@ Change one or more properties on a widget identified by name:
```sql
-- Single property
ALTER PAGE Module.EditPage {
SET Caption = 'Save & Close' ON btnSave
SET (Caption: 'Save & Close') ON btnSave
};

-- Multiple properties at once
ALTER PAGE Module.EditPage {
SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave
SET (Caption: 'Save & Close', ButtonStyle: Success) ON btnSave
};
```

**Supported SET properties:**

| Property | Description | Example |
|----------|-------------|---------|
| `Caption` | Button/link caption | `SET Caption = 'Submit' ON btnSave` |
| `Label` | Input field label | `SET Label = 'Full Name' ON txtName` |
| `ButtonStyle` | Button visual style | `SET ButtonStyle = Danger ON btnDelete` |
| `Class` | CSS class names | `SET Class = 'card p-3' ON cMain` |
| `Style` | Inline CSS | `SET Style = 'margin: 8px;' ON cBox` |
| `DynamicClasses` | Runtime-computed CSS classes | `SET DynamicClasses = if $currentObject/IsActive then 'is-active' else '' ON cMain` |
| `Editable` | Editability mode | `SET Editable = ReadOnly ON txtEmail` |
| `Visible` | Visibility expression | `SET Visible = '$showField' ON txtPhone` |
| `Name` | Widget name | `SET Name = 'txtFullName' ON txtName` |
| `Caption` | Button/link caption | `SET (Caption: 'Submit') ON btnSave` |
| `Label` | Input field label | `SET (Label: 'Full Name') ON txtName` |
| `ButtonStyle` | Button visual style | `SET (ButtonStyle: Danger) ON btnDelete` |
| `Class` | CSS class names | `SET (Class: 'card p-3') ON cMain` |
| `Style` | Inline CSS | `SET (Style: 'margin: 8px;') ON cBox` |
| `DynamicClasses` | Runtime-computed CSS classes | `SET (DynamicClasses: if $currentObject/IsActive then 'is-active' else '') ON cMain` |
| `Editable` | Editability mode | `SET (Editable: ReadOnly) ON txtEmail` |
| `Visible` | Visibility expression | `SET (Visible: '$showField') ON txtPhone` |
| `Name` | Widget name | `SET (Name: 'txtFullName') ON txtName` |

### SET -- Page-Level Properties

Expand All @@ -61,9 +61,9 @@ it to `''` clears it.

```sql
ALTER PAGE Module.EditPage {
SET Title = 'Customer Details';
SET Class = 'container-fluid bg-light'; -- page CSS class (Forms$Appearance)
SET Style = 'min-height: 100vh' -- page inline style
SET (Title: 'Customer Details');
SET (Class: 'container-fluid bg-light'); -- page CSS class (Forms$Appearance)
SET (Style: 'min-height: 100vh') -- page inline style
};
```

Expand All @@ -73,7 +73,7 @@ Use quoted property names to set properties on pluggable widgets (ComboBox, Data

```sql
ALTER PAGE Module.EditPage {
SET 'showLabel' = false ON cbStatus
SET ('showLabel': false) ON cbStatus
};
```

Expand Down Expand Up @@ -129,18 +129,18 @@ ALTER PAGE Module.EditPage {

The inserted widgets use the same syntax as in `CREATE PAGE`. Multiple widgets can be inserted in a single block.

### DROP WIDGET -- Remove Widgets
### DROP -- Remove Widgets

Remove one or more widgets by name:

```sql
ALTER PAGE Module.EditPage {
DROP WIDGET txtUnused
DROP txtUnused
};

-- Multiple widgets
ALTER PAGE Module.EditPage {
DROP WIDGET txtFax, txtPager, btnObsolete
DROP txtFax, txtPager, btnObsolete
};
```

Expand Down Expand Up @@ -181,10 +181,10 @@ Multiple operations can be combined in a single ALTER statement. They are applie
```sql
ALTER PAGE Module.Customer_Edit {
-- Change button appearance
SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave;
SET (Caption: 'Save & Close', ButtonStyle: Success) ON btnSave;

-- Remove unused fields
DROP WIDGET txtFax;
DROP txtFax;

-- Add new fields after email
INSERT AFTER txtEmail {
Expand Down Expand Up @@ -227,8 +227,8 @@ ALTER PAGE MyModule.Customer_Edit {

```sql
ALTER PAGE MyModule.Order_Edit {
SET (Caption = 'Submit Order', ButtonStyle = Success) ON btnSave;
SET Caption = 'Discard' ON btnCancel
SET (Caption: 'Submit Order', ButtonStyle: Success) ON btnSave;
SET (Caption: 'Discard') ON btnCancel
};
```

Expand All @@ -246,12 +246,12 @@ ALTER PAGE MyModule.Customer_Overview {

-- Remove a column
ALTER PAGE MyModule.Customer_Overview {
DROP WIDGET dgCustomers.OldColumn
DROP dgCustomers.OldColumn
};

-- Change a column's caption
ALTER PAGE MyModule.Customer_Overview {
SET Caption = 'E-mail Address' ON dgCustomers.Email
SET (Caption: 'E-mail Address') ON dgCustomers.Email
};

-- Replace a column
Expand Down
Loading
Loading