diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index d26296cf3e..ddc1de6b83 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -738,3 +738,6 @@ {"area": "mdl/executor", "date": "2026-09-27", "symptom": "describe microflow printed `validation feedback $O/A message '{1} \u2026';` with no template arguments; describe -> exec stored the feedback with none", "cause": "formatAction's ValidationFeedbackAction case never read TemplateParameters, although the reader (textTemplateFromGen) and the writer both carried them", "file": "mdl/executor/cmd_microflows_format_action.go (templateArgsClause)", "insight": "A text template has two halves, the text and its parameters; check every describe case that prints one prints both. Found while switching the template form to `with ({1} = \u2026)` (ako/mxcli#751)."} {"area": "mdl/executor", "date": "2026-09-28", "symptom": "create or modify workflow of its own describe output renames Studio Pro's implicit activities: start1 -> Start, end1 -> End, an event sub-process end2 -> End2", "cause": "describe prints no name for the implicit start/end, branch ends, jumps or end-of-path markers, and the rebuild names them from captions", "file": "mdl/executor/workflow_carry_names.go", "fix": "Pair declared and stored flows structurally (named activities by name, unnamed ones by type ordinal, nested flows by position, event sub-processes by name) and carry the stored names before deduplication", "insight": "Carry before the CE0495 deduplication, not after, so the carried names take part in it; pair nested flows with nil slots kept, because the rebuild leaves an empty outcome's flow nil where the stored one is an empty Flow", "refs": ["ako/mxcli#743"], "test": "TestCreateOrModifyWorkflow_CarriesNamesDescribeCannotPrint"} {"area": "mdl/executor", "date": "2026-09-28", "symptom": "create or modify external entity declaring OrderId: String(20) on an Edm.Int64 property keeps RemoteType Edm.Int64; mx check CE6616", "cause": "carryStoredAttributeState carries RemoteType by name regardless of the declared type", "file": "mdl/executor/external_entity_remote_type.go", "fix": "Refuse under mdl 1 (MDL-V1-REMOTETYPE), warn under mdl 0", "insight": "CE6616 compares against the service's $metadata, not the stored RemoteType, so deriving RemoteType from the declared type would not fix it; measured with mx check on a TestApp copy", "refs": ["ako/mxcli#764"], "test": "TestCreateOrModifyExternalEntity_RemoteTypeMismatchRefusedUnderMdl1"} +{"area": "mdl/executor", "date": "2026-09-28", "symptom": "describe output that lost data on replay, the part of ako/mxcli#707 that #728 left open: a user role's Description and CheckSecurity were printed as `--` comments and a user role with no module roles described as `create user role X;`, which did not parse; workflow activity, event sub-process and workflow-level annotations were `-- annotation:` comments; a consumed REST header `'Bearer ' + $Token` stored only `Bearer ` and described back as that literal; image-collection describe wrote /tmp/mxcli-preview files and printed their paths.", "cause": "Each describer printed what its grammar had no slot for as a comment: the positional user-role form had no key for the description, check-security or manageable roles; workflows had only the standalone `annotation` statement (refused by MDL-WF04) and no attached form; the REST header grammar took an expression but the writer stores a value template, so the variable was dropped; image bytes live in the model, but describe exported them to a side file rather than into the statement.", "file": "mdl/grammar/domains/MDLSecurity.g4, MDLWorkflow.g4, MDLService.g4, MDLDomainModel.g4; mdl/visitor/visitor_security.go, visitor_workflow.go, visitor_rest.go, visitor_imagecollection.go; mdl/executor/cmd_security.go, cmd_security_write.go, cmd_workflows.go, cmd_workflows_write.go, cmd_rest_clients.go, cmd_imagecollections.go; mdl/backend (SetUserRoleProperties)", "fix": "User role properties are a ( Key: value ) list (ModuleRoles, Description, ManageAllRoles, ManageableRoles, ManageUsersWithoutRoles, CheckSecurity), the list optional; the positional form is MDL-DEPR710 with an fmt --upgrade rewrite. Workflow notes are `@annotation '…'` before an activity or event sub-process and a header `annotation '…'` clause. A REST header value is the template `'Bearer {Token}'`; the expression form is MDL-DEPR711 and now builds that template. Image describe writes `Data: ''` (Format only when the bytes do not show it), the TUI preview decodes it, and a File: path resolves script-relative.", "insight": "A `--` comment in describe output is a data-loss marker, not a rendering choice: every one of these was a property the grammar could not state. Grep describers for `-- ` emits of model values before assuming a describer round-trips. For the REST header the evidence that settled the template syntax was Studio Pro's own documentation (via the MCP search_mendix_knowledge_base tool): parameters are `{name}` in the path, query, header value and body.", "test": "mdl/executor/issue707_remaining_test.go, mdl/upgrade/issue707_upgrade_test.go, cmd/mxcli/tui/image_render_test.go"} +{"area": "mdl/executor", "date": "2026-09-28", "symptom": "re-executing the full describe of a flow whose if-merge wraps onto a new row (CxLayout.CX_VAL_Factory, CX_VAL_EmailTemplate in 02c) draws the merge-to-split flow out of the merge's right side instead of its bottom; layout flows, ELK, diff, with handles and create or modify's diff inherit it", "cause": "the flow leaving an if's closing merge had no annotation slot: describe wrote only the next split's to: top, and the top-level builder dropped an if's own @anchor(from:) (it honoured it only inside branch bodies)", "file": "mdl/executor/cmd_microflows_show_helpers.go", "fix": "describe writes the merge exit side as from: on the if's @anchor (keyed on the merge the @merge line already names); the builder hands the if's from: to the merge-out flow via nextFlowAnchor, and case/type-split branches clear a nested if's leftover nextFlowAnchor", "insight": "the canonical describe hid it: omitting the split's @position let the rebuild wrap the row, and wrapping re-derives bottom->top. Only the full description, where every node is pinned, exposed that a merge's outgoing anchor was unwritable; compare full describe -> re-exec geometry against the stored flow, not canonical", "refs": ["ako/mxcli#767"], "test": "TestDescribe_MergeExitAnchorRoundTrips"} +{"area": "mdl/executor", "date": "2026-09-28", "symptom": "`refresh catalog full source` under `go test -race` reports two DATA RACEs in getModulesFromCache/findModule (helpers.go), reached from describeEntity in the catalog's parallel describe workers", "cause": "executorCache.modules is filled lazily with no synchronisation; the catalog runs captureDescribeParallel from a goroutine pool sharing one ExecContext.Cache, and preWarmCache warmed the hierarchy and name maps but not the module list, so several workers each listed the modules and published the slice while others read the field", "file": "mdl/executor/helpers.go (getModulesFromCache, executorCache.cachedModules), mdl/executor/executor.go (modulesMu), mdl/executor/cmd_pages_builder.go (getModules)", "fix": "Fill and invalidate the module list under executorCache.modulesMu; the page builder's getModules goes through the same cachedModules", "insight": "The race detector only reports a location pair once per process, so a -race test run with -count>1 passes on later iterations even against the unfixed code; pair it with a detector-free assertion (ListModules called once by N concurrent lookups on a slow mock). Driving the whole `refresh catalog full source` under -race also surfaces a separate race inside the ANTLR Go runtime's shared DFA (page/workflow describes parse expressions concurrently), so the regression test drives entity describes only", "refs": ["ako/mxcli#765"], "test": "TestModuleCache_ConcurrentFillListsOnce, TestParallelEntityDescribes_NoDataRace"} diff --git a/.claude/skills/fix-issue/findings/mdl-visitor.jsonl b/.claude/skills/fix-issue/findings/mdl-visitor.jsonl index 81eedde092..452b190a3b 100644 --- a/.claude/skills/fix-issue/findings/mdl-visitor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-visitor.jsonl @@ -37,3 +37,4 @@ {"area": "mdl/visitor", "date": "2026-09-25", "symptom": "A page action's microflow argument `Flag: true and false` was stored as the expression \"trueandfalse\", `Mode: if true then 'a' else 'b'` as \"iftruethen'a'else'b'\", and a REST call parameter `$OrderId = if $x then $id else 'none'` as \"if$xthen$idelse'none'\" (measured by decoding the units on a copy of ako/TestApp). `check -p --references` passed; describe printed the fused text back", "cause": "Four sites stored an expression as text via ANTLR's ctx.GetText(), which concatenates tokens without the hidden-channel whitespace: microflowArgV3 values (page/nanoflow call arguments), contentparams values, send-rest-request WITH parameters, and a dynamic `execute database query`. Literals, `+` and a lone $currentObject are single tokens or need no spacing, so every common case looked correct", "file": "`mdl/visitor/visitor_helpers.go` (`expressionSourceText`), `mdl/visitor/visitor_page_v3.go` (`buildMicroflowArgV3`, `buildParamAssignmentV3`), `mdl/visitor/visitor_microflow_actions.go` (dynamic query, send rest params)", "insight": "**GetText() on an expression context is always a bug** — grep `Expression().*GetText()` / `expr.GetText()` in mdl/visitor; each hit either builds the AST or must use expressionSourceText (whitespace kept, MDL comments stripped). The earlier comment-leak fix moved the six microflow sites to extractExpressionText and missed these four because they lived in page/REST code, not microflow statements: fix a text-extraction defect by searching for the call, not the feature. The tell that hid it: tests used `$currentObject` and `'a' + 'b'`, both immune; a probe with `and` / `if…then` exposed all four at once. Found while writing PROPOSAL_first_class_expressions.md §5.1. Tests `visitor_expression_source_text_test.go`", "refs": ["mendixlabs/mxcli#750"]} {"area": "mdl/visitor", "date": "2026-09-26", "refs": ["ako/mxcli#706"], "rules": ["MDL-IDX01", "MDL-ENUMDOC01"], "symptom": "Five forms parsed and `check` printed \"Syntax OK\", then the model lost or changed them: `throw 'x';` vanished from the microflow; `Amount: float` / `currency` became String(unlimited) (Void as a microflow type) and `date` became DateTime; `create association X (from A to B, type: referenceset, storage: table)` was stored as a Reference with column storage; a `/** … */` on an enumeration value and an index name (`index Idx on (…)`, `create index Idx on E (…)`) were read and written nowhere", "cause": "Three different gaps behind one symptom: throwStatement had a grammar rule and no listener (a whole statement); buildDataType/buildMicroflowDataType end in a default return (String / Void) that the FLOAT_TYPE and CURRENCY_TYPE tokens fell into, and DATE_TYPE mapped to an ast.TypeDate the writers alias to DateTime; ExitCreateAssociationStatement read only ctx.AssociationOptions(), while the parenthesised alternative puts its options directly under the statement as `(COMMA associationOption)*`. Enumeration-value docs and index names have no home in the metamodel (EnumerationValue and Index have no such property)", "file": "`mdl/visitor/visitor_silent_drops.go` (ExitThrowStatement, EnterDataType, EnterNonListDataType, rejectParenthesisedAssociation), `mdl/executor/validate_unstored_text.go` (MDL-IDX01, MDL-ENUMDOC01); tests `mdl/visitor/silent_drops_706_test.go`, `mdl/executor/validate_unstored_text_test.go`", "insight": "**Split by what is lost: refuse when the MODEL would be wrong, warn when only WORDS are lost.** throw/types/association write a different model, so they are visitor errors with the working spelling in the message; doc comments on values and index names leave a correct model, and were used in 56 places in our own examples, so they are check warnings. **Refuse from the parse tree, not by deleting the grammar alternative**: `float`, `currency`, `date` and `throw` are also keywords-as-identifiers, so removing `| FLOAT_TYPE` from dataType would let `Amount: float` re-parse as an enumeration reference named float — the same silence again. A listener on EnterDataType covers every place a type is written (attribute, parameter, return, declare, constant, service rules) in one method; checking only the direct child token keeps `M.Currency` and `enumeration(M.Float)` legal (the control). To find these: any `if … ; return ast.DataType{Kind: …}` fall-through default in a builder is a silent-substitution site, and a grammar alternative whose sub-rules differ from its sibling's (`associationOption` vs `associationOptions`) needs its own read. Sweep: run old vs new `mxcli check` over `git ls-files '*.mdl'` and diff exit codes — it found every example that relied on the dropped forms (5 files)"} {"date": "2026-09-27", "area": "mdl/visitor", "symptom": "A member path after `*`, `div`, `mod` (`$a/X * $b/Y`) is stored as division: `describe` shows `$a/X * $b / Y`, and inside a list `filter` `$b / $currentObject/Y`. `mxcli check`, `mx check` and the build are all green; Mendix evaluates a different expression.", "cause": "`/` shares a precedence level with `*`, `div`, `mod` in `multiplicativeExpression` (MDLSettings.g4), so the left-associative chain parses `$a/X * $b/Y` as `(($a/X) * $b) / Y`. `buildMultiplicativeExpression` only turned `/ Member` into an AttributePathExpr when the WHOLE left side was a variable or path; here `$b` was already the right operand of the product, so the `/` stayed a BinaryExpr. The serializer then printed a spaced `/` and the filter's `qualifyIteratorAttributes` read the stranded `Y` as a bare attribute.", "file": "mdl/visitor/visitor_microflow_expression.go (attachPathToLastOperand)", "fix": "When a `/ Member` step cannot attach to the whole left side, attach it to the last operand of the preceding multiplicative BinaryExpr (`*`, `div`, `mod`, `%`, `:`): `(L op $b) / Y` becomes `L op ($b/Y)`. Mendix has no `/` division, so `/` followed by a member name is always navigation. Division by a parenthesised expression is left alone (control test).", "insight": "Fixing it where the AST is BUILT fixes every consumer at once (serializer, iterator qualification, MDL045's slash-division check), where the earlier #52 fix only taught MDL045 to tolerate the mis-nested tree. Test: `TestMemberPathAfterMultiplicativeOperatorIsStoredAsWritten` (flow builder, stored strings for sum, filter, declare, div chain) plus a control `TestSlashAfterNonPathIsNotMadeAMemberPath`. Revert check: stubbing the fix brings back all four wrong strings. A unary-minus branch was dropped because removing it failed nothing (`2 * -$b/Y` was already stored correctly).", "refs": ["#768", "#52"], "rules": []} +{"date": "2026-09-28", "area": "mdl/visitor", "symptom": "`create view entity if not exists M.V (...) as (...)` on an existing view entity fails with \"entity already exists\" although `mxcli syntax` documents `CREATE VIEW ENTITY [IF NOT EXISTS]`; every other create kind rejected `if not exists` as a parse error.", "cause": "The grammar accepted `ifNotExists?` on all five entity alternatives, but only `ExitCreateEntityStatement`'s non-view branch read it; `buildViewEntity` built a `CreateViewEntityStmt`, which had no field for it, so the guard parsed and was dropped. A per-builder guard is the one the next builder forgets (same class as #531 for drop if exists).", "file": "mdl/visitor/visitor_create_guard.go (applyCreateGuard), mdl/executor/cmd_create_guard.go (skipExistingCreate)", "fix": "`ast.CreateGuard` embedded in every named create statement; the visitor applies `if not exists` once in ExitCreateStatement to whichever statement the create rule built (and errors if that type cannot carry it), and Registry.Dispatch probes existence and skips before the handler. Entity and association keep their in-handler check.", "insight": "Tests are driven by the grammar's own list of create kinds (createStatementKinds) plus allKnownStatements, so a new create rule fails until it carries the guard or is exempted with a reason. The executor test swaps the handler for a recorder, which isolates the guard from what each handler needs from a mock; revert check: stubbing skipExistingCreate fails all 38 existing-element cases, and the absent-element and unguarded controls keep it honest.", "refs": ["#731"], "rules": ["MDL067"]} diff --git a/.claude/skills/mendix/alter-page/SKILL.md b/.claude/skills/mendix/alter-page/SKILL.md index f1733e50f2..57f69e2765 100644 --- a/.claude/skills/mendix/alter-page/SKILL.md +++ b/.claude/skills/mendix/alter-page/SKILL.md @@ -96,7 +96,7 @@ everywhere. Removing one has its own form: alter page Pages.Vehicle_Overview { insert into vehicleListView { template for Pages.Motorcycle { - dynamictext mcLabel (content: 'Motorcycle {1}', contentparams: [{1} = Brand]) + dynamictext mcLabel (content: 'Motorcycle {1}', contentparams: ({1} = Brand)) } }; drop template for Pages.SUV in vehicleListView diff --git a/.claude/skills/mendix/atlas-design/SKILL.md b/.claude/skills/mendix/atlas-design/SKILL.md index 4c2bbf48e2..53ace22414 100644 --- a/.claude/skills/mendix/atlas-design/SKILL.md +++ b/.claude/skills/mendix/atlas-design/SKILL.md @@ -151,9 +151,9 @@ mxcli -p app.mpr -c "describe building block Atlas_Web_Content.Card" ``` ``` { - container container2 (DesignProperties: ['Card style': on]) { + container container2 (DesignProperties: ('Card style': on)) { dynamictext text22 (Content: 'Card title', RenderMode: H4, Class: 'card-title', - DesignProperties: ['Spacing': ['margin-bottom': 'L']]) + DesignProperties: ('Spacing': ('margin-bottom': 'L'))) } } ``` diff --git a/.claude/skills/mendix/atlas-design/reference/building-blocks.md b/.claude/skills/mendix/atlas-design/reference/building-blocks.md index 5440173086..eb838aaa73 100644 --- a/.claude/skills/mendix/atlas-design/reference/building-blocks.md +++ b/.claude/skills/mendix/atlas-design/reference/building-blocks.md @@ -63,13 +63,13 @@ create page MyModule.CardDemo layout: Atlas_Core.Atlas_Default ) { - container myCard (designproperties: ['Card style': on]) { + container myCard (designproperties: ('Card style': on)) { dynamictext cardTitle ( content: 'Customers', rendermode: H4, class: 'card-title', - designproperties: ['Spacing': ['margin-bottom': 'L']] + designproperties: ('Spacing': ('margin-bottom': 'L')) ) } }; @@ -81,7 +81,7 @@ arbitrary bodies, no copy-paste of the wrapper markup. ```mdl create fragment SectionCard as { - container card1 (designproperties: ['Card style': on, 'Spacing': ['margin-bottom': 'Large']]) { + container card1 (designproperties: ('Card style': on, 'Spacing': ('margin-bottom': 'Large'))) { container cardBody (class: 'card-body') { slot content -- each page's widgets land here } @@ -120,7 +120,7 @@ inside; typed **parameters** vary *which entity* and *which microflow*. Declare ```mdl create fragment EntityCard($data: datasource, $onOpen: action) as { - container card1 (designproperties: ['Card style': on]) { + container card1 (designproperties: ('Card style': on)) { listview lv (datasource: $data) { slot content actionbutton open (caption: 'Open', action: $onOpen, buttonstyle: primary) @@ -149,10 +149,10 @@ For a binding the override rule can't reach, copy the block in (`as prefix_`) an ``` { - container container1 (Class: 'pageheader', DesignProperties: ['Item gap': 'None']) { + container container1 (Class: 'pageheader', DesignProperties: ('Item gap': 'None')) { dynamictext text40 (Content: 'Page header title', RenderMode: H1, Class: 'pageheader-title') dynamictext text39 (Content: 'Supporting text', RenderMode: Paragraph, Class: 'pageheader-subtitle', - DesignProperties: ['Color': 'Detail color', 'Spacing': ['margin-bottom': 'None']]) + DesignProperties: ('Color': 'Detail color', 'Spacing': ('margin-bottom': 'None'))) } } ``` @@ -166,14 +166,14 @@ create page MyModule.CustomersHeaderDemo layout: Atlas_Core.Atlas_Default ) { - container pageHeader (class: 'pageheader', designproperties: ['Item gap': 'None']) { + container pageHeader (class: 'pageheader', designproperties: ('Item gap': 'None')) { dynamictext headerTitle (content: 'Customers', rendermode: H1, class: 'pageheader-title') dynamictext headerSubtitle ( content: 'All active accounts', rendermode: Paragraph, class: 'pageheader-subtitle', - designproperties: ['Color': 'Detail color', 'Spacing': ['margin-bottom': 'None']] + designproperties: ('Color': 'Detail color', 'Spacing': ('margin-bottom': 'None')) ) } }; @@ -245,13 +245,13 @@ cleanly into Studio Pro. Common mappings: | Class-style | Typed design-property equivalent | |---|---| -| `class:'card'` | `designproperties: ['Card style': on]` | -| `class:'background-primary'` | `designproperties: ['Background color': 'Brand Primary']` | -| `class:'flex-column'` | `designproperties: ['Flex container': 'Vertical (column)']` | -| `class:'flex-row'` | `designproperties: ['Flex container': 'Horizontal (row)']` | -| `class:'align-x-center'` | `designproperties: ['Align items X': 'Center']` | -| `class:'Shadow'` | `designproperties: ['Shadow': 'None' / 'Small' / …]` | -| spacing utilities | `designproperties: ['Spacing': ['margin-bottom': 'L', 'padding-top': 'S']]` | +| `class:'card'` | `designproperties: ('Card style': on)` | +| `class:'background-primary'` | `designproperties: ('Background color': 'Brand Primary')` | +| `class:'flex-column'` | `designproperties: ('Flex container': 'Vertical (column)')` | +| `class:'flex-row'` | `designproperties: ('Flex container': 'Horizontal (row)')` | +| `class:'align-x-center'` | `designproperties: ('Align items X': 'Center')` | +| `class:'Shadow'` | `designproperties: ('Shadow': 'None' / 'Small' / …)` | +| spacing utilities | `designproperties: ('Spacing': ('margin-bottom': 'L', 'padding-top': 'S'))` | **Both channels render identically at runtime** — raw `class:` is sufficient for the visual result today. The typed channel matters for Studio Pro round-trip and is the diff --git a/.claude/skills/mendix/check-syntax/SKILL.md b/.claude/skills/mendix/check-syntax/SKILL.md index e15eed2e0b..8f9dea6300 100644 --- a/.claude/skills/mendix/check-syntax/SKILL.md +++ b/.claude/skills/mendix/check-syntax/SKILL.md @@ -87,8 +87,10 @@ fourth — so "run it and see" is not a free experiment. `check` reports every conflict in the script before anything is written. Three spellings say "fine if it already exists", and none is reported: -`create or modify`, `create or replace`, and `create … if not exists` (which -leaves the stored element untouched rather than rewriting it). `create module M;` +`create or modify`, `create or replace`, and `create if not exists ` +(which leaves the stored element untouched rather than rewriting it; every +`create` that names one element takes it, e.g. `create page if not exists M.P …` +— `mxcli syntax create-if-not-exists`). `create module M;` is never reported either — it is a no-op when the module exists, which is what lets it open every script. @@ -311,7 +313,7 @@ Before writing any MDL, verify these requirements: > **Exception — never quote `$`-prefixed variable/parameter references.** The quote > rule is for *bare* names (entities, attributes, associations, declared parameter > names). Variable and parameter **references** in expressions and widget bindings -> stay **unquoted**: `datasource: $X`, `params: { $X: MES."Order" }`, `$currentObject`. +> stay **unquoted**: `datasource: $X`, `params: ( $X: MES."Order" )`, `$currentObject`. > Quoting them (`"$X"`) breaks resolution ("parameter … references '$X' but no such > parameter is declared"). > diff --git a/.claude/skills/mendix/choose-edit-mode/SKILL.md b/.claude/skills/mendix/choose-edit-mode/SKILL.md index f308ec52d6..933408ec63 100644 --- a/.claude/skills/mendix/choose-edit-mode/SKILL.md +++ b/.claude/skills/mendix/choose-edit-mode/SKILL.md @@ -42,7 +42,7 @@ Not sure who owns it? Treat it as Studio Pro-owned. | Association | `alter association … set …` | | Enumeration | `alter enumeration` (add / rename / modify / drop value) | | Page, snippet, layout | `alter page` / `alter snippet` / `alter layout` { set / insert / drop / replace } | -| Workflow | `alter workflow` (set, insert after, drop / replace activity, outcomes, paths) | +| Workflow | `alter workflow` { set / insert before / after / into / drop / replace } — activities by name or 'caption' | | Settings, security | `alter settings`, `alter app security`, `grant` / `revoke` | | Many pages at once | `update widgets … where …` (see `bulk-widget-updates`) | | **Microflow, nanoflow** | **none yet** | diff --git a/.claude/skills/mendix/create-page/SKILL.md b/.claude/skills/mendix/create-page/SKILL.md index 23214d2139..1d1d47946f 100644 --- a/.claude/skills/mendix/create-page/SKILL.md +++ b/.claude/skills/mendix/create-page/SKILL.md @@ -29,8 +29,8 @@ Guide for writing CREATE PAGE statements in Mendix Definition Language (MDL). ```sql create [or replace] page Module.PageName ( - [params: { $ParamName: Module.EntityType | PrimitiveType, ... },] - [variables: { $varName: DataType = 'defaultExpression', ... },] + [params: ( $ParamName: Module.EntityType | PrimitiveType, ... ),] + [variables: ( $varName: DataType = 'defaultExpression', ... ),] title: 'Page Title', layout: Module.LayoutName, [url: 'page-url',] @@ -75,7 +75,7 @@ Both are optional and can be changed later with `alter page … { set (Class: ' | Selection binding | `datasource: selection widget` | `dataview dv (datasource: selection galleryList)` | | CSS class | `class: 'classes'` | `container c (class: 'card mx-spacing-top-large')` | | Inline style | `style: 'css'` | `container c (style: 'padding: 16px;')` | -| Design properties | `designproperties: [...]` | `container c (designproperties: ['Spacing top': 'Large', 'full width': on])` | +| Design properties | `designproperties: (...)` | `container c (designproperties: ('Spacing top': 'Large', 'full width': on))` | ### FOLDER Option @@ -124,13 +124,13 @@ container c (style: 'background-color: #f8f9fa; padding: 16px;') { ... } **Design Properties** — Atlas UI structured properties (spacing, colors, toggles): ```sql -- Option property: 'Key': 'Value' -container c (designproperties: ['Spacing top': 'Large', 'Background color': 'Brand Primary']) { ... } +container c (designproperties: ('Spacing top': 'Large', 'Background color': 'Brand Primary')) { ... } -- Toggle property: 'Key': ON (enabled) or OFF (disabled/omitted) -container c (designproperties: ['Full width': on]) { ... } +container c (designproperties: ('Full width': on)) { ... } -- Multiple types combined -actionbutton btn (caption: 'Save', designproperties: ['Size': 'Large', 'Full width': on]) +actionbutton btn (caption: 'Save', designproperties: ('Size': 'Large', 'Full width': on)) ``` **Dynamic Classes** — a Mendix expression evaluated at runtime that returns a @@ -157,7 +157,7 @@ container ctnHero ( class: 'card', style: 'border-left: 4px solid #264AE5;', dynamicclasses: if $currentObject/Featured then 'is-featured' else '', - designproperties: ['Spacing top': 'Large', 'Full width': on] + designproperties: ('Spacing top': 'Large', 'Full width': on) ) { dynamictext txtTitle (content: 'Styled Container', rendermode: H3) } diff --git a/.claude/skills/mendix/create-page/reference/examples.md b/.claude/skills/mendix/create-page/reference/examples.md index 65c1b431c3..0ba8361a58 100644 --- a/.claude/skills/mendix/create-page/reference/examples.md +++ b/.claude/skills/mendix/create-page/reference/examples.md @@ -9,7 +9,7 @@ Supporting reference for [create-page](../SKILL.md). ```sql create or replace page CRM.CustomerEdit ( - params: { $Customer: CRM.Customer }, + params: ( $Customer: CRM.Customer ), title: 'Edit Customer', layout: Atlas_Core.PopupLayout ) @@ -82,8 +82,8 @@ create page CRM.Customer_MasterDetail dynamictext heading (content: 'Customers', rendermode: H3) gallery customerList (datasource: database from CRM.Customer sort by Name asc, selection: single) { template { - dynamictext name (content: '{1}', contentparams: [{1} = Name], rendermode: H4) - dynamictext email (content: '{1}', contentparams: [{1} = Email]) + dynamictext name (content: '{1}', contentparams: ({1} = Name), rendermode: H4) + dynamictext email (content: '{1}', contentparams: ({1} = Email)) } } } diff --git a/.claude/skills/mendix/create-page/reference/widgets.md b/.claude/skills/mendix/create-page/reference/widgets.md index 7e75d8e763..aa73e7247b 100644 --- a/.claude/skills/mendix/create-page/reference/widgets.md +++ b/.claude/skills/mendix/create-page/reference/widgets.md @@ -17,13 +17,13 @@ dynamictext heading (content: 'Heading Text', rendermode: H2) dynamictext productName (content: '$Product.Name', rendermode: H3) -- Explicit template with page parameter binding -dynamictext greeting (content: 'Welcome, {1}!', contentparams: [{1} = $Customer.Name]) +dynamictext greeting (content: 'Welcome, {1}!', contentparams: ({1} = $Customer.Name)) -- Template with attribute from current DataView context (simple attribute name) -dynamictext email (content: 'Email: {1}', contentparams: [{1} = Email]) +dynamictext email (content: 'Email: {1}', contentparams: ({1} = Email)) -- Bind directly to an attribute of the surrounding DataView/ListView/Gallery --- entity. `Attribute: X` is shorthand for `content: '{1}', contentparams: [{1} = X]`. +-- entity. `Attribute: X` is shorthand for `content: '{1}', contentparams: ({1} = X)`. dynamictext title (Attribute: Title) ``` @@ -40,11 +40,11 @@ default (e.g. `5068.38000000`). ```sql -- Decimal: 2 decimals + thousands separator -> "5,068.38" -dynamictext amt (content: '{1}', contentparams: [{1} = Amount format (decimalPrecision: 2, groupDigits: true)]) +dynamictext amt (content: '{1}', contentparams: ({1} = Amount format (decimalPrecision: 2, groupDigits: true))) -- DateTime: date + time, or a custom pattern -dynamictext due (content: '{1}', contentparams: [{1} = DueOn format (dateFormat: DateTime)]) -dynamictext day (content: '{1}', contentparams: [{1} = DueOn format (dateFormat: Custom, customDateFormat: 'dd-MM-yyyy')]) +dynamictext due (content: '{1}', contentparams: ({1} = DueOn format (dateFormat: DateTime))) +dynamictext day (content: '{1}', contentparams: ({1} = DueOn format (dateFormat: Custom, customDateFormat: 'dd-MM-yyyy'))) ``` | Format key | Applies to | Values | @@ -198,13 +198,13 @@ name, which is why the keyword takes `for` and a qualified entity: ```sql listview vehicleListView (datasource: database from Pages.Vehicle) { -- the default body: used for an object no template matches - dynamictext defaultVehicle (content: '{1} {2}', contentparams: [{1} = Brand, {2} = Model]) + dynamictext defaultVehicle (content: '{1} {2}', contentparams: ({1} = Brand, {2} = Model)) template for Pages.Bus { - dynamictext busLabel (content: 'Bus, capacity {1}', contentparams: [{1} = PassengerCapacity]) + dynamictext busLabel (content: 'Bus, capacity {1}', contentparams: ({1} = PassengerCapacity)) } template for Pages.Truck { - dynamictext truckLabel (content: 'Truck, max load {1} kg', contentparams: [{1} = MaxLoadKg]) + dynamictext truckLabel (content: 'Truck, max load {1} kg', contentparams: ({1} = MaxLoadKg)) } } ``` @@ -322,7 +322,7 @@ datagrid gridName (datasource: database from Module.Entity) { Caption: 'Amount', ShowContentAs: dynamicText, Content: 'Amt: {1}', - ContentParams: [{1} = Amount format (decimalPrecision: 2, groupDigits: true)] + ContentParams: ({1} = Amount format (decimalPrecision: 2, groupDigits: true)) ) column due (attribute: DueOn, caption: 'Due') } @@ -549,8 +549,8 @@ gallery galleryName ( PhoneColumns: 1 ) { template { - dynamictext name (content: '{1}', contentparams: [{1} = Name], rendermode: H4) - dynamictext email (content: '{1}', contentparams: [{1} = Email]) + dynamictext name (content: '{1}', contentparams: ({1} = Name), rendermode: H4) + dynamictext email (content: '{1}', contentparams: ({1} = Email)) } } ``` @@ -562,8 +562,8 @@ gallery productGallery (datasource: database Module.Product, selection: single) textfilter searchName (attribute: Name) } template { - dynamictext prodName (content: '{1}', contentparams: [{1} = Name], rendermode: H4) - dynamictext prodCode (content: 'SKU: {1}', contentparams: [{1} = Code]) + dynamictext prodName (content: '{1}', contentparams: ({1} = Name), rendermode: H4) + dynamictext prodCode (content: 'SKU: {1}', contentparams: ({1} = Code)) } } ``` @@ -658,7 +658,7 @@ Embed a reusable snippet: snippetcall snippetName (snippet: Module.SnippetName) -- With parameters -snippetcall actions (snippet: Module.EntityActions, params: {entity: $Param}) +snippetcall actions (snippet: Module.EntityActions, params: (entity = $Param)) ``` **A parameter satisfied by the enclosing data context takes NO mapping.** Mendix @@ -672,12 +672,12 @@ dataview dvOrder (datasource: $Order) { } ``` -`params: {Order: $currentObject}` means the same thing and produces the same +`params: (Order = $currentObject)` means the same thing and produces the same (empty) mapping. Naming a real page parameter or variable produces a real mapping, as expected: ```sql -snippetcall scActions (snippet: MyModule.OrderActions, params: {Order: $Order}) +snippetcall scActions (snippet: MyModule.OrderActions, params: (Order = $Order)) ``` Omitting `Params:` where the context is a *different* entity is still an error — @@ -804,7 +804,7 @@ pluggablewidget 'com.mendix.widget.web.image.Image' cardImage ( -- numbered placeholders + contentparams: needed for several values, or a format block pluggablewidget 'com.mendix.widget.web.image.Image' cardImage ( datasource: imageUrl, - imageUrl: '{1}/{2}', contentparams: [{1} = BaseUrl, {2} = PictureUrl] + imageUrl: '{1}/{2}', contentparams: ({1} = BaseUrl, {2} = PictureUrl) ) -- `Params`: the property's OWN parameters. `contentparams` is one list @@ -812,8 +812,8 @@ pluggablewidget 'com.mendix.widget.web.image.Image' cardImage ( -- `alternativeText` to different attributes; this can (ako/mxcli#575). pluggablewidget 'com.mendix.widget.web.image.Image' cardImage ( datasource: imageUrl, - imageUrl: '{1}', imageUrlParams: [{1} = PictureUrl], - alternativeText: '{1}', alternativeTextParams: [{1} = Name] + imageUrl: '{1}', imageUrlParams: ({1} = PictureUrl), + alternativeText: '{1}', alternativeTextParams: ({1} = Name) ) ``` @@ -911,13 +911,13 @@ container card1 (class: 'card', style: 'padding: 16px;') { } -- Container with design properties -container spaced1 (designproperties: ['Spacing top': 'Large', 'Full width': on]) { +container spaced1 (designproperties: ('Spacing top': 'Large', 'Full width': on)) { dynamictext text1 (content: 'Spaced full-width content') } -- Nested containers with combined styling customcontainer outer1 (class: 'section') { - container inner1 (class: 'card', designproperties: ['Spacing top': 'Medium']) { + container inner1 (class: 'card', designproperties: ('Spacing top': 'Medium')) { dynamictext text1 (content: 'Nested content') } } diff --git a/.claude/skills/mendix/custom-widgets/SKILL.md b/.claude/skills/mendix/custom-widgets/SKILL.md index 62526e3a45..83b6aac102 100644 --- a/.claude/skills/mendix/custom-widgets/SKILL.md +++ b/.claude/skills/mendix/custom-widgets/SKILL.md @@ -142,8 +142,8 @@ gallery galleryName ( PhoneColumns: 1 ) { template { - dynamictext title (content: '{1}', contentparams: [{1} = Name], rendermode: H4) - dynamictext info (content: '{1}', contentparams: [{1} = Email]) + dynamictext title (content: '{1}', contentparams: ({1} = Name), rendermode: H4) + dynamictext info (content: '{1}', contentparams: ({1} = Email)) } filter { textfilter searchName (attribute: Name) @@ -456,7 +456,7 @@ Set `"templateFile": "mywidget.json"` in the .def.json. Project definitions over ```sql MYWIDGET myWidget1 (datasource: database Module.Entity, attribute: Name) { template content1 { - dynamictext label1 (content: '{1}', contentparams: [{1}=Name]) + dynamictext label1 (content: '{1}', contentparams: ({1}=Name)) } } ``` @@ -559,7 +559,7 @@ A `texttemplate` takes **text**, so a bare value renders the same string on ever row. Bind it with the property's own `Params` companion, named for whichever spelling the template used (`ImageUrl:` pairs with `ImageUrlParams:`) and taking the same `format (...)` block a `dynamictext` does — e.g. -`headerCaption: '{1}', headerCaptionParams: [{1} = Name]`, or a Timeline's +`headerCaption: '{1}', headerCaptionParams: ({1} = Name)`, or a Timeline's `title` / `description` bound separately. `contentparams:` is ONE list shared by every template on the widget, so it only disambiguates a widget with a single one; `'{AttrName}'` is the short form for one attribute. A companion whose diff --git a/.claude/skills/mendix/database-connections/SKILL.md b/.claude/skills/mendix/database-connections/SKILL.md index d5ebc3fd6c..eab17666e3 100644 --- a/.claude/skills/mendix/database-connections/SKILL.md +++ b/.claude/skills/mendix/database-connections/SKILL.md @@ -65,17 +65,22 @@ create constant MyModule.DbPassword type string ### Basic Connection Structure ```sql -create database connection Module.ConnectionName -type '' -connection string @Module.ConnectionStringConstant -username @Module.UsernameConstant -password @Module.PasswordConstant -begin - -- Query definitions go here -end; +create database connection Module.ConnectionName ( + Type: '', + ConnectionString: @Module.ConnectionStringConstant, + Username: @Module.UsernameConstant, + Password: @Module.PasswordConstant +) { + -- query definitions go here: query Name ( Sql: …, Returns: … ) +}; ``` -**The `@` is not optional.** `connection string`, `username` and `password` are +The connection's properties are in `( )` and its queries are its children, in +`{ }` — the shape of every declarative document (R2). The old clause form +(`type '…' connection string @… begin query … ; end`) still parses and warns +(MDL-DEPR127); `mxcli fmt --upgrade` rewrites it. + +**The `@` is not optional.** `ConnectionString`, `Username` and `Password` are ConstantIdentifier properties — Mendix stores a *reference to a Constant document*, never a value. The grammar accepts a bare string there, but writing one produces a project that **cannot be opened at all**: @@ -91,15 +96,15 @@ it as **MDL058** at both `check` and `exec`. ```sql -- WRONG — writes an unopenable .mpr -connection string 'jdbc:postgresql://localhost:5432/app' -username 'app' +ConnectionString: 'jdbc:postgresql://localhost:5432/app', +Username: 'app' -- RIGHT — declare the constant, then reference it create constant Module.DbUrl type String default 'jdbc:postgresql://localhost:5432/app'; create constant Module.DbUser type String default 'app'; -connection string @Module.DbUrl -username @Module.DbUser +ConnectionString: @Module.DbUrl, +Username: @Module.DbUser ``` The indirection is the point: the constant's value is per-environment, so a @@ -111,7 +116,7 @@ These are the values Studio Pro's own connector editor offers — read out of th shipped bundle at `modeler/ide-client/database-connector-editor/`, identical on 11.10.0, 11.12.1 and 11.13.0. -| Database | TYPE Value | Studio Pro label | +| Database | `Type:` value | Studio Pro label | |----------|------------|------------------| | SQL Server | `'MSSQL'` | Microsoft SQL | | MySQL | `'MySQL'` | MySQL | @@ -158,42 +163,50 @@ the file system disagree about where the dependency comes from. **`'Redshift'` and `'SQLServer'` are not real values.** Both appeared in an earlier version of this table and neither is in the picker on any version checked. mxcli writes the type string through unchanged and **mxbuild does not -validate it** — `type 'Redshift'` builds 0 errors and simply does not connect — +validate it** — `Type: 'Redshift'` builds 0 errors and simply does not connect — so `mxcli check` warns about an unrecognised type (MDL-DB01) rather than letting a green build hide it. ## Query Definition Syntax +A query is a child of the connection, with its properties in `( )`: +`Sql`, `Parameters`, `Returns` and `Map`. The SQL may be a string or `$$…$$`, +which needs no quote doubling. + ### Simple Query (No Parameters) ```sql -query QueryName - sql 'SELECT column1, column2 FROM table_name' - returns Module.EntityName; +query QueryName ( + Sql: 'SELECT column1, column2 FROM table_name', + Returns: Module.EntityName +) ``` ### Parameterized Query ```sql -query QueryName - sql 'SELECT * FROM table_name WHERE column = {paramName}' - parameter paramName: string - returns Module.EntityName; +query QueryName ( + Sql: 'SELECT * FROM table_name WHERE column = {paramName}', + Parameters: ( paramName: string ), + Returns: Module.EntityName +) ``` ### Query with Column Mapping -When database column names don't match entity attribute names: +When database column names don't match entity attribute names, `Map` binds each +attribute to its column — `Attribute = column`, the way a mapping side is written: ```sql -query QueryName - sql 'SELECT emp_id, emp_name, dept_no FROM employees' - returns Module.EmployeeRecord - map ( - emp_id as EmployeeId, - emp_name as EmployeeName, - dept_no as DepartmentNumber - ); +query QueryName ( + Sql: 'SELECT emp_id, emp_name, dept_no FROM employees', + Returns: Module.EmployeeRecord, + Map: ( + EmployeeId = emp_id, + EmployeeName = emp_name, + DepartmentNumber = dept_no + ) +) ``` ### Supported Parameter Types @@ -210,10 +223,10 @@ Parameters can include a test value for Studio Pro testing, or indicate they sho ```sql -- Test value (used in Studio Pro's Execute Query dialog) -parameter empName: string default 'Smith' +Parameters: ( empName: string default 'Smith' ) -- Test with NULL value -parameter optionalDate: datetime null +Parameters: ( optionalDate: datetime null ) ``` ## Complete Examples @@ -242,26 +255,29 @@ create non-persistent entity OracleDemo.EmpRecord ( ); -- Step 4: Create database connection -create database connection OracleDemo.HRDatabase -type 'Oracle' -connection string @OracleDemo.OracleConnectionString -username @OracleDemo.OracleUser -password @OracleDemo.OraclePassword -begin - query GetAllEmployees - sql 'SELECT EMPNO, ENAME, JOB, SAL, DEPTNO FROM EMP ORDER BY EMPNO' - returns OracleDemo.EmpRecord; - - query GetEmployeeByName - sql 'SELECT EMPNO, ENAME, JOB, SAL, DEPTNO FROM EMP WHERE ENAME = {empName}' - parameter empName: string - returns OracleDemo.EmpRecord; - - query GetHighEarners - sql 'SELECT EMPNO, ENAME, JOB, SAL, DEPTNO FROM EMP WHERE SAL >= {minSalary}' - parameter minSalary: decimal - returns OracleDemo.EmpRecord; -end; +create database connection OracleDemo.HRDatabase ( + Type: 'Oracle', + ConnectionString: @OracleDemo.OracleConnectionString, + Username: @OracleDemo.OracleUser, + Password: @OracleDemo.OraclePassword +) { + query GetAllEmployees ( + Sql: 'SELECT EMPNO, ENAME, JOB, SAL, DEPTNO FROM EMP ORDER BY EMPNO', + Returns: OracleDemo.EmpRecord + ) + + query GetEmployeeByName ( + Sql: 'SELECT EMPNO, ENAME, JOB, SAL, DEPTNO FROM EMP WHERE ENAME = {empName}', + Parameters: ( empName: string ), + Returns: OracleDemo.EmpRecord + ) + + query GetHighEarners ( + Sql: 'SELECT EMPNO, ENAME, JOB, SAL, DEPTNO FROM EMP WHERE SAL >= {minSalary}', + Parameters: ( minSalary: decimal ), + Returns: OracleDemo.EmpRecord + ) +}; ``` ### Example 2: PostgreSQL Connection @@ -280,33 +296,35 @@ create non-persistent entity Inventory.ProductRecord ( Price: decimal ); -create database connection Inventory.ProductDatabase -type 'PostgreSQL' -connection string @Inventory.PgConnectionString -username @Inventory.PgUser -password @Inventory.PgPassword -begin - query GetAllProducts - sql 'SELECT product_id, product_name, quantity, price FROM products' - returns Inventory.ProductRecord - map ( - product_id as ProductId, - product_name as ProductName, - quantity as Quantity, - price as Price - ); - - query SearchProducts - sql 'SELECT product_id, product_name, quantity, price FROM products WHERE product_name ILIKE {searchPattern}' - parameter searchPattern: string - returns Inventory.ProductRecord - map ( - product_id as ProductId, - product_name as ProductName, - quantity as Quantity, - price as Price - ); -end; +create database connection Inventory.ProductDatabase ( + Type: 'PostgreSQL', + ConnectionString: @Inventory.PgConnectionString, + Username: @Inventory.PgUser, + Password: @Inventory.PgPassword +) { + query GetAllProducts ( + Sql: 'SELECT product_id, product_name, quantity, price FROM products', + Returns: Inventory.ProductRecord, + Map: ( + ProductId = product_id, + ProductName = product_name, + Quantity = quantity, + Price = price + ) + ) + + query SearchProducts ( + Sql: 'SELECT product_id, product_name, quantity, price FROM products WHERE product_name ILIKE {searchPattern}', + Parameters: ( searchPattern: string ), + Returns: Inventory.ProductRecord, + Map: ( + ProductId = product_id, + ProductName = product_name, + Quantity = quantity, + Price = price + ) + ) +}; ``` ## Viewing Connections @@ -418,14 +436,15 @@ standalone JDBC harness before that. ### Parameterized Queries -Pass values for query parameters defined with `parameter` in the query definition: +Pass values for the query parameters declared in the query's `Parameters:` list: ```sql --- Query definition (in DATABASE CONNECTION block): --- QUERY GetDriversByNationality --- SQL 'SELECT * FROM drivers WHERE nationality = {nation}' --- PARAMETER nation: String --- RETURNS Module.DriverRecord; +-- Query definition (in the database connection's { } block): +-- query GetDriversByNationality ( +-- Sql: 'SELECT * FROM drivers WHERE nationality = {nation}', +-- Parameters: ( nation: String ), +-- Returns: Module.DriverRecord +-- ) -- Microflow execution: $Drivers = execute database query Module.Connection.GetDriversByNationality @@ -463,23 +482,25 @@ create constant HR.DbUrl type string default 'jdbc:postgresql://localhost:5432/h create constant HR.DbUser type string default 'app'; create constant HR.DbPass type string default ''; -create database connection HR.MainDB -type 'PostgreSQL' -connection string @HR.DbUrl -username @HR.DbUser -password @HR.DbPass -begin - query GetAllEmployees - sql 'SELECT emp_id, name, department FROM employees' - returns HR.EmployeeRecord - map (emp_id as EmpId, name as Name, department as Department); - - query GetByDepartment - sql 'SELECT emp_id, name, department FROM employees WHERE department = {dept}' - parameter dept: string - returns HR.EmployeeRecord - map (emp_id as EmpId, name as Name, department as Department); -end; +create database connection HR.MainDB ( + Type: 'PostgreSQL', + ConnectionString: @HR.DbUrl, + Username: @HR.DbUser, + Password: @HR.DbPass +) { + query GetAllEmployees ( + Sql: 'SELECT emp_id, name, department FROM employees', + Returns: HR.EmployeeRecord, + Map: (EmpId = emp_id, Name = name, Department = department) + ) + + query GetByDepartment ( + Sql: 'SELECT emp_id, name, department FROM employees WHERE department = {dept}', + Parameters: ( dept: string ), + Returns: HR.EmployeeRecord, + Map: (EmpId = emp_id, Name = name, Department = department) + ) +}; -- Microflow that executes the query create microflow HR.ACT_LoadEmployees($Department: string) diff --git a/.claude/skills/mendix/fragments/SKILL.md b/.claude/skills/mendix/fragments/SKILL.md index de133d3ca7..487408a169 100644 --- a/.claude/skills/mendix/fragments/SKILL.md +++ b/.claude/skills/mendix/fragments/SKILL.md @@ -52,7 +52,7 @@ Inside a page or snippet body: ```mdl create page Module.CustomerEdit ( - params: { $Customer: Module.Customer }, + params: ( $Customer: Module.Customer ), title: 'Edit Customer', layout: Atlas_Core.PopupLayout ) @@ -80,7 +80,7 @@ widgets should land, then fill it with the `use fragment X { … }` payload form ```mdl create fragment Card as { - container cardWrap (class: 'card', designproperties: ['Card style': on]) { + container cardWrap (class: 'card', designproperties: ('Card style': on)) { container cardBody (class: 'card-body') { slot content -- caller's widgets are spliced in here } diff --git a/.claude/skills/mendix/manage-navigation/SKILL.md b/.claude/skills/mendix/manage-navigation/SKILL.md index 07cb796d95..4c68c074bf 100644 --- a/.claude/skills/mendix/manage-navigation/SKILL.md +++ b/.claude/skills/mendix/manage-navigation/SKILL.md @@ -31,7 +31,7 @@ Use when the user asks to: - **Offline Profiles** — An offline profile makes the app work without a connection (a PWA with a local database that syncs). It is not a different document — same properties as its online twin — but it **constrains every page it can reach**, see below. - **Home Page** — The default page shown after login. Can be a PAGE or MICROFLOW. - **Role-Based Home Pages** — Override the default home page per user role (e.g., admins see a dashboard, users see a task list). -- **Menu Items** — Hierarchical menu tree. Each item has a caption and optionally targets a PAGE or MICROFLOW. Sub-menus nest with `menu 'caption' (...)`. +- **Menu Items** — Hierarchical menu tree. Each item has a caption and optionally targets a PAGE or MICROFLOW. Sub-menus nest with `menu 'caption' { ... }`. - **Menu Documents** — A *separate* document type (`Menus$MenuDocument`) holding a reusable menu that a menu widget points at, e.g. Atlas_Core's `Phone_Menu`. Not the same thing as a profile's menu, though both are built from the same items, so the item syntax is identical. Managed with `create/describe/drop menu` — see below. - **Login Page** — Custom login page (optional; Mendix provides a default). - **Not-Found Page** — Custom 404 page (optional). @@ -97,25 +97,29 @@ roles called `Administrator` in three modules. List the real ones with ### Full Menu Tree -The `menu (...)` block replaces the entire menu. Use `menu item` for leaf items and `menu 'caption' (...)` for sub-menus: +The menu items are the profile's children, in `{ ... }` after its clauses, like a page's widgets — no `;` between them. The block replaces the entire menu. Use `menu item 'Caption' ( OnClick: … )` for leaf items and `menu 'caption' { ... }` for sub-menus. The action is `OnClick:` in the words a page action uses: `show page M.P`, `call microflow M.F`, or `sign out`: ```sql create or replace navigation Responsive home page MyModule.Home_Web login page Administration.Login - menu ( - menu item 'Home' page MyModule.Home_Web; - menu 'Orders' ( - menu item 'All Orders' page Orders.Order_Overview; - menu item 'New Order' page Orders.Order_New; - ); - menu 'Admin' ( - menu item 'Users' page Administration.Account_Overview; - menu item 'Run Report' microflow Reports.ACT_GenerateReport; - ); - ); + { + menu item 'Home' ( OnClick: show page MyModule.Home_Web ) + menu 'Orders' { + menu item 'All Orders' ( OnClick: show page Orders.Order_Overview ) + menu item 'New Order' ( OnClick: show page Orders.Order_New ) + } + menu 'Admin' { + menu item 'Users' ( OnClick: show page Administration.Account_Overview ) + menu item 'Run Report' ( OnClick: call microflow Reports.ACT_GenerateReport ) + } + }; ``` +The old spelling — `menu ( menu item 'Home' page M.Home; menu 'Admin' ( … ); )`, +the action and icon as clauses and `;` after each item — still parses and warns +(MDL-DEPR121, MDL-DEPR122); `mxcli fmt --upgrade` rewrites it. + ### Menu Icons **Give every menu item an icon.** It is optional in the grammar and `mxcli check` @@ -125,14 +129,15 @@ shows its icon, and one without falls back to the first few characters of its caption — rarely enough to tell `Orders` from `Order lines`. Nothing else catches it. The model builds, `mx check` passes, and the menu is simply hard to use. -Both `menu item` and `menu 'caption' (...)` take an `icon`, in one of three -forms — Mendix stores three different icon **elements**, not three spellings of -one value: +Both `menu item` and `menu 'caption' { ... }` take an `Icon:` in their property +list, in one of three forms — Mendix stores three different icon **elements**, not +three spellings of one value: ```sql -menu item 'Home' page M.Home icon Atlas_Core.Atlas.home; -- icon collection -menu item 'Close' page M.Close icon glyph 57377; -- numeric glyph code -menu item 'Logo' page M.Logo icon image M.Images.logo; -- image collection +menu item 'Home' ( OnClick: show page M.Home, Icon: Atlas_Core.Atlas.home ) -- icon collection +menu item 'Close' ( OnClick: show page M.Close, Icon: glyph 57377 ) -- numeric glyph code +menu item 'Logo' ( OnClick: show page M.Logo, Icon: image M.Images.logo ) -- image collection +menu 'Admin' ( Icon: Atlas_Core.Atlas.user ) { … } -- a sub-menu's icon ``` The **bare** form is the icon-collection icon and is what you normally want. Use @@ -148,7 +153,7 @@ not the cause. `mxcli check` now warns (**MDL078**) against the 247 codes the shipped font defines, but a glyph is still an unchecked number where an icon collection reference is a resolved model reference. Browse the codes with `show glyphs` (`show glyphs like 'star'` searches by name, `describe glyph 57350` goes the -other way), or use `icon Atlas_Core.Atlas.` and list the names with +other way), or use `Icon: Atlas_Core.Atlas.` and list the names with `describe icon collection Atlas_Core.Atlas`. The icon-collection form is a **qualified name** — a model reference, written @@ -157,13 +162,13 @@ like every other reference in MDL, not a string: ```sql create or replace navigation Responsive home page MyModule.Home_Web - menu ( - menu item 'Home' page MyModule.Home_Web icon Atlas_Core.Atlas.home; - menu 'Orders' icon Atlas_Core.Atlas."shopping-cart" ( - menu item 'All Orders' page Orders.Order_Overview - icon Atlas_Core.Atlas."list-bullets"; - ); - ); + { + menu item 'Home' ( OnClick: show page MyModule.Home_Web, Icon: Atlas_Core.Atlas.home ) + menu 'Orders' ( Icon: Atlas_Core.Atlas."shopping-cart" ) { + menu item 'All Orders' ( OnClick: show page Orders.Order_Overview, + Icon: Atlas_Core.Atlas."list-bullets" ) + } + }; ``` **Hyphenated names are double-quoted** (`Atlas_Core.Atlas."align-center"`) — @@ -182,11 +187,11 @@ describe icon collection Atlas_Core.Atlas; *glyph* icon (a numeric character code) or an *image* icon (pointing into an image collection). Those are different elements with different fields, so MDL does not write them — and `describe navigation` reports them as a comment rather -than emitting an `icon` clause that would silently convert one into the other on +than emitting an `Icon:` that would silently convert one into the other on replay: ``` -menu item 'Close' page MyModule.Close; +menu item 'Close' ( OnClick: show page MyModule.Close ) -- icon System.Images.Close (Forms$ImageIcon) is not reproducible by CREATE NAVIGATION; set it in Studio Pro ``` @@ -239,7 +244,7 @@ doubled — and a stored constraint already carries Mendix's own escaping, so th two compose into runs of six quotes. `describe navigation` emits the bracket form. This is the general problem tracked as `mendixlabs/mxcli#750`. -**The block replaces the stored list**, the way `menu (...)` replaces the menu. +**The block replaces the stored list**, the way the `{ ... }` menu block replaces the menu. Omitting it leaves the stored configuration alone. **Ask the catalog which entities sync, rather than reading the profile.** @@ -280,12 +285,12 @@ silently dropped. ### Clear the Menu -An empty `menu ()` block removes all menu items: +An empty `{ }` menu block removes all menu items: ```sql create or replace navigation Responsive home page MyModule.Home_Web - menu (); + {}; ``` ### Not-Found Page @@ -317,10 +322,10 @@ describe navigation Responsive; create or replace navigation Responsive home page MyModule.Home_Web login page Administration.Login - menu ( - menu item 'Home' page MyModule.Home_Web; - menu item 'New Feature' page MyModule.NewFeature; - ); + { + menu item 'Home' ( OnClick: show page MyModule.Home_Web ) + menu item 'New Feature' ( OnClick: show page MyModule.NewFeature ) + }; -- Step 3: Verify describe navigation Responsive; @@ -370,9 +375,9 @@ create page MyModule.Home_Web -- Configure navigation create or replace navigation Responsive home page MyModule.Home_Web - menu ( - menu item 'Home' page MyModule.Home_Web; - ); + { + menu item 'Home' ( OnClick: show page MyModule.Home_Web ) + }; ``` ### Adding a New Page to Navigation @@ -387,13 +392,13 @@ describe navigation Responsive; create or replace navigation Responsive home page MyModule.Home_Web login page Administration.Login - menu ( - menu item 'Home' page MyModule.Home_Web; - menu item 'Customers' page MyModule.Customer_Overview; -- new - menu 'Admin' ( - menu item 'Users' page Administration.Account_Overview; - ); - ); + { + menu item 'Home' ( OnClick: show page MyModule.Home_Web ) + menu item 'Customers' ( OnClick: show page MyModule.Customer_Overview ) -- new + menu 'Admin' { + menu item 'Users' ( OnClick: show page Administration.Account_Overview ) + } + }; ``` ## Menu Documents (standalone, reusable) @@ -410,17 +415,17 @@ show navigation menu; -- the menu inside each profile describe menu Atlas_Core.Phone_Menu; -- a standalone menu document ``` -Menu documents use the same item syntax as the profile `menu (...)` block: +Menu documents use the same item syntax as the profile's `{ ... }` menu block: ```sql -create or modify menu MyModule.Main_Menu ( - menu item 'Home' page MyModule.Home_Web icon Atlas_Core.Atlas.home; - menu item 'Run' microflow MyModule.DoThing; - menu 'Admin' ( - menu item 'Accounts' page Administration.Account_Overview; - ); - menu item 'Plain'; -); +create or modify menu MyModule.Main_Menu { + menu item 'Home' ( OnClick: show page MyModule.Home_Web, Icon: Atlas_Core.Atlas.home ) + menu item 'Run' ( OnClick: call microflow MyModule.DoThing ) + menu 'Admin' { + menu item 'Accounts' ( OnClick: show page Administration.Account_Overview ) + } + menu item 'Plain' +}; drop menu MyModule.Main_Menu; ``` @@ -454,9 +459,9 @@ An offline profile is created the same way as any other: ```sql create or replace navigation TabletOffline home page Maintenance.Request_Overview - menu ( - menu item 'Requests' page Maintenance.Request_Overview; - ); + { + menu item 'Requests' ( OnClick: show page Maintenance.Request_Overview ) + }; ``` Two things about it are worth knowing before you do. @@ -503,9 +508,9 @@ stored. MDL does not author per-entity sync modes — set those in Studio Pro. - [ ] For an **offline** profile, no page it can reach binds an attribute across more than one association (CE6206) - [ ] All PAGE/MICROFLOW targets are fully qualified (`Module.Name`) - [ ] Role references in `for` clauses are fully qualified (`Module.Role`) -- [ ] Every `menu item` and `menu 'caption' (...)` ends with `;` -- [ ] Sub-menu items are wrapped in `menu 'caption' ( ... );` -- [ ] `icon` is a qualified name (not a string); hyphenated segments are double-quoted +- [ ] Menu items are in `{ }` with no `;` between them; sub-menu items in `menu 'caption' { ... }` +- [ ] A menu item's action is `( OnClick: show page M.P )`, `call microflow M.F` or `sign out` +- [ ] `Icon:` is a qualified name (not a string); hyphenated segments are double-quoted - [ ] The icon exists — check with `describe icon collection Module.Name`, do not guess - [ ] Use `describe navigation` to verify changes after applying - [ ] For a **menu document**, confirm you want `create menu` and not a profile menu — `show navigation menu` vs `describe menu` tells them apart @@ -517,4 +522,4 @@ an offline navigation profile downloads **nothing** until each entity has a sync ## Menu documents (CREATE OR MODIFY/DESCRIBE/DROP MENU) -standalone `Menus$MenuDocument`, the reusable menu a menu widget points at (Atlas_Core's `Phone_Menu`/`Tablet_Menu`) — **not** the menu inside a navigation profile, though both are built from the same items, so the item syntax is shared with `CREATE NAVIGATION`'s `MENU (...)` block. DESCRIBE is round-trippable. Written through gen+codec, which is load-bearing: Studio Pro's menu documents carry typed-array marker **3** on the item collection and each item's sub-items (the codec default), while the navigation writers hand-build items with marker **1** — unverified whether that is a latent navigation bug or a real difference, so navigation is left alone. Authoring is modelsdk-only; legacy refuses. Two traps: a menu item cannot open a page with required parameters (**CE1571**), and only `Forms$IconCollectionIcon` round-trips (glyph/image icons are flagged by DESCRIBE, not dropped silently) +standalone `Menus$MenuDocument`, the reusable menu a menu widget points at (Atlas_Core's `Phone_Menu`/`Tablet_Menu`) — **not** the menu inside a navigation profile, though both are built from the same items, so the item syntax is shared with `CREATE NAVIGATION`'s `{ ... }` menu block. DESCRIBE is round-trippable. Written through gen+codec, which is load-bearing: Studio Pro's menu documents carry typed-array marker **3** on the item collection and each item's sub-items (the codec default), while the navigation writers hand-build items with marker **1** — unverified whether that is a latent navigation bug or a real difference, so navigation is left alone. Authoring is modelsdk-only; legacy refuses. Two traps: a menu item cannot open a page with required parameters (**CE1571**), and only `Forms$IconCollectionIcon` round-trips (glyph/image icons are flagged by DESCRIBE, not dropped silently) diff --git a/.claude/skills/mendix/manage-security/SKILL.md b/.claude/skills/mendix/manage-security/SKILL.md index 8a0df57354..57e3cfa0ac 100644 --- a/.claude/skills/mendix/manage-security/SKILL.md +++ b/.claude/skills/mendix/manage-security/SKILL.md @@ -385,10 +385,22 @@ grant read (EmployeeNo) on entity Docs.Employee to Docs.Viewer; -- not Name/Bl ```sql -- Create with module roles -create user role RegularUser (MyModule.User, OtherModule.Reader); +create user role RegularUser ( ModuleRoles: (MyModule.User, OtherModule.Reader) ); -- Create with manage all roles permission -create user role SuperAdmin (MyModule.Admin) manage all roles; +create user role SuperAdmin ( ModuleRoles: (MyModule.Admin), ManageAllRoles: true ); + +-- Every property is optional: Description, CheckSecurity, ManageableRoles, +-- ManageUsersWithoutRoles. `create user role Guest;` has no module roles. +-- `create or modify` adds the listed module roles and sets only the stated +-- properties. The positional `create user role R (M.A) manage all roles` +-- is deprecated (MDL-DEPR710); `mxcli fmt --upgrade` rewrites it. +create or modify user role Manager ( + ModuleRoles: (MyModule.Manager), + Description: 'Approves orders', + ManageableRoles: (RegularUser), + CheckSecurity: true +); -- Add/drop module roles alter user role RegularUser add module roles (MyModule.Viewer); @@ -420,7 +432,7 @@ the important half: **whatever that role can read is the app's public surface.** ```sql -- The role anonymous visitors are given. System.User is what lets an -- unauthenticated session exist at all. -create user role Anonymous (Shop.Viewer, System.User); +create user role Anonymous ( ModuleRoles: (Shop.Viewer, System.User) ); alter app security guest access on role Anonymous; @@ -502,8 +514,8 @@ grant view on page Shop.Customer_Overview to Shop.User, Shop.Admin, Shop.Viewer; grant view on page Shop.Customer_Edit to Shop.User, Shop.Admin; -- 5. Create user roles (project-level) -create user role AppUser (Shop.User); -create user role AppAdmin (Shop.Admin) manage all roles; +create user role AppUser ( ModuleRoles: (Shop.User) ); +create user role AppAdmin ( ModuleRoles: (Shop.Admin), ManageAllRoles: true ); -- 6. Verify describe security matrix in Shop; diff --git a/.claude/skills/mendix/master-detail-pages/SKILL.md b/.claude/skills/mendix/master-detail-pages/SKILL.md index f60378fefc..de5266670d 100644 --- a/.claude/skills/mendix/master-detail-pages/SKILL.md +++ b/.claude/skills/mendix/master-detail-pages/SKILL.md @@ -28,7 +28,7 @@ create page Module.Entity_MasterDetail column (desktopwidth: 4) { gallery entityList (datasource: database Module.Entity, selection: single) { template { - dynamictext name (content: '{1}', contentparams: [{1} = Name], rendermode: H4) + dynamictext name (content: '{1}', contentparams: ({1} = Name), rendermode: H4) } } } @@ -59,7 +59,7 @@ gallery widgetName ( ) { template template1 { -- Widgets for each item - dynamictext name (content: '{1}', contentparams: [{1} = AttrName], rendermode: H4) + dynamictext name (content: '{1}', contentparams: ({1} = AttrName), rendermode: H4) } } ``` @@ -110,8 +110,8 @@ create page CRM.Customer_MasterDetail dynamictext heading (content: 'Customers', rendermode: H3) gallery customerList (datasource: database from CRM.Customer sort by Name asc, selection: single) { template { - dynamictext name (content: '{1}', contentparams: [{1} = Name], rendermode: H4) - dynamictext email (content: '{1}', contentparams: [{1} = Email]) + dynamictext name (content: '{1}', contentparams: ({1} = Name), rendermode: H4) + dynamictext email (content: '{1}', contentparams: ({1} = Email)) } } } @@ -154,8 +154,8 @@ The selection binding uses widget names to connect: Inside Gallery templates, use `contentparams` to reference current item attributes: ```sql template template1 { - dynamictext name (content: '{1}', contentparams: [{1} = Name], rendermode: H4) - dynamictext email (content: '{1}', contentparams: [{1} = Email]) + dynamictext name (content: '{1}', contentparams: ({1} = Name), rendermode: H4) + dynamictext email (content: '{1}', contentparams: ({1} = Email)) } ``` @@ -174,7 +174,7 @@ template template1 { | Attribute binding | `attribute: attributename` | | Action binding | `action: save changes` | | Button style | `buttonstyle: success` | -| Text content | `content: 'text'` with `contentparams: [{1} = attr]` | +| Text content | `content: 'text'` with `contentparams: ({1} = attr)` | | Render mode | `rendermode: H4` | | Template content | `template template1 { ... }` | diff --git a/.claude/skills/mendix/migrate-design-prototype/SKILL.md b/.claude/skills/mendix/migrate-design-prototype/SKILL.md index d270f7a2ed..6e82e38d2c 100644 --- a/.claude/skills/mendix/migrate-design-prototype/SKILL.md +++ b/.claude/skills/mendix/migrate-design-prototype/SKILL.md @@ -57,7 +57,7 @@ gives you *before* hand-writing custom SCSS. In order of preference: 2. **Atlas utility classes and typed design properties** — `class:'card'`, `class:'btn btn-primary'`, `spacing-inner-*`/`spacing-outer-*` for padding/margin, `flex-row`/`flex-column` + `align-x-*`/`align-y-*` for layout (no `layoutgrid` needed); or the typed equivalents - `designproperties: ['Card style': on]`, `['Background color': 'Brand Primary']`, + `designproperties: ('Card style': on)`, `['Background color': 'Brand Primary']`, `['Spacing': ['margin-bottom': 'L']]`. `mxcli check -p` validates design-property keys and values (MDL-WIDGET11/12) and lists the allowed values. 3. **Brand-token retune** — build the palette with `mxcli theme create --from` @@ -485,7 +485,7 @@ create or replace page ResourceScheduling.ResourceHeatmap ( Class: 'ss-panel ss-heat-lv' ) { container heatRow (Class: 'ss-heat-row') { - dynamictext hc01 (Content: '{1}', ContentParams: [{1} = M01], Class: 'ss-heat-cell') + dynamictext hc01 (Content: '{1}', ContentParams: ({1} = M01), Class: 'ss-heat-cell') } } } @@ -646,10 +646,10 @@ the fast index so a design migration doesn't rediscover them. children bind to the related entity: ``` dataview dvEmp (datasource: $currentObject/Module.Entity_Related) { - dynamictext n (content: 'By {1}', contentparams: [{1} = Name]) -- own attr of the related entity + dynamictext n (content: 'By {1}', contentparams: ({1} = Name)) -- own attr of the related entity } ``` - - An **association path** for a single inline value: `contentparams: [{1} = Entity_Related/Name]` + - An **association path** for a single inline value: `contentparams: ({1} = Entity_Related/Name)` in a `dynamictext`, or `attribute: Entity_Related/Name` on a DataGrid2 column — both persist as an AttributeRef over the association (use a **bare** association name; a module-qualified one is rejected on a column). @@ -690,7 +690,7 @@ the fast index so a design migration doesn't rediscover them. - [ ] Persistent sidebar/topbar built as **navigation profile + layout + CSS**, not per-page widgets; new screens added via `CREATE OR REPLACE NAVIGATION` - [ ] Shell **restructured** to the design (full-height sidebar via `position:fixed` + `margin-left` offset; region sizes forced with `flex-basis`; collapse toggle hidden if unused) - [ ] Multi-part chrome (brand block, user chip) rendered as **inline-SVG backgrounds**; single-style chrome (labels, dots, footer) as plain `::before`/`::after` -- [ ] Related (to-one) object attributes shown via a **nested "data from context" DataView** (full read view) or an **association path** (`contentparams: [{1} = Assoc/Attr]` / column `attribute: Assoc/Attr`) for a single inline value — both persist +- [ ] Related (to-one) object attributes shown via a **nested "data from context" DataView** (full read view) or an **association path** (`contentparams: ({1} = Assoc/Attr)` / column `attribute: Assoc/Attr`) for a single inline value — both persist - [ ] A real **`docker build`** run after each slice (not just `mxcli check`) before screenshotting - [ ] Widgets styled with `Class:`; data-driven state via `DynamicClasses:` - [ ] No inline `Style:` on any DYNAMICTEXT diff --git a/.claude/skills/mendix/migrate-k2-nintex/SKILL.md b/.claude/skills/mendix/migrate-k2-nintex/SKILL.md index 0da04733e2..720698e619 100644 --- a/.claude/skills/mendix/migrate-k2-nintex/SKILL.md +++ b/.claude/skills/mendix/migrate-k2-nintex/SKILL.md @@ -180,7 +180,7 @@ SmartForms Views map to Mendix pages/snippets. The rules system (event-driven, " ```sql create page CRM.Customer_Edit ( - params: { $Customer: CRM.Customer }, + params: ( $Customer: CRM.Customer ), title: 'Edit Customer', layout: Atlas_Core.PopupLayout ) diff --git a/.claude/skills/mendix/overview-pages/SKILL.md b/.claude/skills/mendix/overview-pages/SKILL.md index 3faef3acb5..9a2a11debf 100644 --- a/.claude/skills/mendix/overview-pages/SKILL.md +++ b/.claude/skills/mendix/overview-pages/SKILL.md @@ -41,7 +41,7 @@ create snippet Module.Entity_Menu ```sql create [or replace] snippet Module.SnippetName [( - params: { $ParamName: Module.EntityType } + params: ( $ParamName: Module.EntityType ) )] [folder 'path'] { @@ -96,7 +96,7 @@ create page Module.Entity_Overview datasource: database Module.Entity, selection: Multi, PagingPosition: both, - designproperties: ['Compact': on, 'Hover': on, 'Striped': on] + designproperties: ('Compact': on, 'Hover': on, 'Striped': on) ) { column (attribute: Name, caption: 'Name') { textfilter textFilter1 @@ -124,7 +124,7 @@ Include a snippet in a page using SNIPPETCALL: snippetcall widgetName (snippet: Module.SnippetName) -- With parameters (for parameterized snippets): -snippetcall widgetName (snippet: Module.SnippetName, params: {Customer: $Customer}) +snippetcall widgetName (snippet: Module.SnippetName, params: (Customer = $Customer)) ``` ### Overview Page Components @@ -141,7 +141,7 @@ datagrid GridName ( datasource: database from Module.Entity where [IsActive = true] sort by Name asc, selection: Multi, PagingPosition: both, - designproperties: ['Compact': on, 'Hover': on, 'Striped': on] + designproperties: ('Compact': on, 'Hover': on, 'Striped': on) ) { column (attribute: Name, caption: 'Name') { textfilter textFilter1 @@ -163,7 +163,7 @@ datagrid GridName ( thing (mendixlabs/mxcli#1152) - `selection: Multi` - Multi-selection (`Multi`, `Single`, or omit for none) - `PagingPosition: both` - Pagination bar position (`top`, `bottom`, `both`) -- `designproperties: ['Compact': on, 'Hover': on, 'Striped': on]` - Atlas design tokens +- `designproperties: ('Compact': on, 'Hover': on, 'Striped': on)` - Atlas design tokens **Column Types:** - `column (attribute: attribute, caption: 'label')` - Attribute column (own-entity attribute) @@ -236,7 +236,7 @@ Form for creating or editing a single entity. **Requires a page parameter** to r ```sql create page Module.Entity_NewEdit ( - params: { $entity: Module.Entity }, + params: ( $entity: Module.Entity ), title: 'Edit Entity', layout: Atlas_Core.PopupLayout, folder: 'OverviewPages' @@ -268,7 +268,7 @@ create page Module.Entity_NewEdit ```sql create page Module.PageName ( - params: { $ParamName: Module.EntityName }, + params: ( $ParamName: Module.EntityName ), title: '...', layout: ... ) @@ -281,7 +281,7 @@ create page Module.PageName ### NewEdit Page Components -1. **Page Parameter**: `params: { $entity: Module.Entity }` - Receives the object to edit +1. **Page Parameter**: `params: ( $entity: Module.Entity )` - Receives the object to edit 2. **Layout**: `Atlas_Core.PopupLayout` - Popup/modal style 3. **DataView**: Container bound to page parameter (`datasource: $entity`) 4. **Input Widgets**: Match entity attributes with `attribute:` property @@ -354,7 +354,7 @@ create page MdlTemplates.Store_Overview ```sql create page MdlTemplates.Store_NewEdit ( - params: { $store: MdlTemplates.Store }, + params: ( $store: MdlTemplates.Store ), title: 'Edit Store', layout: Atlas_Core.PopupLayout, folder: 'OverviewPages' @@ -407,7 +407,7 @@ Shows various input widget types: ```sql create page MdlTemplates.Car_NewEdit ( - params: { $Car: MdlTemplates.Car }, + params: ( $Car: MdlTemplates.Car ), title: 'Edit Car', layout: Atlas_Core.PopupLayout, folder: 'OverviewPages' @@ -498,7 +498,7 @@ module/ ## Parameterized Snippets Snippets can accept parameters to display context-specific data. **A snippet -parameter must be an entity.** A primitive one (`params: { $Label: String }`) is +parameter must be an entity.** A primitive one (`params: ( $Label: String )`) is refused as **MDL087**, because Mendix rejects it with **CE0046** *"Invalid data type 'String'."* — a *page* parameter may be a primitive, a snippet parameter may not. To parameterise a snippet on a value, keep the primitive on the calling @@ -508,7 +508,7 @@ page's parameters, or pass an object and read the member inside the snippet. -- Create a snippet with a parameter create snippet Module.CustomerDetails ( - params: { $Customer: Module.Customer } + params: ( $Customer: Module.Customer ) ) { layoutgrid detailsGrid { @@ -521,7 +521,7 @@ create snippet Module.CustomerDetails } -- Use the snippet with parameter passing -snippetcall customerDetails (snippet: Module.CustomerDetails, params: {Customer: $Customer}) +snippetcall customerDetails (snippet: Module.CustomerDetails, params: (Customer = $Customer)) ``` ## Entity Menu Snippets with NavigationList @@ -531,7 +531,7 @@ For entity-specific action menus (Edit, Delete, etc.), use the `navigationlist` ```sql create snippet Module.Entity_Menu ( - params: { $EntityParameter: Module.Entity } + params: ( $EntityParameter: Module.Entity ) ) { navigationlist EntityMenuNav { @@ -604,7 +604,7 @@ create snippet Module.NavigationMenu -- Step 2: Create all pages (they reference the snippet via SNIPPETCALL) create page Module.Customer_NewEdit ( - params: { $Customer: Module.Customer }, + params: ( $Customer: Module.Customer ), title: 'Edit Customer', layout: Atlas_Core.PopupLayout ) diff --git a/.claude/skills/mendix/resolve-forward-references/SKILL.md b/.claude/skills/mendix/resolve-forward-references/SKILL.md index a57a1111ec..3542a473d8 100644 --- a/.claude/skills/mendix/resolve-forward-references/SKILL.md +++ b/.claude/skills/mendix/resolve-forward-references/SKILL.md @@ -133,7 +133,7 @@ page must exist before the overview can reference it. create page MyModule.Customer_NewEdit ( - params: { $Customer: MyModule.Customer }, + params: ( $Customer: MyModule.Customer ), title: 'Edit Customer', layout: Atlas_Core.PopupLayout ) @@ -181,7 +181,7 @@ For simple cases, reordering declarations is sufficient and no placeholder is ne -- Page first create page MyModule.Order_Detail ( - params: { $Order: MyModule.Order }, + params: ( $Order: MyModule.Order ), title: 'Order Detail', layout: Atlas_Core.Atlas_Default ) @@ -267,7 +267,7 @@ create snippet MyModule.AppNav -- 2. NewEdit page (referenced by Overview's New button) create page MyModule.Customer_NewEdit ( - params: { $Customer: MyModule.Customer }, + params: ( $Customer: MyModule.Customer ), title: 'Edit Customer', layout: Atlas_Core.PopupLayout ) diff --git a/.claude/skills/mendix/rest-client/SKILL.md b/.claude/skills/mendix/rest-client/SKILL.md index f42788c556..88ccd6542a 100644 --- a/.claude/skills/mendix/rest-client/SKILL.md +++ b/.claude/skills/mendix/rest-client/SKILL.md @@ -85,7 +85,7 @@ create consumed rest service Module.OpenMeteoAPI ( method: get, path: '/forecast', query: ($latitude: decimal, $longitude: decimal, $current: string), - headers: ('Accept' = 'application/json'), + headers: ('Accept': 'application/json'), timeout: 30, response: json as $WeatherJson ) @@ -93,13 +93,24 @@ create consumed rest service Module.OpenMeteoAPI ( operation PostData ( method: post, path: '/submit', - headers: ('Content-Type' = 'application/json'), + headers: ('Content-Type': 'application/json'), body: json from $JsonPayload, response: none ) }; ``` +A header value is a template, like the path: `{Name}` is the operation +parameter `Name`, which must be declared in `Parameters:` (CE7056 otherwise). + +```sql +parameters: ($Token: string), +headers: ('Authorization' = 'Bearer {Token}') +``` + +`'Bearer ' + $Token` is the old spelling of the same header (MDL-DEPR711); +`mxcli fmt --upgrade` rewrites it. It used to store only `Bearer `. + ### Authentication ```sql @@ -421,7 +432,7 @@ create consumed rest service Module.WeatherAPI ( method: get, path: '/forecast', query: ($latitude: decimal, $longitude: decimal, $current: string), - headers: ('Accept' = 'application/json'), + headers: ('Accept': 'application/json'), response: json as $Result ) }; diff --git a/.claude/skills/mendix/theme-styling/SKILL.md b/.claude/skills/mendix/theme-styling/SKILL.md index aaf19b3556..b8164731e0 100644 --- a/.claude/skills/mendix/theme-styling/SKILL.md +++ b/.claude/skills/mendix/theme-styling/SKILL.md @@ -367,10 +367,10 @@ Two surprises when styling a **DataGrid2** matrix/pivot (ledger finding #46): Keys must match the `name` field in `design-properties.json` exactly: ```sql -- CORRECT -designproperties: ['Spacing top': 'Large'] +designproperties: ('Spacing top': 'Large') -- WRONG (case mismatch — silently ignored) -designproperties: ['spacing top': 'Large'] +designproperties: ('spacing top': 'Large') ``` ### Compound (Nested) Design Properties @@ -381,11 +381,11 @@ one whose value is itself a set of sub-properties (e.g. Atlas's `Spacing` → `margin-top`, `margin-bottom`, …). A compound value is written as a nested list: ```sql -designproperties: [ +designproperties: ( 'Column gap': 'Medium', -- flat option 'Cards style': ON, -- flat toggle - 'Spacing': ['margin-top': 'Large', 'margin-bottom': 'Medium'] -- compound -] + 'Spacing': ('margin-top': 'Large', 'margin-bottom': 'Medium') -- compound +) ``` Supported on the **modelsdk** (`.mpr`) and **MCP** (live Studio Pro) backends. diff --git a/.claude/skills/mendix/write-layouts/SKILL.md b/.claude/skills/mendix/write-layouts/SKILL.md index 2e17bccd5a..401963067b 100644 --- a/.claude/skills/mendix/write-layouts/SKILL.md +++ b/.claude/skills/mendix/write-layouts/SKILL.md @@ -160,9 +160,9 @@ instead of its own. The fix is a phone layout of your own whose bottom bar names your own menu document, not an edit to Atlas's layout: ```sql -create or modify menu MyModule.Phone_Menu ( - menu item 'Home' page MyModule.Home_Phone icon Atlas_Core.Atlas.home; -); +create or modify menu MyModule.Phone_Menu { + menu item 'Home' ( OnClick: show page MyModule.Home_Phone, Icon: Atlas_Core.Atlas.home ) +}; create or replace layout MyModule.Phone_Bottom ( layouttype: 'Phone', class: 'layout-atlas layout-atlas-phone' ) { diff --git a/.claude/skills/mendix/write-microflows/reference/control-flow.md b/.claude/skills/mendix/write-microflows/reference/control-flow.md index 563de70482..dedcf79da8 100644 --- a/.claude/skills/mendix/write-microflows/reference/control-flow.md +++ b/.claude/skills/mendix/write-microflows/reference/control-flow.md @@ -311,6 +311,7 @@ commit $Product; - **Leave `@position` out unless you are reproducing a hand-made diagram.** Without it the builder lays the flow out itself: the main line wraps onto rows past two canvas widths, a guard's branch drops into the lane below while the main line carries on above it, and a `case` of four or more branches leaves the decision in three groups so its lines do not cross. A statement with `@position` is never moved and is not measured against what is placed around it, so a few hand-placed statements in an otherwise automatic flow are what produces overlaps (mendixlabs/mxcli#1154) - `@position` always appears in DESCRIBE output; `@caption` only when custom; `@color` only when not Default - DESCRIBE MICROFLOW shows `@` annotations before their activities +- `@anchor(from: X)` is the side the flow **leaving** a statement starts from. On an `if` that is the flow out of its closing merge (the merge has no statement of its own, just as its position rides on the if as `@merge`); the if's `to:` is its incoming flow and `true:` / `false:` its branches. `@anchor(from: bottom, to: top, …)` on an if is how DESCRIBE writes a decision whose merge drops onto the next row (#767) - `@start(x, y)` positions the **start event** and goes on the first statement, because the start has no statement of its own. Omit it and the start is derived — one spacing unit (160) left of the first activity, on its centre line — and a rewrite re-derives it so the start follows the activities when they move. A start that is not at the derived spot was placed by hand (in Studio Pro or with `@start`): it survives a rewrite that does not mention it, and DESCRIBE emits `@start` for it. An explicit `@start` overrides both (#951) - `@position(x, y)` on a **parameter** goes inside the parameter list, ahead of the parameter it places — a parameter is a stored node with its own coordinates, and this is the only annotation it takes. Omit it and the parameters form a row along the top of the canvas (200;53, 300;53, …). The `@start` rule above applies unchanged: a parameter on that derived row is re-derived on a rewrite, one anywhere else was placed by hand, survives, and is emitted by DESCRIBE (#993). Before this, a hand-aligned parameter block was moved back onto the row by any rewrite — including a describe → exec of mxcli's own output: diff --git a/.claude/skills/mendix/write-workflows/SKILL.md b/.claude/skills/mendix/write-workflows/SKILL.md index aaaa351de8..ecf8184435 100644 --- a/.claude/skills/mendix/write-workflows/SKILL.md +++ b/.claude/skills/mendix/write-workflows/SKILL.md @@ -161,13 +161,26 @@ begin end workflow; ``` -> **Do NOT use `annotation '...'` in a workflow body.** It parses, but the -> annotation is written into the workflow's activity flow, which Mendix loads by +**Notes** attach to an activity with `@annotation '…'` on the line before it, +as in a microflow; the workflow's own note is the header clause +`annotation '…'`, and an event sub-process takes `@annotation` before +`event subprocess`. One note per activity; no other `@` annotation is accepted. + +```sql +create workflow Module.Approve + parameter $WorkflowContext: Module.Request + annotation 'Started from the request form' +begin + @annotation 'Escalates after two days' + user task review 'Review' page Module.Review_Task outcomes 'Done' { }; +end workflow; +``` + +> **Do NOT use a standalone `annotation '...';` statement in a workflow body.** +> It parses, but the note is written into the activity flow, which Mendix loads by > constructing every child with a `Flow` parent — no annotation type takes one, so -> the resulting `.mpr` **cannot be loaded at all**: Studio Pro will not open the -> project and `mx check` fails before validating anything. `mxcli` now refuses the -> statement (MDL-WF04) at both check and exec time. Keep the note as an MDL comment -> (`-- ...`); workflow canvas annotations are not yet writable. +> the resulting `.mpr` **cannot be loaded at all**. `mxcli` refuses it (MDL-WF04) +> at check and exec time. Attach the note to an activity with `@annotation`. **Boundary events** attach a timer to a user task / call-microflow / wait: @@ -201,8 +214,8 @@ end workflow; targets it, so it takes a **name** instead of a delay: `boundary event interrupting notification Withdrawn 'Request withdrawn' { end workflow; }`. The name is unique in the workflow. Only one interrupting boundary event per - activity, of either kind (CE6697, MDL-WF15). `alter workflow … insert boundary - event` cannot add one yet — restate the workflow. + activity, of either kind (CE6697, MDL-WF15). `alter workflow … { insert into X + { boundary event … } }` cannot add one yet — restate the workflow. - **Over MCP (`--mcp`), Studio Pro dictates how a notification path ends**, which mxbuild does not: an interrupting one ends in `end workflow;` (in `jump to` inside a parallel split), a non-interrupting one runs to its end. mxcli refuses the @@ -248,44 +261,64 @@ drop workflow Module.ApprovalFlow; ## ALTER WORKFLOW -In-place edits go through the workflow mutator — no full rewrite. Supports -`SET` properties, and `INSERT` / `DROP` / `REPLACE` of activities, outcomes, -parallel paths, decision conditions, and boundary events. Reference an activity -by its name (or an auto-named one by its caption in quotes). - -Each operation is its **own statement** — there is no `{ … }` wrapper, and `SET` -uses no `=` (`set display 'X'`, not `set display = 'X'`): +In-place edits go through the workflow mutator — no full rewrite. `alter +workflow` is the generic alter (the same shape as `alter page`): the operations +go in `{ … }`, properties are set with `set ( Key: value )`, and a fragment is +written exactly as in `create workflow`. ```sql -alter workflow Module.ApprovalFlow set display 'Updated Approval'; -alter workflow Module.ApprovalFlow set activity Review page Module.AltReviewPage; -alter workflow Module.ApprovalFlow insert after Review call microflow Module.ACT_Log; -alter workflow Module.ApprovalFlow replace activity ACT_Validate with call microflow Module.ACT_Process; +alter workflow Module.ApprovalFlow { + set (Display: 'Updated Approval', DueDate: addDays([%CurrentDateTime%], 7)); + set (Page: Module.AltReviewPage, Description: 'Check the amount') on Review; + set (Targeting: xpath [Active = true()]) on Review; + insert before Review { call microflow Module.ACT_Prepare; } + insert after Review { call microflow Module.ACT_Log; call microflow Module.ACT_Notify; } + replace ACT_Validate with { call microflow Module.ACT_Process; } + drop ObsoleteStep; +}; ``` -Consecutive `set`s may chain in one statement: -`alter workflow Module.ApprovalFlow set display 'X' set description 'Y';` - -See `mdl-examples/doctype-tests/24-workflow-examples.mdl` for the full ALTER -surface (insert path, drop path, insert condition, boundary events). - -**The INSERT op has to match the activity kind.** An activity's outcome list is -typed, and each op writes exactly one outcome type into it: - -| Op | Writes | Only on | -|----|--------|---------| -| `insert outcome '' on X { }` | `UserTaskOutcome` | a user task | -| `insert condition '' on X { }` | `…ConditionOutcome` | a decision, a call microflow | -| `insert path on X { }` | `ParallelSplitOutcome` | a parallel split | -| `insert boundary event on X interrupting timer { }` | a boundary event | user task, call microflow, call workflow, wait for notification | +**Addressing an activity.** A target is the activity's **name** (`Review` — +`describe workflow` prints every name) or its **caption** in quotes +(`'Review the request'`); add `@n` to choose one of several matches. A name wins +over a caption that repeats it. An ambiguous target is refused, and the error +lists the matches (`@1 user task Review, @2 decision Review`) — mxcli never +guesses. Every target is resolved before anything changes, so a refused +statement leaves the workflow untouched. The flow's start activity (`start1`, +caption `'Start'`) is addressable too, but nothing goes before it — `insert +before start1` is refused (it would be `CE9526`); use `insert after start1`. + +Workflow keys: `Display`, `Description`, `ExportLevel`, `DueDate`, +`OverviewPage`, `Parameter: $WorkflowContext: Module.Entity`. Activity keys +(with `on `): `Page`, `Description`, `Targeting: microflow M.F` / +`Targeting: xpath [ … ]`, `DueDate`. + +**Adding to an activity: `insert into`.** What goes in the braces is the +activity's own clause, as `create workflow` writes it — and it has to match the +activity kind, because an activity's outcome list is typed: + +| Fragment | Writes | Only on | +|----------|--------|---------| +| `insert into X { outcomes '' { … } }` | `UserTaskOutcome` | a user task | +| `insert into X { outcomes '' -> { … } }` (or `true`, `false`, `default`) | `…ConditionOutcome` | a decision, a call microflow | +| `insert into X { path { … } }` (`path n` must be the next number) | `ParallelSplitOutcome` | a parallel split | +| `insert into X { boundary event interrupting timer { … } }` | a boundary event | user task, call microflow, call workflow, wait for notification | Aim one at the wrong kind and the outcome lands in a list that cannot hold it, which is **not** a build error: the project stops **loading**, so Studio Pro will not open it and `mx check` dies before it validates anything (ako/mxcli#415). -mxcli refuses all of these now — at `check --references` and at `exec`, which -call the same function — and the refusal names the op that fits the target. The -`drop` ops are unaffected: removing a branch cannot write a wrong type, and it -leaves an ordinary build error (`CE6686`) rather than an unloadable project. +mxcli refuses all of these — at `check --references` and at `exec`, which call +the same function — and the refusal names the fragment that fits the target. + +**Removing a member: `drop X outcome 'Reject'`**, `drop Decision1 outcome true` +(`false`, `default`), `drop Split1 path 2`, `drop X boundary event`. Removing a +branch cannot write a wrong type; it leaves an ordinary build error (`CE6686`) +rather than an unloadable project. + +The old per-action statements (`alter workflow M.W set display 'X';`, `set +activity X page …`, `insert outcome 'N' on X { }`, `drop path 'Path 2' on X`) +still parse and warn `MDL-DEPR140`–`149`; `mxcli fmt --upgrade` rewrites them. +See `mdl-examples/doctype-tests/24-workflow-examples.mdl` for the full surface. ## DESCRIBE round-trip @@ -432,9 +465,9 @@ values. The full list and the System **entities** are in `system-module`. reset it.** An event sub-process and a workflow event handler subscribed to no event types are set in Studio Pro. `create or modify` on a workflow that holds any of them is refused with the - list, and so is `alter workflow … replace activity` on an activity that holds - one. Change such a workflow with `alter workflow … set activity …` (it edits - the stored document and keeps the rest) or in Studio Pro. + list, and so is `alter workflow … { replace X with { … } }` on an activity that holds + one. Change such a workflow with `alter workflow … { set ( … ) on X; }` (it + edits the stored document and keeps the rest) or in Studio Pro. - **`end workflow` ends the whole workflow from inside a branch** — the workflow counterpart of a microflow's `return`. `return;` itself is refused in a workflow diff --git a/CHANGELOG.md b/CHANGELOG.md index 40db61988d..d8108fc36c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **Session commands belong to the REPL** (ako/mxcli#755, R7) — `connect`, `disconnect`, `use`, `set = …`, `status`, `check`, `build`, `lint`, `debug`, `execute script`, `execute runtime`, `help` and `introspect api` need a session, not a model. Under the `mdl 1;` preview header a script that holds one is refused; without the header it runs as before and `check` / `exec` warn **MDL-V1-SESSION**. The REPL keeps accepting them. Put the session on the command line instead: `mxcli exec script.mdl -p app.mpr --json`. `exit` / `quit` are not session commands. - **Document types are named as Studio Pro names them** (ako/mxcli#755, R10) — `consumed rest service` (was `rest client`), `consumed odata service` (was `odata client`), `published odata service` (was `odata service`), `task queue` (was `queue`), `alter app security` (was `alter project security`) and `alter settings runtime` (was `alter settings model`), in every statement that names the type: create, alter, drop, describe, list, move, grant/revoke on a published OData service, and an external entity's `from consumed odata service`. `describe` writes the new names. **Migrating a script:** nothing breaks — the old names still parse and build the identical statement, `check` / `exec` warn **MDL-DEPR550** to **MDL-DEPR555**, and `mxcli fmt --upgrade` rewrites them. `call microflow … in queue M.Q` is a call option, not the type name, and is unchanged. +- **Menus, property maps and the database connection take R2's brackets** (ako/mxcli#754, R2) — menu items are children in `{ }` with no `;` between them, and an item's action and icon are its properties: `create or modify navigation Responsive home page Shop.Home { menu item 'Home' ( OnClick: show page Shop.Home, Icon: Atlas_Core.Atlas.home ) menu 'Admin' { menu item 'Users' ( OnClick: call microflow Shop.ShowUsers ) } };`, and the same items in `create menu M.Main { … }`. The property maps are in `( )`: a page's or snippet's `Params: ( $Order: Shop.Order )` and `Variables: ( $show: Boolean = 'true' )`, `ContentParams: ({1} = Name)` (also `CaptionParams` and `Params`), `DesignProperties: ('Spacing': ('margin-top': 'Large'), 'Full width': on)`, a snippet call's arguments `Params: (Asset = $Asset)` (bound as at every call site, R4), and a REST operation's `Headers: ('Accept': 'application/json')`. A database connection is `create database connection M.Db ( Type: 'PostgreSQL', ConnectionString: @M.Url, Username: @M.User, Password: @M.Pass ) { query Q ( Sql: $$…$$, Parameters: ( p: Integer default '0' ), Returns: M.E, Map: ( Attr = column ) ) }`. `describe` prints these forms. **Migrating:** every old spelling still parses under both language versions, builds the same statement, warns (MDL-DEPR120–127) and is rewritten by `mxcli fmt --upgrade`. + - **Integration documents put properties in `( )` and children in `{ }`; an `alter microflow` fragment is `begin … end`** (ako/mxcli#754, R2) — a consumed REST service's operations are `operation GetUser ( Method: get, Path: '/u/{id}', Response: none )`; an agent's attachments are `tool X ( … )`, `mcp service M.S ( … )` and `knowledge base K ( … )`, in `create agent` and `alter agent … add`; an image collection is `create image collection M.Icons { image Logo ( File: 'logo.png' ) }`; a message definition collection is `{ definition D for M.E { A, M.E_B/M.B { C } } }`, and an association may select no member (`M.E_B/M.B { }`, which describe already printed as the unparseable `()`); and `alter microflow` / `alter nanoflow` take `insert after begin … end;` and `replace with begin … end;`, the fragment written as the body of `create microflow` is. `describe` prints the new forms. **Migrating a script:** nothing breaks — the old brackets still parse and build the identical statement, `check` / `exec` warn **MDL-DEPR070** (operation), **MDL-DEPR071** (agent attachment), **MDL-DEPR072** (image collection), **MDL-DEPR073** (message definitions) and **MDL-DEPR074** (alter fragment), once per statement, and `mxcli fmt --upgrade` rewrites them. - **An entity grant names the rights first, and every XPath is written in `[ ]`** (ako/mxcli#753, R5) — `grant read *, write (Email), create on entity Shop.Order to Shop.User where [Status = 'Open'];` takes the word order of every other grant, and the constraint is bracketed like a datasource or retrieve `where`, so the quotes inside it are written once instead of doubled. A workflow user task's targeting is the same: `targeting users xpath [System.UserRoles = '[%UserRole_Manager%]']`, and `alter workflow … set activity … targeting xpath [ … ]`. Sibling predicate groups (`[a][b]`) are one constraint, kept as written. `describe entity` and `describe workflow` print the new forms; a stored constraint the bracketed grammar does not read (none in the Studio Pro-authored PedApp and TestApp fixtures) is printed in the old quoted form so the output stays re-executable. **Migrating a script:** nothing breaks — `grant Shop.User on Shop.Order (read *) where '[…]'` and `targeting xpath '[…]'` still parse and build the same rule, and `check` / `exec` warn **MDL-DEPR030** and **MDL-DEPR031**; `mxcli fmt --upgrade` rewrites both. A quoted value that is not a bracketed XPath is left in place and reported. - **A microflow error handler is `on error [without rollback] begin … end error`; `while` takes `begin … end while` like `loop`** (ako/mxcli#754, R2) — braces hold declarative children and imperative flow is `begin … end `, and the error handler was the only brace block inside a microflow. `describe microflow` / `describe nanoflow` now print `commit $O on error begin … end error;` (an empty handler `… on error without rollback begin end error;`), and so do the generated test flows of `mxcli test`. Under the `mdl 1;` preview header a `while` without `begin`, or ending in a bare `end`, is an error; without the header it keeps parsing and warns **MDL-V1-WHILE**, and `mxcli fmt --upgrade --header` inserts the missing words. Measured on the Studio Pro-authored PedApp and TestApp fixtures: the round-trip harness stays green. **Migrating a script:** nothing breaks — `on error { … }` still parses, builds the identical handler, and `check` / `exec` warn **MDL-DEPR540**; `mxcli fmt --upgrade` rewrites it. @@ -25,6 +27,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **`retrieve … first` binds one object; under `mdl 1;` `retrieve … limit 1` is a list of one** (ako/mxcli#734, ADR-0011) — `first` is Mendix's "First object" range in every language version, the same split as `import from mapping … first | limit n`. Under the `mdl 1;` preview header a bare `limit 1` is a Custom range, a list of one. Without the header it keeps its old meaning, the object, and `check` / `exec` warn **MDL-V1-LIMIT1**. `describe` prints the object range as `first`, so its output means the same whatever header runs it; measured on the Studio Pro-authored PedApp fixture (describe → exec keeps both ranges as stored) and with `mx check` 11.13 on a copy of a Studio Pro project (a loop over an `mdl 1` `limit 1` builds at 0 errors; the headerless script is CE0100). **MDL-RETRIEVE01** now keys on the object range the writer stores and names CE0100 for a loop. `first` on an association retrieve is refused: Mendix gives that source no range. **Migrating a script:** nothing to do — without the header every script behaves as before; replace an object-meaning `limit 1` with `first` to silence the warning and to keep the meaning under `mdl 1;`. +- **`alter workflow` is on the generic ALTER** (ako/mxcli#712, ADR-0012) — `alter workflow M.W { set (Display: 'x', DueDate: …); set (Page: M.P) on ReviewOrder; insert before|after ReviewOrder { … } insert into ReviewOrder { outcomes 'Escalate' { … } } insert into split1 { path { … } } replace X with { … } drop X, ReviewOrder outcome 'Reject', split1 path 2; }`. A target is an activity's name or 'caption' with `@n`, resolved through the workflow's target resolver on both backends before anything changes; an ambiguous one is refused with the matches listed. A fragment is written exactly as in `create workflow`, so `insert after` can add several activities, and `insert before` is new. **Migrating:** the old per-action statements (`alter workflow M.W set display 'x' insert outcome 'N' on X { };`) keep parsing and warn MDL-DEPR140–149; `mxcli fmt --upgrade` rewrites them, except `drop path ''` whose caption is not `Path n`. + - **`alter page`, `alter snippet` and `alter layout` are the first document types on the generic ALTER** (ako/mxcli#712, ADR-0012) — one grammar, `alter Module.Name { set (Key: value) on ; insert before|after|into { … } replace with { … } drop ; }`, 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. diff --git a/README.md b/README.md index 56a8b4be8d..aed53b69c0 100644 --- a/README.md +++ b/README.md @@ -432,7 +432,7 @@ create association MyModule.Order_Product -- Create pages create page MyModule.Product_Edit ( - params: { $Product: MyModule.Product }, + params: ( $Product: MyModule.Product ), title: 'Edit Product', layout: Atlas_Core.PopupLayout ) diff --git a/cmd/mxcli/cmd_new_layout.go b/cmd/mxcli/cmd_new_layout.go index 82aa3d23c7..820cfce67d 100644 --- a/cmd/mxcli/cmd_new_layout.go +++ b/cmd/mxcli/cmd_new_layout.go @@ -57,12 +57,12 @@ const scaffoldLayoutMDL = `create or replace layout {{.Module}}.{{.Layout}} ( region top (class: 'region-topbar') { container topbarContent ( class: 'topbar-content', - designproperties: [ + designproperties: ( 'Flex container': 'Horizontal (row)', 'Align items Y': 'Center', 'Disable row wrap': on, 'Grow / shrink (self)': 'Fill container (only grow)' - ] + ) ) { menubar mainMenu (profile: '{{.Profile}}') snippetcall languageSelector (snippet: Atlas_Core.LanguageSelectorWidget) diff --git a/cmd/mxcli/lsp_completion.go b/cmd/mxcli/lsp_completion.go index 0c6ea087cd..5d3f575c3a 100644 --- a/cmd/mxcli/lsp_completion.go +++ b/cmd/mxcli/lsp_completion.go @@ -477,7 +477,7 @@ func (s *mdlServer) variableCompletionItems(docText string, linePrefix string, c } } - // Extract page parameter names from CREATE PAGE ... Params: { $Name: Type, ... } + // Extract page parameter names from CREATE PAGE ... Params: ( $Name: Type, ... ) paramNames := extractPageParamNames(docText) for _, name := range paramNames { if partial == "" || strings.HasPrefix(strings.ToUpper(name), partial) { diff --git a/cmd/mxcli/syntax/features_integration.go b/cmd/mxcli/syntax/features_integration.go index ee3a1699ea..44aeb9ebc0 100644 --- a/cmd/mxcli/syntax/features_integration.go +++ b/cmd/mxcli/syntax/features_integration.go @@ -304,7 +304,7 @@ func init() { "body", "response", "mapping", "authentication", "json structure", "import mapping", "export mapping", }, - Syntax: "CREATE [OR MODIFY] CONSUMED REST SERVICE Module.Name (\n BaseUrl: 'https://...',\n Authentication: NONE | BASIC (...)\n)\n{\n OPERATION Name (\n Method: GET|POST|PUT|DELETE|PATCH,\n Path: '/path/{param}',\n Parameters: ($param: Type),\n Query: ($param: Type),\n Headers: ('Key' = 'Value'),\n Timeout: 30,\n Body: JSON FROM $var | MAPPING Entity { jsonField = Attribute, ... },\n Response: JSON AS $var | MAPPING Entity { Attribute = jsonField, ... }\n )\n};\n\n-- An operation is a child of the service, so its properties are in ( ).\n-- `OPERATION Name { ... }` is the deprecated spelling (MDL-DEPR070).\n\n-- MAPPING takes a target ENTITY plus a body listing the JSON fields; Mendix\n-- stores it inline on the operation. An existing import/export mapping\n-- document cannot be referenced here (rejected as MDL-REST01).\n-- There is no FILE request body: Mendix's consumed operation stores one of\n-- Rest$JsonBody, Rest$StringBody or Rest$ImplicitMappingBody, so a file\n-- document has nowhere to go. `Body: FILE FROM $Doc` is rejected as\n-- MDL-REST02 rather than sent as the literal text \"$Doc\" (it used to be,\n-- returning 200 with a 4-byte payload). Binary POST lives on the\n-- microflow activity: `call rest service post '' body binary $Doc/Contents`.\n-- `Response: FILE AS $Doc` is unaffected — downloads work.", + Syntax: "CREATE [OR MODIFY] CONSUMED REST SERVICE Module.Name (\n BaseUrl: 'https://...',\n Authentication: NONE | BASIC (...)\n)\n{\n OPERATION Name (\n Method: GET|POST|PUT|DELETE|PATCH,\n Path: '/path/{param}',\n Parameters: ($param: Type),\n Query: ($param: Type),\n Headers: ('Key': 'Value'),\n Timeout: 30,\n Body: JSON FROM $var | MAPPING Entity { jsonField = Attribute, ... },\n Response: JSON AS $var | MAPPING Entity { Attribute = jsonField, ... }\n )\n};\n\n-- An operation is a child of the service, so its properties are in ( ).\n-- `OPERATION Name { ... }` is the deprecated spelling (MDL-DEPR070).\n\n-- MAPPING takes a target ENTITY plus a body listing the JSON fields; Mendix\n-- stores it inline on the operation. An existing import/export mapping\n-- document cannot be referenced here (rejected as MDL-REST01).\n-- There is no FILE request body: Mendix's consumed operation stores one of\n-- Rest$JsonBody, Rest$StringBody or Rest$ImplicitMappingBody, so a file\n-- document has nowhere to go. `Body: FILE FROM $Doc` is rejected as\n-- MDL-REST02 rather than sent as the literal text \"$Doc\" (it used to be,\n-- returning 200 with a 4-byte payload). Binary POST lives on the\n-- microflow activity: `call rest service post '' body binary $Doc/Contents`.\n-- `Response: FILE AS $Doc` is unaffected — downloads work.", Example: "CREATE CONSUMED REST SERVICE Module.PetStore (\n BaseUrl: 'https://petstore.example.com/api',\n Authentication: NONE\n)\n{\n OPERATION GetPet (\n Method: GET,\n Path: '/pets/{id}',\n Parameters: ($id: String),\n Query: ($verbose: String),\n Response: MAPPING Module.Pet {\n Name = name,\n Status = status\n }\n )\n};", SeeAlso: []string{"rest", "rest.published"}, }) @@ -411,19 +411,26 @@ func init() { "jdbc", "byod", "database connector", "execute database query", "postgresql", "mysql", "oracle", "snowflake", "sql server", }, - Syntax: `CREATE [OR MODIFY] DATABASE CONNECTION Module.Name [FOLDER 'path'] - TYPE '' - CONNECTION STRING @Module.UrlConstant - USERNAME @Module.UserConstant - PASSWORD @Module.PasswordConstant -[BEGIN - QUERY - SQL $$$$ - [PARAMETER : [DEFAULT '' | NULL]] - [RETURNS Module.Entity - [MAP ( AS , ... )]] - ; -END]; + Syntax: `CREATE [OR MODIFY] DATABASE CONNECTION Module.Name [FOLDER 'path'] ( + Type: '', + ConnectionString: @Module.UrlConstant, + Username: @Module.UserConstant, + Password: @Module.PasswordConstant + [, Host: '', Port: , DatabaseName: ''] +) [{ + QUERY ( + Sql: $$$$ + [, Parameters: ( : [DEFAULT '' | NULL], ... )] + [, Returns: Module.Entity] + [, Map: ( = , ... )] + ) + ... +}]; + +The properties are in ( ) and the queries are children in { } (R2). The old +clause form -- TYPE '…' CONNECTION STRING @… BEGIN QUERY … SQL … RETURNS … +MAP (column AS Attribute); END -- still parses and warns (MDL-DEPR127); +mxcli fmt --upgrade rewrites it. SHOW DATABASE CONNECTIONS [IN ]; DESCRIBE DATABASE CONNECTION Module.Name; @@ -432,7 +439,7 @@ DROP DATABASE CONNECTION Module.Name; Calling a query from a microflow: EXECUTE DATABASE QUERY Module.Connection.QueryName (...); -TYPE is one of Studio Pro's entries — 'MSSQL', 'MySQL', 'Oracle', +Type is one of Studio Pro's entries — 'MSSQL', 'MySQL', 'Oracle', 'PostgreSQL', 'Snowflake' — or 'BYOD' ("bring your own driver"), which skips the driver-presence check and takes the connection string verbatim. Use BYOD for any JDBC driver Mendix has no entry for; put the driver on the classpath @@ -442,12 +449,12 @@ build stays green and the connection simply does not work. Three traps: - 1. CONNECTION STRING / USERNAME / PASSWORD must reference CONSTANTS + 1. ConnectionString / Username / Password must reference CONSTANTS (@Module.Name), not literals. A literal produces a project Studio Pro cannot open at all: StorageLoadException "is not a valid ConstantIdentifier". mxcli catches it (MDL058); mxbuild does not, so the build is green. - 2. USERNAME and PASSWORD must be given even when the driver needs neither. + 2. Username and Password must be given even when the driver needs neither. Omitting them writes an empty constant reference, the build stays green, and the query fails only at run time with "Could not find value for constant ''". @@ -477,22 +484,22 @@ CREATE NON-PERSISTENT ENTITY Ops.EmployeeRow ( Name: String(100) ); -CREATE DATABASE CONNECTION Ops.Erp - TYPE 'PostgreSQL' - CONNECTION STRING @Ops.DbUrl - USERNAME @Ops.DbUser - PASSWORD @Ops.DbPass -BEGIN - QUERY GetEmployees - SQL $$SELECT id, name FROM employees WHERE dept = {dept}$$ - PARAMETER dept: String DEFAULT 'sales' - RETURNS Ops.EmployeeRow - MAP ( - id AS EmployeeId, - name AS Name +CREATE DATABASE CONNECTION Ops.Erp ( + Type: 'PostgreSQL', + ConnectionString: @Ops.DbUrl, + Username: @Ops.DbUser, + Password: @Ops.DbPass +) { + QUERY GetEmployees ( + Sql: $$SELECT id, name FROM employees WHERE dept = {dept}$$, + Parameters: ( dept: String DEFAULT 'sales' ), + Returns: Ops.EmployeeRow, + Map: ( + EmployeeId = id, + Name = name ) - ; -END; + ) +}; -- A named type needs its driver declared, or the build fails with CE5278. ALTER MODULE Ops ADD JAR DEPENDENCY ( @@ -664,7 +671,7 @@ DESCRIBE DATABASE CONNECTION Ops.Erp;`, "image collection", "create image collection", "drop image collection", "export level", "image", "icon", "logo", }, - Syntax: "SHOW IMAGE COLLECTION [IN Module];\nDESCRIBE IMAGE COLLECTION Module.Name;\n[/** documentation */]\nCREATE IMAGE COLLECTION Module.Name [FOLDER 'path']\n [EXPORT LEVEL 'Hidden'|'Public']\n [{ IMAGE name ( File: 'path' ) ... }];\nCREATE OR MODIFY IMAGE COLLECTION Module.Name [...];\nDROP IMAGE COLLECTION Module.Name;\n\n-- The images are the collection's children, so they are in { }, each with\n-- its properties in ( ). `( IMAGE name FROM FILE 'path', ... )` is the\n-- deprecated spelling (MDL-DEPR072).", + Syntax: "SHOW IMAGE COLLECTION [IN Module];\nDESCRIBE IMAGE COLLECTION Module.Name;\n[/** documentation */]\nCREATE IMAGE COLLECTION Module.Name [FOLDER 'path']\n [EXPORT LEVEL 'Hidden'|'Public']\n [{ IMAGE name ( File: 'path' ) | IMAGE name ( Data: 'base64' [, Format: png] ) ... }];\nCREATE OR MODIFY IMAGE COLLECTION Module.Name [...];\nDROP IMAGE COLLECTION Module.Name;\n\n-- The images are the collection's children, so they are in { }, each with\n-- its properties in ( ). `( IMAGE name FROM FILE 'path', ... )` is the\n-- deprecated spelling (MDL-DEPR072). DESCRIBE writes each image as\n-- Data: '', so its output needs no files.", Example: "/** Application icons */\nCREATE OR MODIFY IMAGE COLLECTION MyModule.AppIcons\n EXPORT LEVEL 'Public' {\n IMAGE logo ( File: 'assets/logo.png' )\n IMAGE \"favicon\" ( File: 'assets/favicon.ico' )\n};\n\nDESCRIBE IMAGE COLLECTION MyModule.AppIcons;", SeeAlso: []string{"integration", "icon-collection"}, }) @@ -683,7 +690,7 @@ DESCRIBE DATABASE CONNECTION Ops.Erp;`, Register(SyntaxFeature{ Path: "glyph", - Summary: "Glyph icons — the numeric codes `icon glyph ` accepts, and their names", + Summary: "Glyph icons — the numeric codes `Icon: glyph ` accepts, and their names", Keywords: []string{ "glyph", "glyphs", "show glyphs", "describe glyph", "icon glyph", "glyphicon", "glyph code", "menu icon", "navigation icon", @@ -694,10 +701,10 @@ DESCRIBE DATABASE CONNECTION Ops.Erp;`, Example: "-- A glyph is a character code in the Mendix glyph font, not a document in the\n" + "-- project, so there is no module to scope and no connection needed.\n" + "SHOW GLYPHS LIKE 'star';\n" + - "-- 57350 star icon glyph 57350\n" + - "-- 57351 star-empty icon glyph 57351\n" + + "-- 57350 star Icon: glyph 57350\n" + + "-- 57351 star-empty Icon: glyph 57351\n" + "DESCRIBE GLYPH 'star';\n" + - "-- → then: MENU ITEM 'Favourites' PAGE M.Favourites ICON GLYPH 57350;\n" + + "-- → then: MENU ITEM 'Favourites' ( OnClick: SHOW PAGE M.Favourites, Icon: GLYPH 57350 )\n" + "\n" + "-- Prefer an icon COLLECTION reference where the icon exists there: it is a\n" + "-- model reference that `check --references` resolves, while a glyph code is\n" + diff --git a/cmd/mxcli/syntax/features_microflow.go b/cmd/mxcli/syntax/features_microflow.go index 1026d018e5..34167d54cf 100644 --- a/cmd/mxcli/syntax/features_microflow.go +++ b/cmd/mxcli/syntax/features_microflow.go @@ -619,6 +619,8 @@ func init() { "@anchor(from: right, to: left) -- which SIDE each end of the outgoing flow attaches to\n" + "@curve(from: (40, -90), to: (-40, 90)) -- the flow's bezier control vectors\n" + "@merge(x, y) -- the implicit merge that closes a split\n" + + "@anchor(from: bottom, to: top, true: (…), false: (…)) -- on an IF: to = its incoming flow,\n" + + " -- from = the flow leaving its closing merge\n" + "@caption 'text'\n@color Green\n@annotation 'a note'\n@excluded\n" + "@applyentityaccess | @applyentityaccess(false) -- DOCUMENT-level, before CREATE MICROFLOW/RULE\n" + "@annotation(id: n1, text: 'a note', position: (x, y), size: (w, h))\n" + diff --git a/cmd/mxcli/syntax/features_misc.go b/cmd/mxcli/syntax/features_misc.go index 43f8351f01..0e5bd004e4 100644 --- a/cmd/mxcli/syntax/features_misc.go +++ b/cmd/mxcli/syntax/features_misc.go @@ -89,7 +89,7 @@ func init() { "-- where it does not, DESCRIBE flags the gap as a comment rather than\n" + "-- producing output that looks complete.", Example: "CREATE OR REPLACE MICROFLOW MyModule.ACT_Recalculate ()\nBEGIN\n RETURN;\nEND;\n\nCREATE OR MODIFY PERSISTENT ENTITY MyModule.Customer (\n Name: String(200)\n);", - SeeAlso: []string{"microflow", "domain-model.entity", "page", "document-folder"}, + SeeAlso: []string{"microflow", "domain-model.entity", "page", "document-folder", "create-if-not-exists"}, }) // IF EXISTS sits on every document-level alternative of dropStatement, so it @@ -128,6 +128,41 @@ func init() { SeeAlso: []string{"create-modifiers"}, }) + // IF NOT EXISTS sits in every create rule that names one element, and is + // applied once in the visitor and once in the executor's dispatch, so it is + // documented once here too (ako/mxcli#731, ADR-0010 R1). + Register(SyntaxFeature{ + Path: "create-if-not-exists", + Summary: "CREATE … IF NOT EXISTS — create an element only when it is absent", + Keywords: []string{ + "if not exists", "create if not exists", "re-run", "rerun", + "idempotent", "already exists", "leave alone", "skip", + }, + Syntax: "CREATE IF NOT EXISTS Module.Name …;\n" + + "CREATE MODULE IF NOT EXISTS ModuleName;\n" + + "CREATE USER ROLE IF NOT EXISTS Name (…);\n" + + "CREATE DEMO USER IF NOT EXISTS 'name' PASSWORD '…' (…);\n" + + "CREATE CONFIGURATION IF NOT EXISTS 'Name' (…);\n\n" + + "-- IF NOT EXISTS goes after the kind's keywords, before the name. When the\n" + + "-- element already exists the statement is SKIPPED (and says so) and the\n" + + "-- stored element is left exactly as it is; otherwise it creates, like a\n" + + "-- plain CREATE.\n" + + "--\n" + + "-- It is not CREATE OR MODIFY, which makes the stored element match the\n" + + "-- statement. Writing both is refused as MDL067. DESCRIBE never emits it.\n" + + "--\n" + + "-- Every CREATE that names one element accepts it. Not accepted where\n" + + "-- there is no one named element to test: ANNOTATION, INDEX (use ALTER\n" + + "-- ENTITY … ADD INDEX IF NOT EXISTS), VALIDATION RULE, NAVIGATION,\n" + + "-- TRANSLATIONS and EXTERNAL ENTITIES.", + Example: "-- seed a module once; later runs leave hand edits alone\n" + + "CREATE MODULE IF NOT EXISTS Shop;\n" + + "CREATE ENUMERATION IF NOT EXISTS Shop.Status (Open 'Open', Closed 'Closed');\n" + + "CREATE CONSTANT IF NOT EXISTS Shop.ApiUrl TYPE String DEFAULT 'https://api.example.com';\n" + + "CREATE MICROFLOW IF NOT EXISTS Shop.ACT_Init ()\nBEGIN\n RETURN;\nEND;", + SeeAlso: []string{"create-modifiers", "drop-if-exists"}, + }) + // The folder clause is the other cross-cutting CREATE modifier, and gets one // topic for the same reason OR MODIFY does: it applies to every document // type, so documenting it in all 27 places would guarantee 27 chances to @@ -294,10 +329,6 @@ DISCONNECT;`, [HOME PAGE Module.Page FOR UserRole] [LOGIN PAGE Module.LoginPage] [NOT FOUND PAGE Module.Custom404] - [MENU ( - MENU ITEM 'Label' PAGE Module.Page [ICON Module.IconCollection.Name]; - MENU 'Group' [ICON Module.IconCollection.Name] ( ... ); - )] [ON SYNC ERROR THROW|CONTINUE] [SYNC ( SYNC Module.Entity ONLINE; @@ -306,7 +337,18 @@ DISCONNECT;`, SYNC Module.Entity NEVER; SYNC Module.Entity NONE; SYNC Module.Entity NONE PRESERVE DATA; - )]; + )] + [{ + MENU ITEM 'Label' [( OnClick: SHOW PAGE Module.Page | CALL MICROFLOW Module.Flow | SIGN OUT + [, Icon: Module.IconCollection.Name] )] + MENU 'Group' [( Icon: Module.IconCollection.Name )] { ... } + }]; + +-- The menu items are the profile's CHILDREN, in { } after its clauses, like a +-- page's widgets: no ; between them, since a child ends in ) or }. An item's +-- action is OnClick: in the words a page action uses. The old spelling, +-- MENU ( MENU ITEM 'Label' PAGE M.P ICON I; ... ), still parses and warns +-- (MDL-DEPR121, MDL-DEPR122); mxcli fmt --upgrade rewrites it. -- FOR takes a USER role, written BARE (FOR Administrator). User roles are -- project-level and have no module part; a module role is a different thing @@ -316,10 +358,10 @@ DISCONNECT;`, -- UserRoleIdentifier", raised before checking runs, so there is no error code -- and no line number. List the real ones with LIST USER ROLES. -- --- ICON is a qualified name into an ICON COLLECTION (Atlas_Core.Atlas, +-- Icon: is a qualified name into an ICON COLLECTION (Atlas_Core.Atlas, -- Atlas_Core.Atlas_Filled, Atlas_Core.Atlas_Styling, or your own) -- a model -- reference, not a string. Hyphenated Atlas names are double-quoted: --- ICON Atlas_Core.Atlas."align-center" +-- Icon: Atlas_Core.Atlas."align-center" -- Browse the available names with: -- SHOW ICON COLLECTION / DESCRIBE ICON COLLECTION Module.Name -- @@ -355,7 +397,7 @@ DISCONNECT;`, -- than a new keyword. OMITTING it leaves the stored value alone; DESCRIBE emits -- it only when it is not the default. -- --- The block REPLACES the stored list, the way MENU replaces the menu. An +-- The block REPLACES the stored list, the way { } replaces the menu. An -- entity's compatibility-mode flag has no syntax and is preserved across the -- rewrite untouched; DESCRIBE NAVIGATION flags it rather than dropping it. -- An invented name ("Mobile") is an error: the runtime routes on User-Agent to @@ -368,19 +410,20 @@ DISCONNECT;`, HOME PAGE MyModule.Home_Web HOME PAGE MyModule.AdminDashboard FOR Administrator LOGIN PAGE Administration.Login - MENU ( - MENU ITEM 'Home' PAGE MyModule.Home_Web ICON Atlas_Core.Atlas.home; - MENU 'Orders' ICON Atlas_Core.Atlas."shopping-cart" ( - MENU ITEM 'All Orders' PAGE Orders.Order_Overview ICON Atlas_Core.Atlas."list-bullets"; - MENU ITEM 'New Order' PAGE Orders.Order_New ICON Atlas_Core.Atlas.add; - ); - ); + { + MENU ITEM 'Home' ( OnClick: SHOW PAGE MyModule.Home_Web, Icon: Atlas_Core.Atlas.home ) + MENU 'Orders' ( Icon: Atlas_Core.Atlas."shopping-cart" ) { + MENU ITEM 'All Orders' ( OnClick: SHOW PAGE Orders.Order_Overview, Icon: Atlas_Core.Atlas."list-bullets" ) + MENU ITEM 'New Order' ( OnClick: SHOW PAGE Orders.Order_New, Icon: Atlas_Core.Atlas.add ) + } + MENU ITEM 'Log out' ( OnClick: SIGN OUT, Icon: Atlas_Core.Atlas."log-out" ) + }; CREATE OR REPLACE NAVIGATION TabletOffline HOME PAGE Maintenance.Request_Overview - MENU ( - MENU ITEM 'Requests' PAGE Maintenance.Request_Overview; - );`, + { + MENU ITEM 'Requests' ( OnClick: SHOW PAGE Maintenance.Request_Overview ) + };`, SeeAlso: []string{"navigation.show"}, }) diff --git a/cmd/mxcli/syntax/features_page.go b/cmd/mxcli/syntax/features_page.go index f2247d0169..4021ecbb34 100644 --- a/cmd/mxcli/syntax/features_page.go +++ b/cmd/mxcli/syntax/features_page.go @@ -12,8 +12,8 @@ func init() { "page", "pages", "form", "UI", "user interface", "widget", "layout", "screen", }, - Syntax: "CREATE PAGE Module.Name [FOLDER 'FolderPath']\n (\n Title: 'Page Title',\n Layout: Module.LayoutName\n [, Params: { $Param: Module.Entity }]\n [, Url: 'page-url']\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 }", + Syntax: "CREATE PAGE Module.Name [FOLDER 'FolderPath']\n (\n Title: 'Page Title',\n Layout: Module.LayoutName\n [, Params: ( $Param: Module.Entity )]\n [, Url: 'page-url']\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"}, }) @@ -24,8 +24,8 @@ func init() { "create page", "new page", "page parameters", "page variables", "layout", "url", "folder", }, - Syntax: "CREATE PAGE Module.Name [FOLDER 'FolderPath']\n (\n Title: 'Title',\n Layout: Module.Layout\n [, Params: { $P: Module.Entity, $Qty: Integer }]\n [, Url: 'page-url']\n [, Variables: { $showStock: Boolean = 'true' }]\n )\n { }", - Example: "CREATE PAGE Module.Products\n (\n Title: 'Products',\n Layout: Atlas_Core.Atlas_Default,\n Url: 'products',\n Variables: { $showStock: Boolean = 'true' }\n )\n {\n DATAGRID gridProducts (DataSource: DATABASE Module.Product) {\n COLUMN colName (Attribute: Name, Caption: 'Name')\n }\n }", + Syntax: "CREATE PAGE Module.Name [FOLDER 'FolderPath']\n (\n Title: 'Title',\n Layout: Module.Layout\n [, Params: ( $P: Module.Entity, $Qty: Integer )]\n [, Url: 'page-url']\n [, Variables: ( $showStock: Boolean = 'true' )]\n )\n { }", + Example: "CREATE PAGE Module.Products\n (\n Title: 'Products',\n Layout: Atlas_Core.Atlas_Default,\n Url: 'products',\n Variables: ( $showStock: Boolean = 'true' )\n )\n {\n DATAGRID gridProducts (DataSource: DATABASE Module.Product) {\n COLUMN colName (Attribute: Name, Caption: 'Name')\n }\n }", SeeAlso: []string{"page", "page.widgets", "page.datasource"}, }) @@ -151,7 +151,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "-- The bracketed Visible: [IsActive] (attributes rooted for you) is the deprecated\n" + "-- spelling, MDL-DEPR081.\n\n" + "-- Actions\nACTIONBUTTON name (Caption: 'C', Action: SAVE CHANGES, ButtonStyle: Primary)\nLINKBUTTON name (Caption: 'C', Action: ...)\n\n" + - "-- 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" + + "-- 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" + "-- IMAGE needs a source. Its default, `ImageType: image`, shows an entry from an\n" + "-- image collection, named as three parts: Module.Collection.ImageName.\n" + "-- `show image collections` lists the collections, `describe image collection\n" + @@ -163,8 +163,8 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "-- A text-template property (ImageUrl, AlternativeText, a pluggable widget's\n" + "-- headerCaption/title/…) takes TEXT, so a bare value renders the same string\n" + "-- on every row. Bind it with the property's own `Params` companion:\n" + - "IMAGE name (ImageType: imageUrl, ImageUrl: '{1}', ImageUrlParams: [{1} = PictureUrl],\n" + - " AlternativeText: '{1}', AlternativeTextParams: [{1} = Name])\n" + + "IMAGE name (ImageType: imageUrl, ImageUrl: '{1}', ImageUrlParams: ({1} = PictureUrl),\n" + + " AlternativeText: '{1}', AlternativeTextParams: ({1} = Name))\n" + "-- The widget-wide `contentparams:` is one list shared by every template on the\n" + "-- widget; `'{AttrName}'` is the shortest form for a single attribute.\n\n" + "-- Any pluggable widget by its id (id FIRST, then the name)\nPLUGGABLEWIDGET 'com.mendix.widget.web.badge.Badge' name (value: 'x')\nCUSTOMWIDGET 'com.mendix.widget.custom.x.X' name (prop: 'x') -- legacy spelling\n\n" + @@ -232,12 +232,12 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "template, DROP it and INSERT the new one in the same ALTER block; operations apply in\n" + "order.", Example: "LISTVIEW vehicleListView (DataSource: DATABASE Pages.Vehicle) {\n" + - " DYNAMICTEXT defaultVehicle (Content: '{1} {2}', ContentParams: [{1} = Brand, {2} = Model])\n" + + " DYNAMICTEXT defaultVehicle (Content: '{1} {2}', ContentParams: ({1} = Brand, {2} = Model))\n" + " TEMPLATE FOR Pages.Bus {\n" + - " DYNAMICTEXT busLabel (Content: 'Bus, capacity {1}', ContentParams: [{1} = PassengerCapacity])\n" + + " DYNAMICTEXT busLabel (Content: 'Bus, capacity {1}', ContentParams: ({1} = PassengerCapacity))\n" + " }\n" + " TEMPLATE FOR Pages.Truck {\n" + - " DYNAMICTEXT truckLabel (Content: 'Truck, max load {1} kg', ContentParams: [{1} = MaxLoadKg])\n" + + " DYNAMICTEXT truckLabel (Content: 'Truck, max load {1} kg', ContentParams: ({1} = MaxLoadKg))\n" + " }\n" + "}", SeeAlso: []string{"page.widgets", "page.datasource"}, @@ -290,7 +290,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 { -- 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 ` 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: CALL MICROFLOW Module.MF) ON btnSave; -- any CREATE PAGE action form\n SET ('createFileAction': CALL 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 { };\n INSERT BEFORE widgetName { };\n INSERT INTO containerName { };\n DROP name1, name2;\n DROP TEMPLATE FOR Module.Specialization IN listViewName;\n REPLACE widgetName WITH { };\n};\n-- A target is a widget name, or a DataGrid 2 column by what it shows:\n-- grid column(Attr), grid column('Caption'), @n when two match. 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.", + 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 ` 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: CALL MICROFLOW Module.MF) ON btnSave; -- any CREATE PAGE action form\n SET ('createFileAction': CALL 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 { };\n INSERT BEFORE widgetName { };\n INSERT INTO containerName { };\n DROP name1, name2;\n DROP TEMPLATE FOR Module.Specialization IN listViewName;\n REPLACE widgetName WITH { };\n};\n-- A target is a widget name, or a DataGrid 2 column by what it shows:\n-- grid column(Attr), grid column('Caption'), @n when two match. 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"}, }) @@ -308,8 +308,8 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { " Class: 'css-class-name' -- static CSS classes\n" + " Style: 'color: red; padding: 8px;' -- inline CSS\n" + " DynamicClasses: '' -- runtime-computed (stacks on Class)\n" + - " DesignProperties: ['Spacing top': 'Large']\n" + - " DesignProperties: ['Full width': ON]\n\n" + + " DesignProperties: ('Spacing top': 'Large')\n" + + " DesignProperties: ('Full width': ON)\n\n" + "ON A PAGE THAT ALREADY EXISTS, without rewriting it:\n\n" + " ALTER STYLING ON PAGE|SNIPPET Module.Name WIDGET \n" + " SET (Class: 'css-class', Style: 'css', 'Design property': 'Value'|ON|OFF);\n\n" + @@ -402,8 +402,8 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "snippet", "snippets", "reusable", "snippetcall", "page fragment", "component", }, - Syntax: "CREATE SNIPPET Module.Name [FOLDER 'path']\n [( Params: { $P: Module.Entity } )] -- parameters are entities only\n {\n -- widgets (same as page)\n }\n\n-- Embed in a page:\nSNIPPETCALL scName (Snippet: Module.SnippetName)", - Example: "CREATE SNIPPET MyModule.CustomerInfo (\n Params: { $Customer: MyModule.Customer }\n)\n{\n DATAVIEW dv (DataSource: $Customer) {\n TEXTBOX txtName (Label: 'Name', Attribute: Name)\n TEXTBOX txtEmail (Label: 'Email', Attribute: Email)\n }\n}", + Syntax: "CREATE SNIPPET Module.Name [FOLDER 'path']\n [( Params: ( $P: Module.Entity ) )] -- parameters are entities only\n {\n -- widgets (same as page)\n }\n\n-- Embed in a page:\nSNIPPETCALL scName (Snippet: Module.SnippetName)", + Example: "CREATE SNIPPET MyModule.CustomerInfo (\n Params: ( $Customer: MyModule.Customer )\n)\n{\n DATAVIEW dv (DataSource: $Customer) {\n TEXTBOX txtName (Label: 'Name', Attribute: Name)\n TEXTBOX txtEmail (Label: 'Email', Attribute: Email)\n }\n}", SeeAlso: []string{"snippet.create", "snippet.alter", "page"}, }) @@ -420,7 +420,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { // (a PAGE parameter may be primitive; a snippet parameter may not), and // the reporter of mendixlabs/mxcli#1028 reached the bug by following // this line. A primitive is now refused as MDL087. - Syntax: "CREATE SNIPPET Module.Name [FOLDER 'Snippets/Common']\n [( Params: { $P: Module.Entity } )] -- entities only; a primitive is CE0046\n [( Variables: { $isEditable: Boolean = 'true' } )]\n {\n -- widgets\n }\n\n-- To parameterise a snippet on a primitive, keep the primitive on the\n-- calling PAGE and pass an object, or read the value off an entity member.", + Syntax: "CREATE SNIPPET Module.Name [FOLDER 'Snippets/Common']\n [( Params: ( $P: Module.Entity ) )] -- entities only; a primitive is CE0046\n [( Variables: ( $isEditable: Boolean = 'true' ) )]\n {\n -- widgets\n }\n\n-- To parameterise a snippet on a primitive, keep the primitive on the\n-- calling PAGE and pass an object, or read the value off an entity member.", Example: "CREATE SNIPPET MyModule.NavigationMenu\n{\n NAVIGATIONLIST navMenu {\n ITEM itemCustomers (Action: SHOW PAGE MyModule.CustomerOverview) {\n DYNAMICTEXT txtCustomers (Content: 'Customers')\n }\n }\n}", SeeAlso: []string{"snippet", "snippet.alter", "page.widgets"}, }) @@ -502,9 +502,9 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "}\n\n" + "-- A phone layout's bottom bar, as Atlas_Core.Phone_BottomBar has it —\n" + "-- a simple menu bar rendering a menu document:\n" + - "CREATE OR MODIFY MENU MyModule.Phone_Menu (\n" + - " menu item 'Home' page MyModule.Home_Phone icon Atlas_Core.Atlas.home;\n" + - ");\n" + + "CREATE OR MODIFY MENU MyModule.Phone_Menu {\n" + + " menu item 'Home' ( OnClick: show page MyModule.Home_Phone, Icon: Atlas_Core.Atlas.home )\n" + + "};\n" + "CREATE OR REPLACE LAYOUT MyModule.Phone_Bottom (\n" + " layouttype: 'Phone',\n" + " class: 'layout-atlas layout-atlas-phone'\n" + @@ -626,20 +626,21 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "create menu", "describe menu", "drop menu", "menu", "menus", "menu document", "menu item", }, - Syntax: "CREATE [OR MODIFY] MENU Module.Name [FOLDER 'path'] (\n" + - " MENU ITEM '' [PAGE Module.Page | MICROFLOW Module.Flow | SIGN OUT] [ICON Module.Collection.name];\n" + - " MENU '' [ICON Module.Collection.name] ( );\n" + - ");\n" + + Syntax: "CREATE [OR MODIFY] MENU Module.Name [FOLDER 'path'] {\n" + + " MENU ITEM '' [( [OnClick: SHOW PAGE Module.Page | CALL MICROFLOW Module.Flow | SIGN OUT]\n" + + " [, Icon: Module.Collection.name | GLYPH | IMAGE Module.Images.name] )]\n" + + " MENU '' [( Icon: Module.Collection.name )] { }\n" + + "};\n" + "DESCRIBE MENU Module.Name;\n" + "DROP MENU Module.Name;", - Example: "CREATE OR MODIFY MENU MyModule.Main_Menu (\n" + - " menu item 'Home' page MyModule.Home_Web icon Atlas_Core.Atlas.home;\n" + - " menu item 'Run' microflow MyModule.DoThing;\n" + - " menu 'Admin' (\n" + - " menu item 'Accounts' page Administration.Account_Overview;\n" + - " );\n" + - " menu item 'Plain';\n" + - ");\n\n" + + Example: "CREATE OR MODIFY MENU MyModule.Main_Menu {\n" + + " menu item 'Home' ( OnClick: show page MyModule.Home_Web, Icon: Atlas_Core.Atlas.home )\n" + + " menu item 'Run' ( OnClick: call microflow MyModule.DoThing )\n" + + " menu 'Admin' {\n" + + " menu item 'Accounts' ( OnClick: show page Administration.Account_Overview )\n" + + " }\n" + + " menu item 'Plain'\n" + + "};\n\n" + "-- Notes:\n" + "-- * A menu document is the reusable menu a menu widget points at. It is\n" + "-- NOT the menu inside a navigation profile — for that use\n" + @@ -649,8 +650,11 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "-- * SIGN OUT is the log-out menu item. It needs no target and stores the\n" + "-- same Forms$SignOutClientAction a sign-out BUTTON carries. Works both\n" + "-- here and in a navigation profile's menu.\n" + - "-- * ICON names an icon collection entry. A glyph or image icon cannot be\n" + - "-- expressed in MDL; DESCRIBE flags those rather than dropping them silently.\n" + + "-- * The items are children, in { } with no ; between them (R2). The old\n" + + "-- spelling, ( MENU ITEM 'X' PAGE M.P ICON I; ... ), still parses and warns\n" + + "-- (MDL-DEPR121, MDL-DEPR122).\n" + + "-- * Icon: names an icon collection entry; GLYPH and IMAGE M.Images.x\n" + + "-- write the other two icon kinds.\n" + "-- * A page with required parameters cannot be opened from a menu item\n" + "-- without an argument — Mendix reports CE1571.", SeeAlso: []string{"navigation.create", "navigation.show", "page.show"}, @@ -666,7 +670,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "use fragment", "template", "script scope", }, Syntax: "CREATE FRAGMENT Name AS { };\nCREATE FRAGMENT Name AS { SLOT [name] };\nCREATE FRAGMENT Name ($d: datasource, $a: action) AS { };\nUSE FRAGMENT Name [(args)] [AS prefix_];\nUSE FRAGMENT Name [(args)] [AS prefix_] { };\nSHOW FRAGMENTS;\nDESCRIBE FRAGMENT Name;\nDESCRIBE FRAGMENT FROM PAGE Module.Page WIDGET widgetName;", - Example: "CREATE FRAGMENT SaveCancelFooter AS {\n FOOTER footer1 {\n ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary)\n ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES)\n }\n};\n\nCREATE PAGE Module.EditPage (Params: { $Param: Module.Customer }, Title: 'Edit', Layout: 'Atlas_Core.Atlas_Default') {\n DATAVIEW dv (DataSource: $Param) {\n TEXTBOX txtName (Label: 'Name', Attribute: Name)\n USE FRAGMENT SaveCancelFooter\n }\n};", + Example: "CREATE FRAGMENT SaveCancelFooter AS {\n FOOTER footer1 {\n ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary)\n ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES)\n }\n};\n\nCREATE PAGE Module.EditPage (Params: ( $Param: Module.Customer ), Title: 'Edit', Layout: 'Atlas_Core.Atlas_Default') {\n DATAVIEW dv (DataSource: $Param) {\n TEXTBOX txtName (Label: 'Name', Attribute: Name)\n USE FRAGMENT SaveCancelFooter\n }\n};", SeeAlso: []string{"fragment.define", "fragment.use", "fragment.slot", "fragment.params", "snippet"}, }) @@ -689,7 +693,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "prefix", "name conflict", }, Syntax: "USE FRAGMENT Name\nUSE FRAGMENT Name AS prefix_\nUSE FRAGMENT Name [AS prefix_] { }", - Example: "-- Basic usage\nCREATE PAGE Module.Page (Params: { $Param: Module.Customer }, Title: 'Page', Layout: 'Atlas_Core.Atlas_Default') {\n DATAVIEW dv (DataSource: $Param) {\n USE FRAGMENT FormFields\n USE FRAGMENT SaveCancelFooter\n }\n};\n\n-- With prefix to avoid name conflicts\nUSE FRAGMENT SaveCancelFooter AS order_\n-- Creates: order_footer1, order_btnSave, order_btnCancel\n\n-- Fill a fragment's content slot (see fragment.slot)\nUSE FRAGMENT Card {\n DYNAMICTEXT cardHeading (Content: 'Welcome', RenderMode: H2)\n DYNAMICTEXT cardText (Content: 'Wrapped content')\n}", + Example: "-- Basic usage\nCREATE PAGE Module.Page (Params: ( $Param: Module.Customer ), Title: 'Page', Layout: 'Atlas_Core.Atlas_Default') {\n DATAVIEW dv (DataSource: $Param) {\n USE FRAGMENT FormFields\n USE FRAGMENT SaveCancelFooter\n }\n};\n\n-- With prefix to avoid name conflicts\nUSE FRAGMENT SaveCancelFooter AS order_\n-- Creates: order_footer1, order_btnSave, order_btnCancel\n\n-- Fill a fragment's content slot (see fragment.slot)\nUSE FRAGMENT Card {\n DYNAMICTEXT cardHeading (Content: 'Welcome', RenderMode: H2)\n DYNAMICTEXT cardText (Content: 'Wrapped content')\n}", SeeAlso: []string{"fragment", "fragment.define", "fragment.slot"}, }) @@ -713,7 +717,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "card wrapper", "reusable shell", "use fragment payload", }, Syntax: "-- In the definition, mark where caller content lands:\nCREATE FRAGMENT Name AS { SLOT [name] };\n-- At the use site, supply the payload in a brace block:\nUSE FRAGMENT Name [AS prefix_] { }", - Example: "CREATE FRAGMENT Card AS {\n CONTAINER cardWrap (Class: 'card', DesignProperties: ['Card style': on]) {\n CONTAINER cardBody (Class: 'card-body') {\n SLOT content\n }\n }\n};\n\nCREATE PAGE Module.Dashboard (Title: 'Dashboard', Layout: Atlas_Core.Atlas_Default) {\n USE FRAGMENT Card {\n DYNAMICTEXT cardHeading (Content: 'Welcome', RenderMode: H2)\n DYNAMICTEXT cardText (Content: 'Any widgets can go inside the reusable Card shell')\n }\n};\n\n-- Notes:\n-- * Slot name is optional (defaults to 'content'); one slot per fragment.\n-- * USE FRAGMENT with no payload leaves the slot empty (valid).\n-- * Supplying a payload to a slotless fragment is an error.", + Example: "CREATE FRAGMENT Card AS {\n CONTAINER cardWrap (Class: 'card', DesignProperties: ('Card style': on)) {\n CONTAINER cardBody (Class: 'card-body') {\n SLOT content\n }\n }\n};\n\nCREATE PAGE Module.Dashboard (Title: 'Dashboard', Layout: Atlas_Core.Atlas_Default) {\n USE FRAGMENT Card {\n DYNAMICTEXT cardHeading (Content: 'Welcome', RenderMode: H2)\n DYNAMICTEXT cardText (Content: 'Any widgets can go inside the reusable Card shell')\n }\n};\n\n-- Notes:\n-- * Slot name is optional (defaults to 'content'); one slot per fragment.\n-- * USE FRAGMENT with no payload leaves the slot empty (valid).\n-- * Supplying a payload to a slotless fragment is an error.", SeeAlso: []string{"fragment", "fragment.define", "fragment.use"}, }) diff --git a/cmd/mxcli/syntax/features_security.go b/cmd/mxcli/syntax/features_security.go index daff85ad6f..446e5146db 100644 --- a/cmd/mxcli/syntax/features_security.go +++ b/cmd/mxcli/syntax/features_security.go @@ -133,8 +133,8 @@ func init() { "user role", "application role", "manage roles", "add module roles", "remove module roles", }, - Syntax: "CREATE USER ROLE ( [, ...]) [MANAGE ALL ROLES];\nALTER USER ROLE ADD MODULE ROLES ( [, ...]);\nALTER USER ROLE DROP MODULE ROLES ( [, ...]);\nDROP USER ROLE [IF EXISTS] ;", - Example: "CREATE USER ROLE AppAdmin (Shop.Admin, HR.Admin) MANAGE ALL ROLES;\nALTER USER ROLE AppAdmin ADD MODULE ROLES (Reporting.Viewer);", + Syntax: "CREATE USER ROLE [( ModuleRoles: ( [, ...]), Description: '', ManageAllRoles: true|false, ManageableRoles: ( [, ...]), ManageUsersWithoutRoles: true|false, CheckSecurity: true|false )];\nALTER USER ROLE ADD MODULE ROLES ( [, ...]);\nALTER USER ROLE DROP MODULE ROLES ( [, ...]);\nDROP USER ROLE [IF EXISTS] ;", + Example: "CREATE USER ROLE AppAdmin ( ModuleRoles: (Shop.Admin, HR.Admin), ManageAllRoles: true );\nALTER USER ROLE AppAdmin ADD MODULE ROLES (Reporting.Viewer);", SeeAlso: []string{"security.module-role", "security.demo-user"}, }) @@ -170,7 +170,7 @@ func init() { "-- build (CE0133), so ON is refused unless a role is given or already stored.\n" + "-- Mendix does not check the role exists, so mxcli does — an unknown role\n" + "-- would build cleanly and leave visitors with nothing.", - Example: "CREATE USER ROLE Anonymous (Shop.Viewer, System.User);\n" + + Example: "CREATE USER ROLE Anonymous ( ModuleRoles: (Shop.Viewer, System.User) );\n" + "ALTER APP SECURITY GUEST ACCESS ON ROLE Anonymous;\n" + "GRANT READ * ON ENTITY Shop.Product TO Shop.Viewer;", SeeAlso: []string{"security.user-role", "security.project-security"}, diff --git a/cmd/mxcli/syntax/features_workflow.go b/cmd/mxcli/syntax/features_workflow.go index b625784ee4..52060597e0 100644 --- a/cmd/mxcli/syntax/features_workflow.go +++ b/cmd/mxcli/syntax/features_workflow.go @@ -139,7 +139,7 @@ func init() { "-- page without a WorkflowUserTask one -> CE7412\n" + "-- Other parameters may sit alongside it.", Example: "-- The task page takes the task:\n" + - "CREATE PAGE HR.ReviewPage (\n title: 'Review',\n layout: Atlas_Core.Atlas_Default,\n params: { $WorkflowUserTask: System.WorkflowUserTask }\n) { };\n\n" + + "CREATE PAGE HR.ReviewPage (\n title: 'Review',\n layout: Atlas_Core.Atlas_Default,\n params: ( $WorkflowUserTask: System.WorkflowUserTask )\n) { };\n\n" + "USER TASK ReviewTask 'Review the request'\n PAGE HR.ReviewPage\n TARGETING USERS XPATH [Module.Employee/Active = true()]\n OUTCOMES 'Approve' { } 'Reject' { };", SeeAlso: []string{"workflow.user-task.targeting", "workflow.multi-user-task", "workflow.create"}, }) @@ -411,7 +411,7 @@ func init() { // 'Workflows$TimerBoundaryEvent' could not be found"). This entry showed // the bare form, with 'P3D' — not a valid timer expression — as its delay. // ako/view-entity-examples FINDINGS §7. - Syntax: "-- inline, as a clause of a USER TASK (after OUTCOMES):\nBOUNDARY EVENT (INTERRUPTING | NON INTERRUPTING) TIMER { }\nBOUNDARY EVENT (INTERRUPTING | NON INTERRUPTING) NOTIFICATION [''] { }\n\n-- A notification boundary event (Mendix 11.11+) is triggered by `notify workflow`\n-- targeting its name, so the name is unique in the workflow. ALTER cannot insert\n-- one yet; restate the workflow instead.\n\n-- or add a timer to an existing task:\nALTER WORKFLOW INSERT BOUNDARY EVENT ON (INTERRUPTING | NON INTERRUPTING) TIMER { }\n\n-- Name the kind: a bare TIMER is refused on Mendix 11 (MDL-WF07) — it writes a\n-- type the runtime cannot load. The delay is an expression that yields a\n-- DateTime, e.g. addDays([%CurrentDateTime%], 3). mxcli ends every boundary\n-- path with Mendix's end-of-path marker, so a path may end in a call; use\n-- JUMP TO to return to the task instead.", + Syntax: "-- inline, as a clause of a USER TASK (after OUTCOMES):\nBOUNDARY EVENT (INTERRUPTING | NON INTERRUPTING) TIMER { }\nBOUNDARY EVENT (INTERRUPTING | NON INTERRUPTING) NOTIFICATION [''] { }\n\n-- A notification boundary event (Mendix 11.11+) is triggered by `notify workflow`\n-- targeting its name, so the name is unique in the workflow. ALTER cannot insert\n-- one yet; restate the workflow instead.\n\n-- or add a timer to an existing task:\nALTER WORKFLOW { INSERT INTO { BOUNDARY EVENT (INTERRUPTING | NON INTERRUPTING) TIMER { } } };\n\n-- Name the kind: a bare TIMER is refused on Mendix 11 (MDL-WF07) — it writes a\n-- type the runtime cannot load. The delay is an expression that yields a\n-- DateTime, e.g. addDays([%CurrentDateTime%], 3). mxcli ends every boundary\n-- path with Mendix's end-of-path marker, so a path may end in a call; use\n-- JUMP TO to return to the task instead.", Example: "user task ReviewTask 'Review'\n page Module.WF_Review\n outcomes 'Done' { }\n boundary event interrupting timer addDays([%CurrentDateTime%], 3) {\n call microflow Module.WF_Escalate;\n };", MinVersion: "10.6.0", SeeAlso: []string{"workflow.user-task", "workflow.notification", "workflow.event-subprocess"}, @@ -491,25 +491,46 @@ func init() { Keywords: []string{ "alter workflow", "modify workflow", "update workflow", "add activity", "drop activity", "replace activity", + "insert before", "insert into", "insert outcome", "insert path", }, - // SET properties are keyword-led phrases, not `name = value` assignments: - // `SET DUE DATE `, `SET DISPLAY ''`, `SET OVERVIEW PAGE - // Module.Page`. The `= ` this entry used to show does not parse. - // INSERT names the anchor first and the activity second — INSERT AFTER - // — and there is no BEFORE. DROP and REPLACE take the - // ACTIVITY keyword. This entry previously showed the operand order - // reversed, advertised a BEFORE that does not exist, and omitted - // ACTIVITY, so none of it parsed. + // The generic ALTER (ADR-0012, ako/mxcli#712): the operations go in + // { }, a target is an activity's name or 'caption' with @n to choose one + // of several matches (an ambiguous one is refused with the matches + // listed), and a fragment is written exactly as in CREATE WORKFLOW. + // The old per-action forms (`SET DISPLAY '…'`, `SET ACTIVITY X …`, + // `INSERT OUTCOME 'x' ON X { … }`, `DROP PATH 'Path 2' ON X`) still + // parse and warn MDL-DEPR140-149; `mxcli fmt --upgrade` rewrites them. // - // The four INSERT ops that add to an activity's outcome list each write - // ONE outcome type, and the list is typed per activity kind — INSERT - // OUTCOME only on a user task, INSERT PATH only on a parallel split, - // INSERT CONDITION only on a decision or call microflow. Aiming one at - // the wrong kind used to produce a project Mendix could not LOAD - // (ako/mxcli#415); it is refused now, but the entry documented only two - // of the ops, which is how an author reached for the wrong one. - Syntax: "ALTER WORKFLOW Module.Name SET DISPLAY '';\nALTER WORKFLOW Module.Name SET DUE DATE ;\nALTER WORKFLOW Module.Name SET OVERVIEW PAGE Module.Page;\nALTER WORKFLOW Module.Name SET ACTIVITY ;\nALTER WORKFLOW Module.Name INSERT AFTER ;\nALTER WORKFLOW Module.Name DROP ACTIVITY ;\nALTER WORKFLOW Module.Name REPLACE ACTIVITY WITH ;\nALTER WORKFLOW Module.Name INSERT OUTCOME '' ON { };\nALTER WORKFLOW Module.Name DROP OUTCOME '' ON ;\nALTER WORKFLOW Module.Name INSERT CONDITION '' ON { };\nALTER WORKFLOW Module.Name INSERT PATH ON { };\nALTER WORKFLOW Module.Name INSERT BOUNDARY EVENT ON (INTERRUPTING | NON INTERRUPTING) TIMER { };", - Example: "ALTER WORKFLOW HR.LeaveApproval SET DUE DATE addDays([%CurrentDateTime%], 7);\nALTER WORKFLOW HR.LeaveApproval INSERT AFTER ReviewTask\n CALL MICROFLOW HR.NotifyHR;\nALTER WORKFLOW HR.LeaveApproval DROP ACTIVITY ObsoleteStep;\n\n-- The INSERT op has to match the activity kind: an outcome list is typed,\n-- and the wrong one is refused (it would leave a project Mendix cannot open).\nALTER WORKFLOW HR.LeaveApproval INSERT OUTCOME 'Rejected' ON ReviewTask { };\nALTER WORKFLOW HR.LeaveApproval INSERT CONDITION 'HR.Status.Urgent' ON Triage { };\nALTER WORKFLOW HR.LeaveApproval INSERT PATH ON NotifyAll { };", + // What `insert into` adds must fit the activity: an outcome list is + // typed per activity kind — a user task takes `outcomes 'x' { … }`, a + // decision or call microflow `outcomes -> { … }`, a parallel + // split `path { … }`. The wrong one is refused (it would leave a project + // Mendix cannot LOAD, ako/mxcli#415). + Syntax: "ALTER WORKFLOW Module.Name {\n" + + " SET ( Display: '', Description: '', ExportLevel: API | Hidden,\n" + + " DueDate: , OverviewPage: Module.Page, Parameter: $WorkflowContext: Module.Entity );\n" + + " SET ( Page: Module.Page, Description: '', DueDate: ,\n" + + " Targeting: microflow Module.Mf | Targeting: xpath [] ) ON ;\n" + + " INSERT BEFORE | AFTER { }\n" + + " INSERT INTO { OUTCOMES '' { } }\n" + + " INSERT INTO { OUTCOMES true | false | default | '' -> { } }\n" + + " INSERT INTO { PATH [] { } }\n" + + " INSERT INTO { BOUNDARY EVENT (INTERRUPTING | NON INTERRUPTING) TIMER { } }\n" + + " REPLACE WITH { }\n" + + " DROP , OUTCOME '' | true | false | default,\n" + + " PATH , BOUNDARY EVENT;\n" + + "};\n\n" + + "-- is a name (ReviewTask) or a caption ('Review the request'),\n" + + "-- optionally @n to choose one of several matches: describe workflow prints the names.", + Example: "alter workflow HR.LeaveApproval {\n" + + " set (DueDate: addDays([%CurrentDateTime%], 7));\n" + + " set (Page: HR.ReviewPage) on ReviewTask;\n" + + " insert after ReviewTask { call microflow HR.NotifyHR; }\n" + + " insert into ReviewTask { outcomes 'Rejected' { } }\n" + + " insert into Triage { outcomes 'HR.Status.Urgent' -> { } }\n" + + " insert into NotifyAll { path { } }\n" + + " drop ObsoleteStep;\n" + + "};", SeeAlso: []string{"workflow.create", "workflow.drop"}, }) } diff --git a/cmd/mxcli/tui/image_render.go b/cmd/mxcli/tui/image_render.go index af827cc306..928aee20ac 100644 --- a/cmd/mxcli/tui/image_render.go +++ b/cmd/mxcli/tui/image_render.go @@ -1,10 +1,13 @@ package tui import ( + "crypto/sha256" "encoding/base64" + "encoding/hex" "fmt" "os" "os/exec" + "path/filepath" "strings" ) @@ -115,23 +118,62 @@ func renderImagesWithSize(paths []string, width, perImgHeight int) string { return sb.String() } -// extractImagePaths parses DESCRIBE IMAGE COLLECTION output and extracts -// file paths from lines matching: IMAGE "name" FROM FILE '/path/to/file' +// extractImagePaths parses DESCRIBE IMAGE COLLECTION output and returns a +// file path per image it shows. Describe writes each image into the statement, +// `image Name ( Data: '' )`, rather than naming a file it wrote itself +// (ako/mxcli#707), so the preview decodes the data into its own cache file; a +// script's `File: ''` (or the old `FROM FILE ''`) is used as is. func extractImagePaths(output string) []string { var paths []string for _, line := range strings.Split(output, "\n") { line = strings.TrimSpace(line) - idx := strings.Index(line, "FROM FILE '") - if idx == -1 { - continue + for _, marker := range []string{"File: '", "FROM FILE '"} { + if v, ok := quotedAfter(line, marker); ok { + paths = append(paths, v) + } } - rest := line[idx+len("FROM FILE '"):] - // Strip trailing quote and optional comma/semicolon - end := strings.Index(rest, "'") - if end == -1 { - continue + if v, ok := quotedAfter(line, "Data: '"); ok { + if path := previewImageFile(v); path != "" { + paths = append(paths, path) + } } - paths = append(paths, rest[:end]) } return paths } + +// quotedAfter returns the text between marker and the next quote in line. +func quotedAfter(line, marker string) (string, bool) { + idx := strings.Index(line, marker) + if idx == -1 { + return "", false + } + rest := line[idx+len(marker):] + end := strings.Index(rest, "'") + if end == -1 { + return "", false + } + return rest[:end], true +} + +// previewImageFile writes base64 image data to a file in the preview cache, +// named by its content so an unchanged image is written once, and returns the +// path; "" when the data does not decode or cannot be written. +func previewImageFile(b64 string) string { + data, err := base64.StdEncoding.DecodeString(b64) + if err != nil || len(data) == 0 { + return "" + } + dir := filepath.Join(os.TempDir(), "mxcli-preview") + if err := os.MkdirAll(dir, 0o755); err != nil { + return "" + } + sum := sha256.Sum256(data) + path := filepath.Join(dir, hex.EncodeToString(sum[:8])) + if _, err := os.Stat(path); err == nil { + return path + } + if err := os.WriteFile(path, data, 0o644); err != nil { + return "" + } + return path +} diff --git a/cmd/mxcli/tui/image_render_test.go b/cmd/mxcli/tui/image_render_test.go new file mode 100644 index 0000000000..542c238001 --- /dev/null +++ b/cmd/mxcli/tui/image_render_test.go @@ -0,0 +1,30 @@ +package tui + +import ( + "bytes" + "encoding/base64" + "os" + "testing" +) + +// Describe writes an image as `Data: ''` (ako/mxcli#707); the preview +// decodes it into a file of its own, and still takes a script's `File:` path. +func TestExtractImagePaths_DataAndFile(t *testing.T) { + t.Setenv("TMPDIR", t.TempDir()) + img := []byte("\x89PNG\r\n\x1a\nfake") + out := "create or modify image collection M.Icons {\n" + + " image Logo ( Data: '" + base64.StdEncoding.EncodeToString(img) + "' )\n" + + " image Home ( File: 'assets/home.png' )\n" + + "};\n" + paths := extractImagePaths(out) + if len(paths) != 2 { + t.Fatalf("paths = %v, want 2", paths) + } + got, err := os.ReadFile(paths[0]) + if err != nil || !bytes.Equal(got, img) { + t.Errorf("decoded image %q (%v), want %q", got, err, img) + } + if paths[1] != "assets/home.png" { + t.Errorf("file path = %q", paths[1]) + } +} diff --git a/docs-site/src/appendixes/quick-reference.md b/docs-site/src/appendixes/quick-reference.md index 45457b6fb8..d0e58fc783 100644 --- a/docs-site/src/appendixes/quick-reference.md +++ b/docs-site/src/appendixes/quick-reference.md @@ -212,7 +212,7 @@ Nested folders use `/` separator: `'Parent/Child/Grandchild'`. Missing folders a | Describe security matrix | `DESCRIBE SECURITY MATRIX [IN Module];` | Full access overview | | Create module role | `CREATE MODULE ROLE Mod.Role [DESCRIPTION 'text'];` | | | Drop module role | `DROP MODULE ROLE Mod.Role;` | | -| Create user role | `CREATE USER ROLE Name (Mod.Role, ...) [MANAGE ALL ROLES];` | Aggregates module roles | +| Create user role | `CREATE USER ROLE Name ( ModuleRoles: (Mod.Role, ...), Description: '...', ManageAllRoles: true, CheckSecurity: true );` | Aggregates module roles; every property optional | | Alter user role | `ALTER USER ROLE Name ADD\|DROP MODULE ROLES (Mod.Role, ...);` | | | Drop user role | `DROP USER ROLE [IF EXISTS] Name;` | `IF EXISTS` makes a cleanup script re-runnable | | Grant microflow access | `GRANT EXECUTE ON MICROFLOW Mod.MF TO Mod.Role, ...;` | | @@ -235,6 +235,7 @@ Nested folders use `/` separator: `'Parent/Child/Grandchild'`. Missing folders a | Describe workflow | `DESCRIBE WORKFLOW Module.Name;` | Full MDL output | | Create workflow | `CREATE [OR MODIFY] WORKFLOW Module.Name PARAMETER $Ctx: Module.Entity BEGIN ... END WORKFLOW;` | See activity types below | | Drop workflow | `DROP WORKFLOW Module.Name;` | | +| Alter workflow | `ALTER WORKFLOW Module.Name { SET (Key: value, ...) [ON ]; INSERT BEFORE\|AFTER { ... } INSERT INTO { OUTCOMES ... \| PATH { ... } \| BOUNDARY EVENT ... } REPLACE WITH { ... } DROP [OUTCOME '' \| PATH \| BOUNDARY EVENT]; };` | An activity is its name or `'caption'`, `@n` for one of several matches; the old per-action form warns MDL-DEPR140–149 | **Workflow Activity Types:** - `USER TASK '' [PAGE Mod.Page] [TARGETING MICROFLOW Mod.MF] [OUTCOMES '' { } ...];` @@ -286,12 +287,12 @@ CREATE OR REPLACE NAVIGATION Responsive HOME PAGE MyModule.AdminHome FOR Administrator LOGIN PAGE Administration.Login NOT FOUND PAGE MyModule.Custom404 - MENU ( - MENU ITEM 'Home' PAGE MyModule.Home_Web; - MENU 'Admin' ( - MENU ITEM 'Users' PAGE Administration.Account_Overview; - ); - ); + { + MENU ITEM 'Home' ( OnClick: SHOW PAGE MyModule.Home_Web ) + MENU 'Admin' { + MENU ITEM 'Users' ( OnClick: SHOW PAGE Administration.Account_Overview ) + } + }; ``` ## Project Settings @@ -349,7 +350,7 @@ MDL uses explicit property declarations for pages: | Element | Syntax | Example | |---------|-----------|---------| | Page properties | `(Key: value, ...)` | `(Title: 'Edit', Layout: Atlas_Core.Atlas_Default)` | -| Page variables | `Variables: { $name: Type = 'expr' }` | `Variables: { $show: Boolean = 'true' }` | +| Page variables | `Variables: ( $name: Type = 'expr' )` | `Variables: ( $show: Boolean = 'true' )` | | Widget name | Required after type | `TEXTBOX txtName (...)` | | Attribute binding | `Attribute: AttrName` | `TEXTBOX txt (Label: 'Name', Attribute: Name)` | | Variable binding | `DataSource: $Var` | `DATAVIEW dv (DataSource: $Product) { ... }` | @@ -360,7 +361,7 @@ MDL uses explicit property declarations for pages: | CSS class | `Class: 'classes'` | `CONTAINER c (Class: 'card mx-spacing-top-large')` | | Inline style | `Style: 'css'` | `CONTAINER c (Style: 'padding: 16px;')` | | Dynamic classes | `DynamicClasses: 'expr'` | `CONTAINER c (DynamicClasses: if $currentObject/IsActive then 'is-active' else '')` — runtime-computed; stacks on `Class` | -| Design properties | `DesignProperties: [...]` | `CONTAINER c (DesignProperties: ['Spacing top': 'Large', 'Full width': ON])` | +| Design properties | `DesignProperties: (...)` | `CONTAINER c (DesignProperties: ('Spacing top': 'Large', 'Full width': ON))` | | Width (pixels) | `Width: integer` | `IMAGE img (Width: 200)` | | Height (pixels) | `Height: integer` | `IMAGE img (Height: 150)` | | Page size | `PageSize: integer` | `DATAGRID dg (PageSize: 25)` | @@ -390,7 +391,7 @@ MDL uses explicit property declarations for pages: ```sql CREATE PAGE MyModule.Customer_Edit ( - Params: { $Customer: MyModule.Customer }, + Params: ( $Customer: MyModule.Customer ), Title: 'Edit Customer', Layout: Atlas_Core.PopupLayout ) diff --git a/docs-site/src/appendixes/sdk-equivalence.md b/docs-site/src/appendixes/sdk-equivalence.md index fbd43c093b..c6f9d13f1f 100644 --- a/docs-site/src/appendixes/sdk-equivalence.md +++ b/docs-site/src/appendixes/sdk-equivalence.md @@ -56,7 +56,7 @@ The [Mendix Model SDK](https://docs.mendix.com/apidocs-mxsdk/mxsdk/) is Mendix's |---------------|----------------| | `security.ModuleRole.createIn(module)` | `CREATE MODULE ROLE Module.RoleName` | | Manual access rule construction | `GRANT role ON Entity (permissions)` | -| Manual user role creation | `CREATE USER ROLE Name (ModuleRoles)` | +| Manual user role creation | `CREATE USER ROLE Name ( ModuleRoles: (ModuleRoles) )` | ## Workflow Comparison diff --git a/docs-site/src/appendixes/version-compatibility.md b/docs-site/src/appendixes/version-compatibility.md index bb9e199d23..d51970d127 100644 --- a/docs-site/src/appendixes/version-compatibility.md +++ b/docs-site/src/appendixes/version-compatibility.md @@ -78,10 +78,10 @@ The tables below show exactly which features are available on each Mendix versio | Conditional visibility | `Visible: ` | -- | -- | -- | Yes | | Conditional editability | `Editable: ` | -- | -- | -- | Yes | | Responsive column widths | `TabletWidth: 6, PhoneWidth: 12` | -- | -- | -- | Yes | -| Page parameters (entity) | `Params: { $Item: Module.Entity }` | 9.4+ | Yes | Yes | Yes | -| Page parameters (primitive) | `Params: { $Qty: Integer }` | -- | -- | -- | 11.6+ | -| Page variables | `Variables: { ... }` | -- | 10.20+ | 10.20+ | Yes | -| Design properties (Atlas v3) | `DesignProperties: [...]` | -- | -- | -- | Yes | +| Page parameters (entity) | `Params: ( $Item: Module.Entity )` | 9.4+ | Yes | Yes | Yes | +| Page parameters (primitive) | `Params: ( $Qty: Integer )` | -- | -- | -- | 11.6+ | +| Page variables | `Variables: ( ... )` | -- | 10.20+ | 10.20+ | Yes | +| Design properties (Atlas v3) | `DesignProperties: (...)` | -- | -- | -- | Yes | ::: tip Widget Templates Pluggable widget templates are currently extracted from Mendix 11.6. When used on 10.x projects, the [MPK augmentation system](../internals/widget-templates.md) reconciles property differences. Some CE0463 ("widget definition changed") errors may still occur. @@ -102,8 +102,8 @@ Pluggable widget templates are currently extracted from Mendix 11.6. When used o |---------|-----------|-------|-------|-------|-------|-------| | OData client | `CREATE CONSUMED ODATA SERVICE` | Yes | Yes | Yes | Yes | Yes | | Business events | `CREATE BUSINESS EVENT SERVICE` | Yes | Yes | Yes | Yes | Yes | -| REST client (basic) | `CREATE CONSUMED REST SERVICE ... BEGIN ... END` | -- | Yes | Yes | Yes | Yes | -| REST client headers | `HEADER 'Name' = 'Value'` | -- | -- | Yes | Yes | Yes | +| REST client (basic) | `CREATE CONSUMED REST SERVICE ... ( ... ) { OPERATION ... }` | -- | Yes | Yes | Yes | Yes | +| REST client headers | `Headers: ('Name': 'Value')` | -- | -- | Yes | Yes | Yes | | Database Connector | `CREATE DATABASE CONNECTION` | -- | -- | -- | Yes | Yes | | REST client query params | `QUERY $param: Type` | -- | -- | -- | -- | Yes | diff --git a/docs-site/src/examples/crm-module.md b/docs-site/src/examples/crm-module.md index d68f0e6d5b..bdeeb4a0dc 100644 --- a/docs-site/src/examples/crm-module.md +++ b/docs-site/src/examples/crm-module.md @@ -131,7 +131,7 @@ CREATE PAGE CRM.Customer_Overview ( -- NewEdit page with validation CREATE PAGE CRM.Customer_NewEdit ( - Params: { $Customer: CRM.Customer }, + Params: ( $Customer: CRM.Customer ), Title: 'Customer', Layout: Atlas_Core.PopupLayout ) { @@ -180,8 +180,8 @@ GRANT VIEW ON PAGE CRM.Customer_Overview TO CRM.User; GRANT VIEW ON PAGE CRM.Customer_NewEdit TO CRM.User; -- User roles -CREATE OR MODIFY USER ROLE CRMUser (System.User, CRM.User); -CREATE OR MODIFY USER ROLE CRMAdmin (System.User, CRM.Admin); +CREATE OR MODIFY USER ROLE CRMUser ( ModuleRoles: (System.User, CRM.User) ); +CREATE OR MODIFY USER ROLE CRMAdmin ( ModuleRoles: (System.User, CRM.Admin) ); -- Demo users for testing CREATE OR MODIFY DEMO USER 'crm_user' PASSWORD 'Password1!' (CRMUser); diff --git a/docs-site/src/examples/data-import.md b/docs-site/src/examples/data-import.md index c8200018db..3a2820baf1 100644 --- a/docs-site/src/examples/data-import.md +++ b/docs-site/src/examples/data-import.md @@ -80,18 +80,19 @@ CREATE NON-PERSISTENT ENTITY Integration.Customer ( / -- Database connection with parameterized query -CREATE DATABASE CONNECTION Integration.LegacyDatabase -TYPE 'PostgreSQL' -CONNECTION STRING @Integration.LegacyDatabase_DBSource -USERNAME @Integration.LegacyDatabase_DBUsername -PASSWORD @Integration.LegacyDatabase_DBPassword -BEGIN - QUERY SearchCustomers - SQL 'SELECT name, email, balance FROM customers WHERE name ILIKE {search}' - PARAMETER search: String DEFAULT '%' - RETURNS Integration.Customer - MAP (name AS Name, email AS Email, balance AS Balance); -END; +CREATE DATABASE CONNECTION Integration.LegacyDatabase ( + Type: 'PostgreSQL', + ConnectionString: @Integration.LegacyDatabase_DBSource, + Username: @Integration.LegacyDatabase_DBUsername, + Password: @Integration.LegacyDatabase_DBPassword +) { + QUERY SearchCustomers ( + Sql: 'SELECT name, email, balance FROM customers WHERE name ILIKE {search}', + Parameters: ( search: String DEFAULT '%' ), + Returns: Integration.Customer, + Map: (Name = name, Email = email, Balance = balance) + ) +}; / -- Microflow that executes the query diff --git a/docs-site/src/examples/master-detail.md b/docs-site/src/examples/master-detail.md index f6e7cda13a..c250eac8ff 100644 --- a/docs-site/src/examples/master-detail.md +++ b/docs-site/src/examples/master-detail.md @@ -16,12 +16,12 @@ CREATE PAGE CRM.Customer_MasterDetail ( TEMPLATE template1 { DYNAMICTEXT name ( Content: '{1}', - ContentParams: [{1} = Name], + ContentParams: ({1} = Name), RenderMode: H4 ) DYNAMICTEXT email ( Content: '{1}', - ContentParams: [{1} = Email] + ContentParams: ({1} = Email) ) } } diff --git a/docs-site/src/examples/rest-integration.md b/docs-site/src/examples/rest-integration.md index b7877e754f..40d66aca05 100644 --- a/docs-site/src/examples/rest-integration.md +++ b/docs-site/src/examples/rest-integration.md @@ -255,7 +255,7 @@ CREATE CONSUMED REST SERVICE Integration.OrdersApi ( Method: GET, Path: '/orders/{id}', Parameters: ($id: String), - Headers: ('Accept' = 'application/json'), + Headers: ('Accept': 'application/json'), Timeout: 30, Response: JSON AS $Result ) @@ -263,7 +263,7 @@ CREATE CONSUMED REST SERVICE Integration.OrdersApi ( OPERATION CreateOrder ( Method: POST, Path: '/orders', - Headers: ('Content-Type' = 'application/json'), + Headers: ('Content-Type': 'application/json'), Body: MAPPING Integration.OrderRequest { customerId = CustomerId, totalAmount = TotalAmount, @@ -278,6 +278,8 @@ CREATE CONSUMED REST SERVICE Integration.OrdersApi ( }; ``` +A header value is a template, like the path: `{Token}` is the operation parameter `Token`, which the operation must declare (`Parameters: ($Token: String)`, else CE7056). An authorization header is `Headers: ('Authorization' = 'Bearer {Token}')`. The expression spelling `'Bearer ' + $Token` is deprecated (MDL-DEPR711) and `mxcli fmt --upgrade` rewrites it; it used to store only `Bearer `. + Use `CREATE OR MODIFY CONSUMED REST SERVICE` to update an existing client without dropping it first: ```sql @@ -290,7 +292,7 @@ CREATE OR MODIFY CONSUMED REST SERVICE Integration.OrdersApi ( Method: GET, Path: '/orders/{id}', Parameters: ($id: String), - Headers: ('Accept' = 'application/json'), + Headers: ('Accept': 'application/json'), Timeout: 60, Response: JSON AS $Result ) diff --git a/docs-site/src/examples/security.md b/docs-site/src/examples/security.md index 10ddba5055..90da1bf1cc 100644 --- a/docs-site/src/examples/security.md +++ b/docs-site/src/examples/security.md @@ -55,9 +55,9 @@ GRANT VIEW ON PAGE Sales.Admin_Dashboard TO Sales.Admin; User roles combine module roles from different modules into a single assignable role: ```sql -CREATE OR MODIFY USER ROLE SalesViewer (System.User, Sales.Viewer); -CREATE OR MODIFY USER ROLE SalesRep (System.User, Sales.User); -CREATE OR MODIFY USER ROLE SalesManager (System.User, Sales.Admin) MANAGE ALL ROLES; +CREATE OR MODIFY USER ROLE SalesViewer ( ModuleRoles: (System.User, Sales.Viewer) ); +CREATE OR MODIFY USER ROLE SalesRep ( ModuleRoles: (System.User, Sales.User) ); +CREATE OR MODIFY USER ROLE SalesManager ( ModuleRoles: (System.User, Sales.Admin), ManageAllRoles: true ); ``` ## Demo Users diff --git a/docs-site/src/examples/validation.md b/docs-site/src/examples/validation.md index 18af118bef..69a96af43a 100644 --- a/docs-site/src/examples/validation.md +++ b/docs-site/src/examples/validation.md @@ -72,7 +72,7 @@ The page's Save button calls the action microflow (not the validation microflow ```sql CREATE PAGE Sales.Order_Edit ( - Params: { $Order: Sales.Order }, + Params: ( $Order: Sales.Order ), Title: 'Order', Layout: Atlas_Core.PopupLayout ) { diff --git a/docs-site/src/language/basics.md b/docs-site/src/language/basics.md index 9601bd1029..0e8a42ed16 100644 --- a/docs-site/src/language/basics.md +++ b/docs-site/src/language/basics.md @@ -122,6 +122,18 @@ A construct with no mechanical rewrite is reported with the reason, and `fmt` re The design is in [ADR-0011](https://github.com/mendixlabs/mxcli/blob/main/docs/13-decisions/0011-mdl-language-versioning.md); `mxcli syntax language-header` has the details. +## Re-runnable Creates: `or modify` and `if not exists` + +A plain `create` fails when the element already exists. Two guards make a script re-runnable, and they mean different things: + +| Statement | Element absent | Element present | +|---|---|---| +| `create page M.P …` | created | error — the script stops | +| `create or modify page M.P …` | created | rewritten to match the statement (identity kept) | +| `create page if not exists M.P …` | created | **left untouched**, reported as skipped | + +`if not exists` goes after the kind's keywords and before the name, on every `create` that names one element — `create microflow if not exists M.MF () …`, `create module if not exists M;`, `create user role if not exists Clerk (M.User);`, `create configuration if not exists 'Default' (…);`. It is not accepted on `annotation`, `index` (use `alter entity … add index if not exists`), `validation rule`, `navigation`, `translations` or `external entities`, which have no single named element to test. Writing `create or modify … if not exists` is refused as `MDL067`: the two guards contradict each other. `describe` never emits `if not exists`. + ## Case Insensitivity All MDL **keywords** are case-insensitive. The following are equivalent: diff --git a/docs-site/src/language/data-binding.md b/docs-site/src/language/data-binding.md index 2200aaa89a..9abfadaa91 100644 --- a/docs-site/src/language/data-binding.md +++ b/docs-site/src/language/data-binding.md @@ -30,7 +30,7 @@ The simplest binding -- connects a DataView to a page parameter: ```sql CREATE PAGE MyModule.Customer_Edit ( - Params: { $Customer: MyModule.Customer }, + Params: ( $Customer: MyModule.Customer ), Title: 'Edit Customer', Layout: Atlas_Core.PopupLayout ) diff --git a/docs-site/src/language/document-access.md b/docs-site/src/language/document-access.md index 16dd9e3035..9731731c79 100644 --- a/docs-site/src/language/document-access.md +++ b/docs-site/src/language/document-access.md @@ -94,7 +94,7 @@ GRANT EXECUTE ON MICROFLOW Shop.ACT_CreateOrder TO Shop.User, Shop.Admin; -- Create the page CREATE PAGE Shop.Order_Edit ( - Params: { $Order: Shop.Order }, + Params: ( $Order: Shop.Order ), Title: 'Edit Order', Layout: Atlas_Core.PopupLayout ) { ... } diff --git a/docs-site/src/language/grant-revoke.md b/docs-site/src/language/grant-revoke.md index 8bd921be68..123565e6cf 100644 --- a/docs-site/src/language/grant-revoke.md +++ b/docs-site/src/language/grant-revoke.md @@ -154,9 +154,9 @@ CREATE MODULE ROLE Shop.User DESCRIPTION 'Standard access'; CREATE MODULE ROLE Shop.Viewer DESCRIPTION 'Read-only access'; -- User roles -CREATE USER ROLE Administrator (Shop.Admin, System.Administrator) MANAGE ALL ROLES; -CREATE USER ROLE Employee (Shop.User); -CREATE USER ROLE Guest (Shop.Viewer); +CREATE USER ROLE Administrator ( ModuleRoles: (Shop.Admin, System.Administrator), ManageAllRoles: true ); +CREATE USER ROLE Employee ( ModuleRoles: (Shop.User) ); +CREATE USER ROLE Guest ( ModuleRoles: (Shop.Viewer) ); -- Entity access GRANT CREATE, DELETE, READ *, WRITE * ON ENTITY Shop.Customer TO Shop.Admin; diff --git a/docs-site/src/language/home-pages.md b/docs-site/src/language/home-pages.md index 65ae2084ec..4e975f3fdc 100644 --- a/docs-site/src/language/home-pages.md +++ b/docs-site/src/language/home-pages.md @@ -58,24 +58,26 @@ CREATE OR REPLACE NAVIGATION Responsive ## Menus -The `MENU` block defines the navigation menu as a tree of items and submenus. +The `{ }` block after the profile's clauses defines the navigation menu as a tree of items and submenus. ### Menu Items -A menu item links a label to a page: +A menu item links a label to a page, a microflow, or sign-out, in the words a page action uses: ```sql -MENU ITEM '