From 813af0874d5e87f403015e6ceaa6d5d617f1abea Mon Sep 17 00:00:00 2001 From: Ako Date: Sun, 27 Sep 2026 18:43:54 +0000 Subject: [PATCH 01/11] fix(visitor): a member path after * / div / mod is navigation, not division (#768) `/` shares a precedence level with `*`, `div` and `mod`, so `$a/X * $b/Y` parsed as `(($a/X) * $b) / Y` and was stored as `$a/X * $b / Y` (and, in a list filter, `$b / $currentObject/Y`). When a `/ Member` step cannot attach to the whole left side, attach it to the last operand of the preceding multiplicative expression. Mendix has no `/` division. Co-Authored-By: Claude Opus 5.5 --- .../fix-issue/findings/mdl-visitor.jsonl | 1 + .../member_path_after_operator_test.go | 103 ++++++++++++++++++ mdl/visitor/visitor_microflow_expression.go | 29 +++++ 3 files changed, 133 insertions(+) create mode 100644 mdl/executor/member_path_after_operator_test.go diff --git a/.claude/skills/fix-issue/findings/mdl-visitor.jsonl b/.claude/skills/fix-issue/findings/mdl-visitor.jsonl index 78b509ca3..81eedde09 100644 --- a/.claude/skills/fix-issue/findings/mdl-visitor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-visitor.jsonl @@ -36,3 +36,4 @@ {"area":"mdl-visitor","date":"2026-09-24","refs":["#653"],"symptom":"describe entity on a view entity, exec'd back, was never idempotent: \"Each cycle reports `Modified view entity` and stores the query with every line after the first indented two spaces further.\" Re-exec of the SAME described file reported Unchanged, so it looked stable until you described again","cause":"The two directions did not mirror: describe (cmd_entities_describe.go, and cmd_diff_mdl.go) prefixes two spaces to every stored OQL line; the visitor stored extractOriginalText(oqlCtx), which starts at the query's first token, so line 1 lost its indentation and lines 2…n kept all of it — +2 per cycle","file":"`mdl/visitor/visitor_entity.go` (dedentOQL, leadingLineWhitespace); tests `mdl/visitor/visitor_view_entity_oql_indent_test.go`, `mdl/executor/view_entity_oql_roundtrip_test.go`; bug-test `mdl-examples/bug-tests/653-view-entity-oql-indent-drift.mdl`","insight":"**Any verbatim-source capture that starts at the first token is asymmetric**: line 1 is dedented for free, the continuation lines are not. Fix it on the way IN (exec), not by making describe emit less: strip the common leading-whitespace prefix of the non-blank lines, counting line 1 at its column when only whitespace precedes it (read it from the input stream, start.GetStart()-GetColumn()). Compare prefixes byte-wise, not by width, so a Studio Pro query indented with tabs comes back byte-identical under describe's two spaces. A round-trip test must run describe → exec at least twice AND start from stored text mxcli did not write (flat, tabs, blank lines, comments): one pass from a script is exactly how this went unnoticed. Control: stubbing dedentOQL to return raw fails every round-trip case with lines 2…n two spaces deeper; a real 11.12.1 project with the old binary printed Modified ×3 with growing indent, the fixed one Unchanged ×3. Separate, not fixed here: a comment AFTER the query's last token is outside the captured span and is dropped on exec"} {"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": []} diff --git a/mdl/executor/member_path_after_operator_test.go b/mdl/executor/member_path_after_operator_test.go new file mode 100644 index 000000000..abd190d6e --- /dev/null +++ b/mdl/executor/member_path_after_operator_test.go @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/visitor" + "github.com/mendixlabs/mxcli/sdk/microflows" +) + +// A member path after a multiplicative operator (`$a/X * $b/Y`) must be stored +// exactly as written. `/` sits at the same precedence as `*` in the MDL grammar, +// so the left-associative chain used to become `($a/X * $b) / Y`: the serializer +// then printed `$b / Y` as division, and a filter's iterator qualification read +// the stranded `Y` as a bare attribute and wrote `$b / $currentObject/Y`. The +// build and mx check stayed green; Mendix evaluates a different expression. +func TestMemberPathAfterMultiplicativeOperatorIsStoredAsWritten(t *testing.T) { + const src = `create microflow MyFirstModule.Agg0 ($Feedbacks: List of FeedbackModule.Feedback, $One: FeedbackModule.Feedback) +returns Integer +begin + $Area = sum($Feedbacks, $currentObject/ScreenWidth * $currentObject/ScreenHeight); + $W = filter($Feedbacks, $currentObject/ScreenWidth * $currentObject/ScreenHeight > 3); + declare $X Integer = $One/ScreenWidth * $One/ScreenHeight; + declare $D Decimal = $One/ScreenWidth div $One/ScreenHeight * $One/Rating; + declare $N Integer = 2 * -$One/ScreenWidth; + return $Area; +end;` + got := storedExpressions(t, src) + + for _, want := range []string{ + "sum: $currentObject/ScreenWidth * $currentObject/ScreenHeight", + "filter: $currentObject/ScreenWidth * $currentObject/ScreenHeight > 3", + "declare X: $One/ScreenWidth * $One/ScreenHeight", + // div binds like *; the member path after it is navigation too. + "declare D: $One/ScreenWidth div $One/ScreenHeight * $One/Rating", + // A unary minus in front of the path keeps the path whole (already + // correct before the fix; guards against a regression). + "declare N: 2 * -$One/ScreenWidth", + } { + if !containsString(got, want) { + t.Errorf("stored expression %q missing; stored: %q", want, got) + } + } +} + +// Control: division by a parenthesised expression or a variable is not member +// navigation and must not be re-associated. +func TestSlashAfterNonPathIsNotMadeAMemberPath(t *testing.T) { + const src = `create microflow MyFirstModule.Agg1 ($One: FeedbackModule.Feedback) +returns Integer +begin + declare $P Decimal = $One/ScreenWidth * ($One/ScreenHeight) div 2; + return 1; +end;` + got := storedExpressions(t, src) + want := "declare P: $One/ScreenWidth * ($One/ScreenHeight) div 2" + if !containsString(got, want) { + t.Errorf("stored expression %q missing; stored: %q", want, got) + } +} + +// storedExpressions builds each statement of a microflow through the flow +// builder and returns the expression strings the actions would store. +func storedExpressions(t *testing.T, src string) []string { + t.Helper() + prog, errs := visitor.Build(src) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + mf, ok := prog.Statements[0].(*ast.CreateMicroflowStmt) + if !ok { + t.Fatalf("expected a CreateMicroflowStmt, got %T", prog.Statements[0]) + } + fb := &flowBuilder{ + posX: 100, posY: 100, spacing: HorizontalSpacing, + varTypes: map[string]string{"Feedbacks": "List of FeedbackModule.Feedback", "One": "FeedbackModule.Feedback"}, + declaredVars: map[string]string{}, + measurer: &layoutMeasurer{varTypes: map[string]string{"Feedbacks": "List of FeedbackModule.Feedback"}}, + } + for _, stmt := range mf.Body { + fb.addStatement(stmt) + } + var got []string + for _, obj := range fb.objects { + act, ok := obj.(*microflows.ActionActivity) + if !ok { + continue + } + switch a := act.Action.(type) { + case *microflows.AggregateListAction: + got = append(got, "sum: "+a.Expression) + case *microflows.ListOperationAction: + if f, ok := a.Operation.(*microflows.FilterOperation); ok { + got = append(got, "filter: "+f.Expression) + } + case *microflows.CreateVariableAction: + got = append(got, "declare "+a.VariableName+": "+a.InitialValue) + } + } + return got +} diff --git a/mdl/visitor/visitor_microflow_expression.go b/mdl/visitor/visitor_microflow_expression.go index 3d32c4efd..b032c8a38 100644 --- a/mdl/visitor/visitor_microflow_expression.go +++ b/mdl/visitor/visitor_microflow_expression.go @@ -256,6 +256,9 @@ func buildMultiplicativeExpression(ctx parser.IMultiplicativeExpressionContext) result = pathExpr continue } + if attachPathToLastOperand(result, right) { + continue + } } result = &ast.BinaryExpr{ @@ -268,6 +271,32 @@ func buildMultiplicativeExpression(ctx parser.IMultiplicativeExpressionContext) return result } +// attachPathToLastOperand makes `/ Member` a member step of the operand it +// belongs to when a multiplicative operator came first. `/` shares a precedence +// level with `*`, `div` and `mod`, so `$a/X * $b/Y` is parsed left to right as +// `(($a/X) * $b) / Y`: by the time the `/` is seen, `$b` is already the right +// operand of the product, and tryBuildAttributePath (which looks at the whole left +// side) cannot reach it. Mendix has no `/` division — it is always navigation +// when a member name follows — so the step moves onto `$b`, giving +// `$a/X * ($b/Y)`, the tree the source means. Reports whether the step was +// attached. +func attachPathToLastOperand(left ast.Expression, right ast.Expression) bool { + bin, ok := left.(*ast.BinaryExpr) + if !ok { + return false + } + switch bin.Operator { + case "*", "div", "mod", "%", ":": + default: + return false + } + if pathExpr := tryBuildAttributePath(bin.Right, right); pathExpr != nil { + bin.Right = pathExpr + return true + } + return false +} + // tryBuildAttributePath attempts to build an AttributePathExpr from a left expression // and a right identifier. Returns nil if not an XPath-style path. func tryBuildAttributePath(left ast.Expression, right ast.Expression) *ast.AttributePathExpr { From 258d7021a2178ece28234f697211bfb95c90664a Mon Sep 17 00:00:00 2001 From: Ako Date: Sun, 27 Sep 2026 19:31:33 +0000 Subject: [PATCH 02/11] R4: one argument form (Param = expr) and one text-template form (with ({n} = e)) (#751) Every call site binds an argument as `Param = expression`: call microflow/nanoflow/java/javascript/external action, web service, execute database query, send rest request, show page, and page/button actions and flow data sources. A workflow's call microflow / call workflow takes its arguments right after the callee as bare expressions. show message and validation feedback take `with ({1} = e)`. The old spellings stay as registered deprecated aliases with fmt --upgrade rewrites computed from the parse tree: MDL-DEPR006 $Param = e MDL-DEPR007 Param: e MDL-DEPR008 workflow `with (Param = '')` (same meaning: the string's content is the expression) MDL-DEPR009 objects [..] / parameters [..] describe writes the canonical forms, and now also writes a validation feedback's template arguments, which it dropped. A stored workflow expression that does not read back as itself when bare keeps the string form in describe. Skills, docs-site, the quick reference and `mxcli syntax` are migrated. Co-Authored-By: Claude Opus 5.5 --- .../fix-issue/findings/mdl-executor.jsonl | 1 + .../skills/mendix/cheatsheet-errors/SKILL.md | 2 +- .claude/skills/mendix/check-syntax/SKILL.md | 2 +- .../mendix/create-page/reference/widgets.md | 26 +- .../skills/mendix/migrate-k2-nintex/SKILL.md | 4 +- .claude/skills/mendix/overview-pages/SKILL.md | 18 +- .claude/skills/mendix/patterns-crud/SKILL.md | 6 +- .../resolve-forward-references/SKILL.md | 4 +- .../mendix/validation-microflows/SKILL.md | 6 +- .../skills/mendix/write-microflows/SKILL.md | 18 +- .../write-microflows/reference/integration.md | 4 + .../skills/mendix/write-nanoflows/SKILL.md | 8 +- .../skills/mendix/write-workflows/SKILL.md | 17 +- CHANGELOG.md | 1 + cmd/mxcli/syntax/features_microflow.go | 8 +- cmd/mxcli/syntax/features_page.go | 6 +- cmd/mxcli/syntax/features_workflow.go | 24 +- docs-site/src/appendixes/quick-reference.md | 4 +- docs-site/src/examples/crm-module.md | 2 +- docs-site/src/examples/validation.md | 2 +- docs-site/src/language/activity-types.md | 20 +- docs-site/src/language/data-binding.md | 2 +- docs-site/src/language/microflow-patterns.md | 2 +- docs-site/src/language/microflows.md | 2 +- docs-site/src/language/nanoflows.md | 6 +- docs-site/src/language/page-patterns.md | 4 +- docs-site/src/language/qualified-names.md | 2 +- docs-site/src/language/widget-types.md | 2 +- docs-site/src/language/workflow-activities.md | 18 +- docs-site/src/migration/validation.md | 2 +- .../reference/microflow/create-microflow.md | 4 +- .../reference/microflow/create-nanoflow.md | 2 +- docs-site/src/reference/page/create-page.md | 6 +- .../src/reference/query/describe-microflow.md | 2 +- docs-site/src/tutorial/create-page.md | 2 +- docs/01-project/MDL_QUICK_REFERENCE.md | 20 +- mdl/deprecation/deprecation.go | 63 +++ mdl/executor/cmd_microflows_format_action.go | 25 +- .../cmd_microflows_format_action_test.go | 2 +- mdl/executor/cmd_pages_describe_datasource.go | 2 +- .../cmd_pages_describe_datasource_test.go | 6 +- .../cmd_pages_describe_flow_args_test.go | 6 +- mdl/executor/cmd_pages_describe_output.go | 11 +- .../cmd_pages_describe_pageparams_test.go | 2 +- mdl/executor/cmd_workflows.go | 74 ++-- .../describe_argument_binding_test.go | 119 ++++++ mdl/executor/validate_workflow_agent_test.go | 2 +- mdl/grammar/domains/MDLMicroflow.g4 | 27 +- mdl/grammar/domains/MDLPage.g4 | 12 +- mdl/grammar/domains/MDLWorkflow.g4 | 20 +- mdl/upgrade/argument_binding_test.go | 82 ++++ mdl/visitor/visitor_argument_binding.go | 388 ++++++++++++++++++ mdl/visitor/visitor_argument_binding_test.go | 178 ++++++++ mdl/visitor/visitor_microflow_actions.go | 26 +- mdl/visitor/visitor_page_v3.go | 9 +- mdl/visitor/visitor_workflow.go | 10 +- 56 files changed, 1119 insertions(+), 204 deletions(-) create mode 100644 mdl/executor/describe_argument_binding_test.go create mode 100644 mdl/upgrade/argument_binding_test.go create mode 100644 mdl/visitor/visitor_argument_binding.go create mode 100644 mdl/visitor/visitor_argument_binding_test.go diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index 713002bfb..d05e03d1d 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -735,3 +735,4 @@ {"area": "mdl/executor", "date": "2026-09-27", "symptom": "describe of a published REST service with no resources printed `create or modify published rest service M.S ( \u2026 )` followed by a bare `;`, which does not parse (`mismatched input ';' expecting '{'`); found while making describe end every statement with `;` (ako/mxcli#744).", "cause": "The resource block `{ \u2026 }` is mandatory in the grammar (publishedRestService: \u2026 RPAREN LBRACE publishedRestResource* RBRACE), but the describer only printed it when the service had resources and otherwise closed the statement with `;` alone.", "file": "mdl/executor/cmd_published_rest.go", "fix": "Always print the block: `{\\n};` when there are no resources.", "insight": "A describe test that asserts substrings never parses the output; feeding every Describe*_Mock output through the parser (assertTerminated, #744) found this at once. The empty-collection branch of a describer is the one no fixture exercises.", "test": "TestDescribePublishedRestService_Mock (assertTerminated)"} {"area": "mdl/executor", "date": "2026-09-27", "symptom": "Running an external entity's describe output (create or modify external entity, either form) turns each attribute's Rest$ODataMappedValue into a DomainModels$StoredValue (RemoteName/RemoteType/Filterable... gone), re-mints attribute $IDs on the `from odata client` form, and rewrites String(36) as unlimited", "cause": "Both rewrite paths rebuilt attributes from the AST, which has no spelling for the OData mapping; the `from odata client` path minted fresh IDs; describe external entity printed GetTypeName() (`String`) instead of formatAttributeType", "file": "mdl/executor/cmd_entities.go (carryStoredAttributeState), cmd_odata.go", "insight": "Carry per attribute by name what the statement cannot spell, in the shared merge, and enumerate every path that rebuilds attributes: there were two statements for one document kind", "refs": ["ako/mxcli#743"]} {"area": "mdl/executor", "date": "2026-09-27", "symptom": "create or modify odata service over a Studio Pro service: EntitySet PageSize 10000 -> 0, entity sets reordered, member CanBeEmpty recomputed, ExportLevel deleted, AuthenticationTypes marker 1 -> 3", "cause": "The modify path rebuilt entity types/sets from the publish block (no spelling for PageSize without paging, set order, CanBeEmpty); the model had no ExportLevel; the writer used a marker-3 list copied from the legacy writer, not Studio Pro", "file": "mdl/executor/cmd_odata.go (carryPublishedEntityState), mdl/backend/modelsdk/odata_write.go", "insight": "Parity with the legacy writer is not evidence of what Studio Pro writes; measure markers on a Studio Pro fixture. A probe that runs describe output with the verb rewritten to create or modify (mdl/roundtrip TestTestAppCreateOrModifyProbe) shows what a verb switch would lose before switching it", "refs": ["ako/mxcli#743"]} +{"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)."} diff --git a/.claude/skills/mendix/cheatsheet-errors/SKILL.md b/.claude/skills/mendix/cheatsheet-errors/SKILL.md index 8565ba180..90b496a68 100644 --- a/.claude/skills/mendix/cheatsheet-errors/SKILL.md +++ b/.claude/skills/mendix/cheatsheet-errors/SKILL.md @@ -265,7 +265,7 @@ Run with `-p` for the fullest coverage. | CE0104 | Action activity is unreachable | Code after RETURN | | CE0105 | Must end with end event | Missing RETURN | | CE0117 | Error in expression | Unqualified association path | -| CE1571 | No argument selected for parameter | A microflow/nanoflow call with a parameter nothing fills — as a `datasource:` **or** an `action:`. Give it an argument (`action: nanoflow M.NF($P = $value)`), or nest the widget in a data container of the parameter's type. `check -p` reports both | +| CE1571 | No argument selected for parameter | A microflow/nanoflow call with a parameter nothing fills — as a `datasource:` **or** an `action:`. Give it an argument (`action: nanoflow M.NF(P = $value)`), or nest the widget in a data container of the parameter's type. `check -p` reports both | | CE1571 | …in a control bar | A control bar is **not** row-scoped, so the grid's row does not fill it: pass the grid's selection (`$dgOrders`, with `Selection:` set) or move the widget into a column. `$currentObject` there is MDL-BUTTON01 | | CE1834 | The 'Page' property is required | Workflow user task without a `page` — `check` flags MDL-WF01 | | CE1876 | Single outcome must not contain activities | Single-outcome user task with a nested activity flow — `check` flags MDL-WF02 | diff --git a/.claude/skills/mendix/check-syntax/SKILL.md b/.claude/skills/mendix/check-syntax/SKILL.md index dbed9603e..1dd142eb4 100644 --- a/.claude/skills/mendix/check-syntax/SKILL.md +++ b/.claude/skills/mendix/check-syntax/SKILL.md @@ -249,7 +249,7 @@ Before writing any MDL, verify these requirements: - `retrieve $Var from Module.Entity [where condition];` - `$Result = call microflow Module.Name (Param = $value);` (NOT `set $Result = ...`) - `$Result = call nanoflow Module.Name (Param = $value);` -- `show page Module.PageName ($Param = $value);` +- `show page Module.PageName (Param = $value);` - `close page;` - `validation feedback $entity/attribute message 'message';` - `log info|warning|error [node 'name'] 'message';` diff --git a/.claude/skills/mendix/create-page/reference/widgets.md b/.claude/skills/mendix/create-page/reference/widgets.md index a2a305959..74edc8f64 100644 --- a/.claude/skills/mendix/create-page/reference/widgets.md +++ b/.claude/skills/mendix/create-page/reference/widgets.md @@ -123,10 +123,9 @@ describe icon collection Atlas_Core.Atlas_Filled -- every icon + its reference - `action: close_page` - Close the page - `action: delete` - Delete object - `action: microflow Module.MicroflowName` - Call microflow -- `action: microflow Module.MicroflowName(Param: $value)` - Call microflow with parameters +- `action: microflow Module.MicroflowName(Param = $value)` - Call microflow with parameters - `action: nanoflow Module.NanoflowName` - Call nanoflow (client-side) -- `action: nanoflow Module.NanoflowName(Param: $value)` - Call nanoflow with parameters -- `action: nanoflow Module.NanoflowName($Param = $value)` - Also accepted (microflow-style) +- `action: nanoflow Module.NanoflowName(Param = $value)` - Call nanoflow with parameters - `action: open_link 'https://example.com'` - Open a fixed web address - `action: open_link $currentObject/URL` - Open the address held in an attribute of the enclosing data container's object (inside a data container only; not over an association) - **Every parameter needs an argument, or an enclosing data container of its @@ -134,13 +133,12 @@ describe icon collection Atlas_Core.Atlas_Filled -- every icon + its reference check -p` reports it. This is the same on every widget that takes an action, a clickable `container` included. - `action: show_page Module.PageName` - Navigate to page -- `action: show_page Module.PageName(Param: $value)` - Navigate with parameters -- `action: show_page Module.PageName($Param = $value)` - Also accepted (microflow-style) +- `action: show_page Module.PageName(Param = $value)` - Navigate with parameters - `action: create_object Module.Entity then show_page Module.PageName` - Create and navigate - **A `show_page` argument must be the context object, and there has to BE one.** Mendix takes the page argument from the enclosing data widget, so the only spellings that mean anything are `$currentObject` or the name of the variable - that widget is bound to (`datasource: $Customer` → `(Customer: $Customer)` is + that widget is bound to (`datasource: $Customer` → `(Customer = $Customer)` is fine). Naming any other variable is refused as **MDL-PAGEARG01** — it used to be accepted and silently opened the page with the context object anyway. - **Outside a data widget the same rule leaves nothing at all**, so a button sitting @@ -150,7 +148,7 @@ describe icon collection Atlas_Core.Atlas_Filled -- every icon + its reference mxbuild reports **CE1571** per parameter of the target page, and a page whose parameters are optional would simply show the wrong data. MDL-PAGEARG01 refuses that too (mendixlabs/mxcli#1029). To open a parameterised page from such a - button, call a microflow that does `show page Module.Page(Param: $value)` — + button, call a microflow that does `show page Module.Page(Param = $value)` — that path wires the arguments properly. - **The list above is the whole vocabulary, and a keyword without its argument is not in it.** `action: open_link` with no URL, `action: show_page` with no page, @@ -173,16 +171,16 @@ describe icon collection Atlas_Core.Atlas_Filled -- every icon + its reference actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: primary) -- Navigate with parameter (inside DATAVIEW) -actionbutton btnEdit (caption: 'Edit', action: show_page Module.EditPage(Product: $Product)) +actionbutton btnEdit (caption: 'Edit', action: show_page Module.EditPage(Product = $Product)) -- Navigate with $currentObject (inside DATAGRID column) -actionbutton btnEdit (caption: 'Edit', action: show_page Module.EditPage(Product: $currentObject)) +actionbutton btnEdit (caption: 'Edit', action: show_page Module.EditPage(Product = $currentObject)) -- Call microflow with page/dataview parameter -actionbutton btnProcess (caption: 'Process', action: microflow Module.ACT_Process(Order: $Order), buttonstyle: success) +actionbutton btnProcess (caption: 'Process', action: microflow Module.ACT_Process(Order = $Order), buttonstyle: success) -- Call microflow with $currentObject (inside DATAGRID/LISTVIEW column) -actionbutton btnDelete (caption: 'Delete', action: microflow Module.ACT_Delete(Target: $currentObject), buttonstyle: danger) +actionbutton btnDelete (caption: 'Delete', action: microflow Module.ACT_Delete(Target = $currentObject), buttonstyle: danger) -- Create object and show page actionbutton btnNew (caption: 'New', action: create_object Module.Product then show_page Module.Product_Edit, buttonstyle: primary) @@ -645,7 +643,7 @@ Create a menu with action items: ```sql navigationlist navName { - item itemEdit (caption: 'Edit', action: show_page Module.EditPage(entity: $EntityParameter)) + item itemEdit (caption: 'Edit', action: show_page Module.EditPage(entity = $EntityParameter)) item itemDelete (caption: 'Delete', action: delete) item itemBack (caption: 'Back', action: close_page) } @@ -947,7 +945,7 @@ as an `actionbutton`'s `action:`: ```sql -- Rich, parameterised trigger: a card that opens the object it represents -container tileCard (OnClick: microflow MyModule.ACT_Open(Item: $currentObject), class: 'tile') { +container tileCard (OnClick: microflow MyModule.ACT_Open(Item = $currentObject), class: 'tile') { dynamictext tileValue (content: '4') dynamictext tileLabel (content: '4 LEFT', class: 'tile-label') } @@ -1013,7 +1011,7 @@ datagrid dgMaterials ( -- Not row-scoped: pass the selection explicitly. container cUnlink ( class: 'command', - action: nanoflow Module.ACT_UnLink ($Material = $dgMaterials) + action: nanoflow Module.ACT_UnLink (Material = $dgMaterials) ) { actionbutton btnUnlink (caption: 'Unlink') } diff --git a/.claude/skills/mendix/migrate-k2-nintex/SKILL.md b/.claude/skills/mendix/migrate-k2-nintex/SKILL.md index 502a4d903..e666fccd5 100644 --- a/.claude/skills/mendix/migrate-k2-nintex/SKILL.md +++ b/.claude/skills/mendix/migrate-k2-nintex/SKILL.md @@ -241,7 +241,7 @@ begin commit $Order; -- Show page for review (like K2 "Task" with form) - show page CRM.Order_Review ($Order = $Order); + show page CRM.Order_Review (Order = $Order); end; -- K2 Decision "Order > $5000?" → Mendix microflow with decision @@ -252,7 +252,7 @@ begin if $Order/TotalAmount > 5000 then -- Route to manager (K2 Destination Rule equivalent) - call microflow CRM.ACT_Order_SubmitForManagerReview ($Order = $Order); + call microflow CRM.ACT_Order_SubmitForManagerReview (Order = $Order); else -- Auto-approve (K2 "Go To" equivalent) change $Order (status = CRM.OrderStatus.Approved); diff --git a/.claude/skills/mendix/overview-pages/SKILL.md b/.claude/skills/mendix/overview-pages/SKILL.md index 2fdc531dd..19d77f784 100644 --- a/.claude/skills/mendix/overview-pages/SKILL.md +++ b/.claude/skills/mendix/overview-pages/SKILL.md @@ -105,7 +105,7 @@ create page Module.Entity_Overview textfilter textFilter2 } column (caption: 'Actions') { - actionbutton btnEdit (caption: 'Edit', action: show_page Module.Entity_NewEdit("entity": $currentObject)) + actionbutton btnEdit (caption: 'Edit', action: show_page Module.Entity_NewEdit("entity" = $currentObject)) actionbutton btnDelete (caption: 'Delete', action: delete, buttonstyle: danger) } } @@ -147,7 +147,7 @@ datagrid GridName ( textfilter textFilter1 } column (caption: 'Actions') { - actionbutton btnEdit (caption: 'Edit', action: show_page Module.Entity_NewEdit("entity": $currentObject)) + actionbutton btnEdit (caption: 'Edit', action: show_page Module.Entity_NewEdit("entity" = $currentObject)) } } ``` @@ -276,8 +276,8 @@ create page Module.PageName - Parameter name conventionally matches the entity name (e.g., `$store`, `$Customer`) - The DataView's binding references this parameter (`datasource: $ParamName`) -- When calling the page via SHOW_PAGE, pass the object by parameter name: `show_page Module.PageName(ParamName: $value)` -- **Reserved-word parameter names must be quoted in the SHOW_PAGE args.** The generic examples above name the parameter `entity`, which is a reserved keyword, so the call quotes it: `show_page Module.Entity_NewEdit("entity": $currentObject)`. A non-reserved name (`$store`, `$Customer`) needs no quotes: `show_page Module.Store_NewEdit(store: $currentObject)`. +- When calling the page via SHOW_PAGE, pass the object by parameter name: `show_page Module.PageName(ParamName = $value)` +- **Reserved-word parameter names must be quoted in the SHOW_PAGE args.** The generic examples above name the parameter `entity`, which is a reserved keyword, so the call quotes it: `show_page Module.Entity_NewEdit("entity" = $currentObject)`. A non-reserved name (`$store`, `$Customer`) needs no quotes: `show_page Module.Store_NewEdit(store = $currentObject)`. ### NewEdit Page Components @@ -535,7 +535,7 @@ create snippet Module.Entity_Menu ) { navigationlist EntityMenuNav { - item itemEdit (caption: 'Edit', action: show_page Module.Entity_NewEdit("entity": $EntityParameter)) + item itemEdit (caption: 'Edit', action: show_page Module.Entity_NewEdit("entity" = $EntityParameter)) item itemDelete (caption: 'Delete', action: delete) item itemBack (caption: 'Back', action: close_page) } @@ -556,13 +556,13 @@ navigationlist widgetName { - `action: close_page` - Close current page - `action: delete` - Delete object - `action: microflow Module.MicroflowName` - Call microflow -- `action: microflow Module.MicroflowName(Param: $value)` - Call microflow with parameters +- `action: microflow Module.MicroflowName(Param = $value)` - Call microflow with parameters - `action: show_page Module.PageName` - Navigate to page -- `action: show_page Module.PageName(Param: $value)` - Navigate with parameters +- `action: show_page Module.PageName(Param = $value)` - Navigate with parameters - **A `show_page` argument must be the context object, and there has to BE one.** Mendix takes the page argument from the enclosing data widget, so the only spellings that mean anything are `$currentObject` or the name of the variable - that widget is bound to (`datasource: $Customer` → `(Customer: $Customer)` is + that widget is bound to (`datasource: $Customer` → `(Customer = $Customer)` is fine). Naming any other variable is refused as **MDL-PAGEARG01** — it used to be accepted and silently opened the page with the context object anyway. - **Outside a data widget the same rule leaves nothing at all**, so a button sitting @@ -572,7 +572,7 @@ navigationlist widgetName { mxbuild reports **CE1571** per parameter of the target page, and a page whose parameters are optional would simply show the wrong data. MDL-PAGEARG01 refuses that too (mendixlabs/mxcli#1029). To open a parameterised page from such a - button, call a microflow that does `show page Module.Page(Param: $value)` — + button, call a microflow that does `show page Module.Page(Param = $value)` — that path wires the arguments properly. ## Handling Circular Dependencies diff --git a/.claude/skills/mendix/patterns-crud/SKILL.md b/.claude/skills/mendix/patterns-crud/SKILL.md index 40e5429d9..b20fbd8aa 100644 --- a/.claude/skills/mendix/patterns-crud/SKILL.md +++ b/.claude/skills/mendix/patterns-crud/SKILL.md @@ -35,7 +35,7 @@ returns boolean begin -- Validate first declare $IsValid boolean = true; - $IsValid = call microflow Module.VAL_Customer_Save($Customer = $Customer); + $IsValid = call microflow Module.VAL_Customer_Save(Customer = $Customer); if $IsValid then commit $Customer; @@ -153,7 +153,7 @@ begin CreatedDate = [%CurrentDateTime%] ); - show page Module.Customer_NewEdit ($Customer = $NewCustomer); + show page Module.Customer_NewEdit (Customer = $NewCustomer); return true; end; / @@ -198,7 +198,7 @@ create microflow Module.ACT_Customer_Edit ( ) returns boolean begin - show page Module.Customer_NewEdit ($Customer = $Customer); + show page Module.Customer_NewEdit (Customer = $Customer); return true; end; / diff --git a/.claude/skills/mendix/resolve-forward-references/SKILL.md b/.claude/skills/mendix/resolve-forward-references/SKILL.md index e72f1f00e..e71ea72a6 100644 --- a/.claude/skills/mendix/resolve-forward-references/SKILL.md +++ b/.claude/skills/mendix/resolve-forward-references/SKILL.md @@ -198,7 +198,7 @@ create page MyModule.Order_Detail create microflow MyModule.ACT_OpenOrder ($Order: MyModule.Order) begin @position(200,200) - show page MyModule.Order_Detail ($Order = $Order); + show page MyModule.Order_Detail (Order = $Order); @position(400,200) return; end; / @@ -313,7 +313,7 @@ begin @position(200,200) $c = create MyModule.Customer; @position(400,200) - show page MyModule.Customer_NewEdit ($Customer = $c); + show page MyModule.Customer_NewEdit (Customer = $c); @position(600,200) return; end; / diff --git a/.claude/skills/mendix/validation-microflows/SKILL.md b/.claude/skills/mendix/validation-microflows/SKILL.md index 1f7e0181e..c3a2ece3a 100644 --- a/.claude/skills/mendix/validation-microflows/SKILL.md +++ b/.claude/skills/mendix/validation-microflows/SKILL.md @@ -42,7 +42,7 @@ validation feedback $VariableName/attributename message 'Error message'; With template arguments (for dynamic messages): ```mdl -validation feedback $VariableName/attributename message '{1}' objects [$MessageVariable]; +validation feedback $VariableName/attributename message '{1}' with ({1} = $MessageVariable); ``` ### CLOSE PAGE Statement @@ -131,7 +131,7 @@ returns boolean as $IsValid folder 'OverviewPages' begin -- Call validation microflow - $IsValid = call microflow MdlTemplates.VAL_Car_NewEdit($param = $Car); + $IsValid = call microflow MdlTemplates.VAL_Car_NewEdit(param = $Car); -- Only save if validation passed if $IsValid then @@ -240,7 +240,7 @@ end if; if trim($ValidationMessage) != '' then set $IsValid = false; - validation feedback $entity/value message '{1}' objects [$ValidationMessage]; + validation feedback $entity/value message '{1}' with ({1} = $ValidationMessage); end if; ``` diff --git a/.claude/skills/mendix/write-microflows/SKILL.md b/.claude/skills/mendix/write-microflows/SKILL.md index 2b31169d5..03dbfb088 100644 --- a/.claude/skills/mendix/write-microflows/SKILL.md +++ b/.claude/skills/mendix/write-microflows/SKILL.md @@ -524,14 +524,15 @@ When calling microflows, always check the target's parameter list. Use `describe ### SHOW PAGE ```mdl --- Open page with parameter (canonical syntax) -show page Module.EditPage($Product = $Product); - --- Widget-style syntax also accepted in microflows -show page Module.EditPage(Product: $Product); +-- Open page with parameter +show page Module.EditPage(Product = $Product); ``` -Both `($Param = $value)` and `(Param: $value)` syntaxes are accepted in microflow SHOW PAGE statements. Similarly, widget Action: properties accept both `show_page Module.Page(Param: $value)` and `show_page Module.Page($Param = $value)`. +Every call site binds an argument as `Param = expression`, with no `$` on the +parameter name: `call microflow`, `show page`, and widget actions +(`action: show_page Module.Page(Param = $value)`) alike. `$Param = $value` and +`Param: $value` still parse but are deprecated (MDL-DEPR006/007); `mxcli fmt +--upgrade` rewrites them. ### CLOSE PAGE @@ -646,8 +647,7 @@ the short `@annotation 'text'` form. ### Page Navigation Pattern ```mdl -show page Module.Page($Param = $value); -- Canonical -show page Module.Page(Param: $value); -- Widget-style (also valid) +show page Module.Page(Param = $value); close page; show home page; ``` @@ -670,7 +670,7 @@ log info node 'App' 'starting' on error { return; }; show message 'saved' on error { return; }; -- BLOCKING halts the client until dismissed; after `objects`, before `on error`. -show message 'Hello {1}' type Warning objects [$Name] blocking; +show message 'Hello {1}' type Warning with ({1} = $Name) blocking; validation feedback $Order/Total message 'must be positive' on error { return; }; show page Module.Page on error { return; }; close page on error { return; }; diff --git a/.claude/skills/mendix/write-microflows/reference/integration.md b/.claude/skills/mendix/write-microflows/reference/integration.md index b342c8523..36a2fae24 100644 --- a/.claude/skills/mendix/write-microflows/reference/integration.md +++ b/.claude/skills/mendix/write-microflows/reference/integration.md @@ -95,6 +95,10 @@ send rest request Module.ServiceName.OperationName; -- With output variable (RESPONSE JSON operation — maps to entity) $Result = send rest request Module.ServiceName.OperationName; +-- With path/query parameters, bound as at every call site: `Param = expression` +$Result = send rest request Module.ServiceName.GetItem + with (id = $ItemId, lang = 'en'); + -- With request body (POST/PUT operations) $Result = send rest request Module.ServiceName.CreateItem body $NewItem; diff --git a/.claude/skills/mendix/write-nanoflows/SKILL.md b/.claude/skills/mendix/write-nanoflows/SKILL.md index afcae59da..0ded55a99 100644 --- a/.claude/skills/mendix/write-nanoflows/SKILL.md +++ b/.claude/skills/mendix/write-nanoflows/SKILL.md @@ -121,7 +121,7 @@ $JsResult = CALL JAVASCRIPT ACTION NanoflowCommons.SignIn (userName = $Name, pas ### UI Activities ```mdl -SHOW PAGE Sales.CartDetail ($Cart = $Cart); +SHOW PAGE Sales.CartDetail (Cart = $Cart); CLOSE PAGE; SHOW MESSAGE WARNING 'Connection unavailable. Working offline.'; VALIDATION FEEDBACK $Item/Quantity MESSAGE 'Quantity must be at least 1'; @@ -156,7 +156,7 @@ IF $Cart/ItemCount = 0 THEN VALIDATION FEEDBACK $Cart/ItemCount MESSAGE 'Cart is empty'; RETURN false; ELSE - SHOW PAGE Sales.Checkout ($Cart = $Cart); + SHOW PAGE Sales.Checkout (Cart = $Cart); RETURN true; END IF; ``` @@ -217,11 +217,11 @@ CREATE OR MODIFY NANOFLOW Inventory.NAV_OpenProductDetail ( ) FOLDER 'Navigation' BEGIN - $IsValid = CALL NANOFLOW Inventory.NAV_ValidateProduct ($Product = $Product); + $IsValid = CALL NANOFLOW Inventory.NAV_ValidateProduct (Product = $Product); IF NOT ($IsValid) THEN RETURN; END IF; - SHOW PAGE Inventory.ProductDetail ($Product = $Product); + SHOW PAGE Inventory.ProductDetail (Product = $Product); END; ``` diff --git a/.claude/skills/mendix/write-workflows/SKILL.md b/.claude/skills/mendix/write-workflows/SKILL.md index 57972e907..a61060169 100644 --- a/.claude/skills/mendix/write-workflows/SKILL.md +++ b/.claude/skills/mendix/write-workflows/SKILL.md @@ -109,8 +109,7 @@ begin outcomes 'Done' { }; -- Call a microflow (server logic); optional name, parameter mapping + outcomes - call microflow Module.ACT_Validate as callMicroflow1 - with (Module.ACT_Validate.Item = '$WorkflowContext'); + call microflow Module.ACT_Validate(Item = $WorkflowContext) as callMicroflow1; -- Decision: a boolean or enum exclusive split. The name is optional; give one -- when a `jump to` targets it. @@ -466,11 +465,11 @@ values. The full list and the System **entities** are in `system-module`. users` is refused — each omitted clause would reset it. - **An AI agent task is `call agent microflow`** (Mendix 11.9+) — the call microflow statement stored as `Workflows$AIAgentTaskActivity`, with the same - `as`, `comment`, `with (…)`, `outcomes` and boundary events. The microflow is + argument list, `as`, `comment`, `outcomes` and boundary events. The microflow is where the agent is invoked. Measured on mxbuild 11.13 against the identical call microflow, one rule differs: **its microflow must take a parameter** - (`CE1590 "Missing parameter"`), usually the context object mapped with - `with (Param = '$WorkflowContext')`. Return Boolean or an enumeration to + (`CE1590 "Missing parameter"`), usually the context object passed as + `(Param = $WorkflowContext)`. Return Boolean or an enumeration to branch on the answer. - **Handler microflows have fixed signatures** (measured, mxbuild 11.13): - `on created microflow` takes exactly `System.WorkflowUserTask` and the context @@ -525,9 +524,11 @@ values. The full list and the System **entities** are in `system-module`. `REPLACE ACTIVITY`. A **required (`not null`) attribute does not exempt it** — measured, the empty outcome is still required. Boolean (`true`/`false`) decisions do not take one. -- **A `with (...)` parameter value is a quoted string**, not a bare variable: - `with (Request = '$WorkflowContext')`. The unquoted spelling used elsewhere in - MDL is a syntax error here (it used to crash the binary — ako/mxcli#1023). +- **Arguments go right after the callee, as bare expressions**, like every other + call: `call microflow HR.Escalate(Request = $WorkflowContext) as callMicroflow1`. + The older `with (Request = '$WorkflowContext')`, the expression inside a + string, still parses with the same meaning but is deprecated (MDL-DEPR008); + `mxcli fmt --upgrade` rewrites it. - The context **Parameter entity must be persistent**. - Write the context variable as **`$WorkflowContext`**, matching the parameter name exactly. Mendix expressions are case-sensitive on 11.9+, so a lowercase diff --git a/CHANGELOG.md b/CHANGELOG.md index 40236a631..a9a29f5ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed +- **One argument form at every call site: `Param = expression`; one text-template form: `with ({1} = …)`** (ako/mxcli#751, R3/R4) — `call microflow`/`nanoflow`/`java action`/…, `execute database query`, `send rest request`, `show page`, and page/button actions and microflow/nanoflow data sources all bind an argument as `Order = $Order`, with no `$` on the parameter name and `=` rather than `:`. A workflow's `call microflow` / `call workflow` takes its arguments the same way, right after the callee, as bare expressions: `call microflow Shop.ACT_Process(Order = $WorkflowContext) as act1 comment 'Process'`. `show message` and `validation feedback` take `with ({1} = $a, {2} = $b)`, like `log` and the REST templates. `describe` writes these forms; it also writes a validation feedback's template arguments, which it used to drop, so describe → exec lost them. Measured on a copy of the Studio Pro-authored TestApp (Mendix 11.14.0): a script in the new forms and the same script in the old forms describe identically, and `mx check` reports 0 errors. **Migrating a script:** nothing breaks — `$Param = e` (**MDL-DEPR006**), `Param: e` (**MDL-DEPR007**), a workflow's `with (Param = '')` (**MDL-DEPR008**, same meaning: the string's content is the expression) and `objects [..]` / `parameters [..]` (**MDL-DEPR009**) still parse and build the same model, `check` / `exec` warn, and `mxcli fmt --upgrade` rewrites them. A workflow string whose content does not read back as the same bare expression is left in place and reported, and `describe` keeps the string form for such a stored expression. - **`describe page` no longer invents names Mendix does not store; `alter page` addresses a DataGrid 2 column by what it shows** (ako/mxcli#749, R12) — a layout grid's rows and columns, a data grid's columns and control bar, a gallery's template and filter, a chart's series and other object-list items have no Name in the model, and describe used to print one it made up (`row row1`, `column col3`, `controlbar controlBar1`, and a data-grid column named after its attribute, so two columns over `FullName` were both `column FullName`). They now print without one — `row { column (DesktopWidth: 6) { … } }`, `column (Attribute: FullName, Caption: 'Full name')` — and the widget name is optional in the grammar for exactly those elements. A column is addressed as `grid column(Attr)`, written as describe writes the attribute (`Owner/Name` over an association), or `grid column('Caption')`; an address two columns share is refused, listing the matches, unless `@n` picks one (`drop dg column(FullName)@2`). A widget Mendix does store a name for still needs one: `check` reports a missing one as **MDL-WIDGET35** and the page builder refuses it. Measured on the Studio Pro-authored PedApp fixture: the round-trip harness stays green, and `mx check` 11.13 shows no new error after `alter page … column(…)` edits and a create with nameless rows, grid columns and gallery slots. **Migrating a script:** nothing breaks — a name written on one of those elements still parses, is ignored as before, and `check` / `exec` warn **MDL-DEPR005**; `mxcli fmt --upgrade` removes it. The older `grid.Column` address keeps working. The info-level MDL-WIDGET16 note about dropped column names is gone, replaced by that warning. - **`describe microflow` / `describe nanoflow` leave out layout the engine derives** (ako/mxcli#748, R12) — `@position`, `@merge`, `@anchor` and `@curve` now follow the rule `@start` has followed since #951: an annotation is printed only when re-executing the description without it would put the node, or route the flow, somewhere else. That is decided by building the flow again from the description exactly as `create or modify` would (nothing is written) and comparing it node by node with the stored flow, pinning the first misplaced node of each run and repeating until nothing moves — so a statement placed after a hand-placed one needs no annotation of its own. A flow authored with no annotations describes with none; a Studio Pro-drawn flow keeps its layout. Measured on the Studio Pro-authored PedApp fixture: re-executing the new description lands every node, anchor and curve where the old one did (all 16 microflows, and all 224 microflows of a second Studio Pro project that re-execute at all); there the output barely shrinks (433 layout lines to 430), as it should, while after `mxcli layout flows` the 24 flows it could lay out go from 163 layout lines to 3. **Migrating a script:** nothing to do — every annotation still parses and means the same, and committed describe output re-executes as before; only a fresh `describe` is shorter. diff --git a/cmd/mxcli/syntax/features_microflow.go b/cmd/mxcli/syntax/features_microflow.go index 74d1d3af0..657d13e39 100644 --- a/cmd/mxcli/syntax/features_microflow.go +++ b/cmd/mxcli/syntax/features_microflow.go @@ -501,8 +501,8 @@ func init() { "show page", "open page", "close page", "display page", "navigate", "page action", }, - Syntax: "SHOW PAGE Module.Page;\nSHOW PAGE Module.Page ($Param = $value);\nCLOSE PAGE;", - Example: "SHOW PAGE MyModule.OrderDetail ($Order = $NewOrder);\nCLOSE PAGE;", + Syntax: "SHOW PAGE Module.Page;\nSHOW PAGE Module.Page (Param = $value);\nCLOSE PAGE;", + Example: "SHOW PAGE MyModule.OrderDetail (Order = $NewOrder);\nCLOSE PAGE;", SeeAlso: []string{"page"}, }) @@ -592,8 +592,8 @@ func init() { "validation", "feedback", "validation feedback", "error message", "field error", "form validation", }, - Syntax: "VALIDATION FEEDBACK $Obj/Attr MESSAGE 'error text';\nVALIDATION FEEDBACK $Obj/Attr MESSAGE '{1} is invalid'\n OBJECTS [$Value];", - Example: "VALIDATION FEEDBACK $Order/Quantity MESSAGE 'Quantity must be positive';\nVALIDATION FEEDBACK $Customer/Email MESSAGE '{1} is not valid'\n OBJECTS [$Customer/Email];", + Syntax: "VALIDATION FEEDBACK $Obj/Attr MESSAGE 'error text';\nVALIDATION FEEDBACK $Obj/Attr MESSAGE '{1} is invalid'\n WITH ({1} = $Value);", + Example: "VALIDATION FEEDBACK $Order/Quantity MESSAGE 'Quantity must be positive';\nVALIDATION FEEDBACK $Customer/Email MESSAGE '{1} is not valid'\n WITH ({1} = $Customer/Email);", SeeAlso: []string{"microflow.error-handling"}, }) diff --git a/cmd/mxcli/syntax/features_page.go b/cmd/mxcli/syntax/features_page.go index c8952bf9a..f9bf6d9ca 100644 --- a/cmd/mxcli/syntax/features_page.go +++ b/cmd/mxcli/syntax/features_page.go @@ -244,7 +244,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "datasource", "data source", "database", "microflow", "selection", "variable", "binding", "binds", "association", "data from context", }, - Syntax: "DataSource: $Variable -- Parameter/variable binding\nDataSource: DATABASE Module.Entity -- Database query\nDataSource: DATABASE Module.Entity WHERE [Attr != ''] SORT BY Attr ASC\n -- ...optionally constrained and sorted\nDataSource: DATABASE Module.Entity SORT BY Module.Assoc/Attr ASC\n -- ...sorted over an association. Name the\n -- hop when two reach the same entity —\n -- the wrong one builds cleanly and sorts\n -- by the wrong thing.\nDataSource: DATABASE Module.Entity SEARCH BY Attr, Attr2\n -- LIST VIEW only: the attributes its\n -- search bar filters on. Mirrors SORT BY,\n -- but takes no direction.\nDataSource: MICROFLOW Module.MF -- Microflow datasource, no parameters\nDataSource: MICROFLOW Module.MF(Param: $P) -- ...one argument per PARAMETER, required,\n -- and NAMED: the positional form\n -- MF($P) is a parse error. Mendix does\n -- NOT auto-map an object in scope, not\n -- even one of the exact type, so a\n -- missing argument is CE1571\nDataSource: SELECTION widgetName -- Selection from another widget\nDataSource: $currentObject/Module.Assoc -- Over an association (\"data from context\")\n -- list widget → to-many collection\n -- nested DATAVIEW → the to-one referenced object\nAttribute: AttributeName -- Attribute binding (inputs)", + Syntax: "DataSource: $Variable -- Parameter/variable binding\nDataSource: DATABASE Module.Entity -- Database query\nDataSource: DATABASE Module.Entity WHERE [Attr != ''] SORT BY Attr ASC\n -- ...optionally constrained and sorted\nDataSource: DATABASE Module.Entity SORT BY Module.Assoc/Attr ASC\n -- ...sorted over an association. Name the\n -- hop when two reach the same entity —\n -- the wrong one builds cleanly and sorts\n -- by the wrong thing.\nDataSource: DATABASE Module.Entity SEARCH BY Attr, Attr2\n -- LIST VIEW only: the attributes its\n -- search bar filters on. Mirrors SORT BY,\n -- but takes no direction.\nDataSource: MICROFLOW Module.MF -- Microflow datasource, no parameters\nDataSource: MICROFLOW Module.MF(Param = $P) -- ...one argument per PARAMETER, required,\n -- and NAMED: the positional form\n -- MF($P) is a parse error. Mendix does\n -- NOT auto-map an object in scope, not\n -- even one of the exact type, so a\n -- missing argument is CE1571\nDataSource: SELECTION widgetName -- Selection from another widget\nDataSource: $currentObject/Module.Assoc -- Over an association (\"data from context\")\n -- list widget → to-many collection\n -- nested DATAVIEW → the to-one referenced object\nAttribute: AttributeName -- Attribute binding (inputs)", Example: "-- Database datasource with grid\nDATAGRID grid (DataSource: DATABASE Module.Customer) {\n COLUMN colName (Attribute: Name, Caption: 'Name')\n}\n\n-- Microflow datasource\nDATAVIEW dv (DataSource: MICROFLOW Module.GetData) {\n TEXTBOX txtName (Label: 'Name', Attribute: Name)\n}\n\n-- Over an association: a nested DataView shows the referenced (to-one) object\nDATAVIEW dvOrder (DataSource: $Order) {\n DATAVIEW dvCustomer (DataSource: $currentObject/Order_Customer) {\n TEXTBOX txtCustName (Label: 'Name', Attribute: Name)\n }\n}\n\n-- Over an association: a list widget shows the (to-many) collection\nLISTVIEW lvLines (DataSource: $currentObject/Order_OrderLine) {\n DYNAMICTEXT dtLine (Content: 'Line')\n}", SeeAlso: []string{"page.widgets", "page.create"}, }) @@ -259,8 +259,8 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "icon", "linkbutton", "link button", "nothing", "no action", "inert", "dead button", }, - Syntax: "Action: NOTHING -- deliberately no action (Forms$NoAction)\nAction: SAVE_CHANGES\nAction: SAVE_CHANGES CLOSE_PAGE -- save, then close the pop-up\nAction: CANCEL_CHANGES\nAction: CANCEL_CHANGES CLOSE_PAGE\nAction: CLOSE_PAGE\nAction: DELETE\nAction: DELETE CLOSE_PAGE\nAction: DELETE_OBJECT\nAction: NANOFLOW Module.NF\nAction: NANOFLOW Module.NF(Param: $val)\nAction: OPEN_LINK 'https://example.com'\nAction: OPEN_LINK $currentObject/URL -- address read from an attribute\nAction: SIGN_OUT\nAction: COMPLETE_TASK 'OutcomeName'\nAction: SHOW_PAGE Module.Page\nAction: SHOW_PAGE Module.Page(Param: $currentObject)\nAction: MICROFLOW Module.MF\nAction: MICROFLOW Module.MF(Param: $val)\nAction: CREATE_OBJECT Module.Entity THEN SHOW_PAGE Module.Page\n\nThe list above is exhaustive. Anything else in an action slot is an\nERROR (MDL-WIDGET28), including a real keyword short its argument --\n`Action: OPEN_LINK` without a URL, `Action: SHOW_PAGE` without a page.\nSuch a widget used to be written with NO action at all and rendered as a\ndead control, with check, exec and mxbuild all clean, because a\nno-action widget is legal Mendix (mendixlabs/mxcli#1062). Write NOTHING\nwhen a control really is meant to be inert.\n\nThe same forms serve `OnClick:` (an alias of `Action:`) and `OnChange:`.\n\nA microflow or nanoflow action is a CALL: it needs an argument for every\nparameter the flow declares, or Mendix rejects the page with CE1571. The\nargument list is the same on every widget that takes an action -- a\nCONTAINER (which is clickable) as much as an ACTIONBUTTON. An enclosing\ndata container of the right type supplies it without an argument; a data\ngrid's CONTROL BAR does not, because it is not row-scoped -- pass the\ngrid's selection there (`$dgOrders`).\n\nA SHOW_PAGE argument must be the enclosing widget's context object --\neither $currentObject or the name of the variable the enclosing data\nwidget is bound to. Mendix infers it from that widget, so naming any\nother variable is refused (MDL-PAGEARG01); call a microflow instead.\nOutside any data widget there is no context object to infer, so such a\nbutton takes NO argument at all -- not a page parameter, not\n$currentObject, not a literal. mxcli used to drop it in silence and\nmxbuild then reported CE1571 per parameter of the target page\n(mendixlabs/mxcli#1029). Route that navigation through a microflow.\n\nOPEN_LINK takes a web address, stored as a Forms$StaticOrDynamicString:\neither a literal, or $currentObject/Attr to read it from an attribute of\nthe enclosing data container's object at runtime (Studio Pro's \"Address:\nattribute\"). An address over an association path is not supported yet.\n\nButton styles: Default, Primary, Success, Info, Warning, Danger\n\nMendix has THREE icon elements and the keyword picks which one:\n\nIcon: 'Atlas_Core.Atlas_Filled.pencil' -- an icon collection\nIcon: image MyModule.Images.logo -- an IMAGE collection\nIcon: glyph 57377 -- a font code point\n\nThe bare form is the icon-collection icon and any collection in the\nproject works, third-party ones included. The image form points into a\ndifferent document, and is spelled the same way apart from the keyword\n-- write it without `image` and mxcli stores a custom-icon reference,\nwhich fails the build with CE1613 (mendixlabs/mxcli#1059).\n`mxcli check -p … --references` resolves each kind against its own\ncollection and names the remedy when the kind is wrong.\n\nA glyph carries a code and no name. Codes are sparse, and an undefined\none fails only at `mxbuild --target=deploy`, naming the PAGE rather\nthan the icon -- so MDL078 checks it against the font's own table.\nList them with `show glyphs`.\n\nA name may be quoted or bare; a hyphenated segment is double-quoted on\nits own: Atlas_Core.Atlas.\"align-center\".\n\nUse `linkbutton` instead of `actionbutton` for link render mode (same properties).", - Example: "ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)\nACTIONBUTTON btnEdit (Caption: 'Edit',\n Action: SHOW_PAGE Module.EditPage(Item: $currentObject))\nLINKBUTTON btnDelete (Caption: 'Delete', Action: DELETE,\n Icon: 'Atlas_Core.Atlas_Filled.pencil')\n\n-- A clickable CONTAINER in a data grid's control bar, calling a nanoflow\n-- with the grid's selection as its argument.\nDATAGRID dgOrders (DataSource: DATABASE FROM Sales.Order, Selection: Single) {\n COLUMN colNr (Attribute: Number, Caption: 'Order #')\n CONTROLBAR cb {\n CONTAINER cShip (Class: 'command',\n Action: NANOFLOW Sales.ACT_Ship($Order = $dgOrders)) {\n ACTIONBUTTON btnShip (Caption: 'Ship')\n }\n }\n}", + Syntax: "Action: NOTHING -- deliberately no action (Forms$NoAction)\nAction: SAVE_CHANGES\nAction: SAVE_CHANGES CLOSE_PAGE -- save, then close the pop-up\nAction: CANCEL_CHANGES\nAction: CANCEL_CHANGES CLOSE_PAGE\nAction: CLOSE_PAGE\nAction: DELETE\nAction: DELETE CLOSE_PAGE\nAction: DELETE_OBJECT\nAction: NANOFLOW Module.NF\nAction: NANOFLOW Module.NF(Param = $val)\nAction: OPEN_LINK 'https://example.com'\nAction: OPEN_LINK $currentObject/URL -- address read from an attribute\nAction: SIGN_OUT\nAction: COMPLETE_TASK 'OutcomeName'\nAction: SHOW_PAGE Module.Page\nAction: SHOW_PAGE Module.Page(Param = $currentObject)\nAction: MICROFLOW Module.MF\nAction: MICROFLOW Module.MF(Param = $val)\nAction: CREATE_OBJECT Module.Entity THEN SHOW_PAGE Module.Page\n\nThe list above is exhaustive. Anything else in an action slot is an\nERROR (MDL-WIDGET28), including a real keyword short its argument --\n`Action: OPEN_LINK` without a URL, `Action: SHOW_PAGE` without a page.\nSuch a widget used to be written with NO action at all and rendered as a\ndead control, with check, exec and mxbuild all clean, because a\nno-action widget is legal Mendix (mendixlabs/mxcli#1062). Write NOTHING\nwhen a control really is meant to be inert.\n\nThe same forms serve `OnClick:` (an alias of `Action:`) and `OnChange:`.\n\nA microflow or nanoflow action is a CALL: it needs an argument for every\nparameter the flow declares, or Mendix rejects the page with CE1571. The\nargument list is the same on every widget that takes an action -- a\nCONTAINER (which is clickable) as much as an ACTIONBUTTON. An enclosing\ndata container of the right type supplies it without an argument; a data\ngrid's CONTROL BAR does not, because it is not row-scoped -- pass the\ngrid's selection there (`$dgOrders`).\n\nA SHOW_PAGE argument must be the enclosing widget's context object --\neither $currentObject or the name of the variable the enclosing data\nwidget is bound to. Mendix infers it from that widget, so naming any\nother variable is refused (MDL-PAGEARG01); call a microflow instead.\nOutside any data widget there is no context object to infer, so such a\nbutton takes NO argument at all -- not a page parameter, not\n$currentObject, not a literal. mxcli used to drop it in silence and\nmxbuild then reported CE1571 per parameter of the target page\n(mendixlabs/mxcli#1029). Route that navigation through a microflow.\n\nOPEN_LINK takes a web address, stored as a Forms$StaticOrDynamicString:\neither a literal, or $currentObject/Attr to read it from an attribute of\nthe enclosing data container's object at runtime (Studio Pro's \"Address:\nattribute\"). An address over an association path is not supported yet.\n\nButton styles: Default, Primary, Success, Info, Warning, Danger\n\nMendix has THREE icon elements and the keyword picks which one:\n\nIcon: 'Atlas_Core.Atlas_Filled.pencil' -- an icon collection\nIcon: image MyModule.Images.logo -- an IMAGE collection\nIcon: glyph 57377 -- a font code point\n\nThe bare form is the icon-collection icon and any collection in the\nproject works, third-party ones included. The image form points into a\ndifferent document, and is spelled the same way apart from the keyword\n-- write it without `image` and mxcli stores a custom-icon reference,\nwhich fails the build with CE1613 (mendixlabs/mxcli#1059).\n`mxcli check -p … --references` resolves each kind against its own\ncollection and names the remedy when the kind is wrong.\n\nA glyph carries a code and no name. Codes are sparse, and an undefined\none fails only at `mxbuild --target=deploy`, naming the PAGE rather\nthan the icon -- so MDL078 checks it against the font's own table.\nList them with `show glyphs`.\n\nA name may be quoted or bare; a hyphenated segment is double-quoted on\nits own: Atlas_Core.Atlas.\"align-center\".\n\nUse `linkbutton` instead of `actionbutton` for link render mode (same properties).", + Example: "ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)\nACTIONBUTTON btnEdit (Caption: 'Edit',\n Action: SHOW_PAGE Module.EditPage(Item = $currentObject))\nLINKBUTTON btnDelete (Caption: 'Delete', Action: DELETE,\n Icon: 'Atlas_Core.Atlas_Filled.pencil')\n\n-- A clickable CONTAINER in a data grid's control bar, calling a nanoflow\n-- with the grid's selection as its argument.\nDATAGRID dgOrders (DataSource: DATABASE FROM Sales.Order, Selection: Single) {\n COLUMN colNr (Attribute: Number, Caption: 'Order #')\n CONTROLBAR cb {\n CONTAINER cShip (Class: 'command',\n Action: NANOFLOW Sales.ACT_Ship(Order = $dgOrders)) {\n ACTIONBUTTON btnShip (Caption: 'Ship')\n }\n }\n}", SeeAlso: []string{"page.widgets"}, }) diff --git a/cmd/mxcli/syntax/features_workflow.go b/cmd/mxcli/syntax/features_workflow.go index 14223800c..d14dbb25a 100644 --- a/cmd/mxcli/syntax/features_workflow.go +++ b/cmd/mxcli/syntax/features_workflow.go @@ -262,11 +262,14 @@ func init() { "call microflow", "microflow task", "automated step", "system task", }, - // The WITH values are QUOTED — the grammar takes a string literal there, - // not a bare variable. Omitting the clause from this entry is how an - // author ends up writing the unquoted form (ako/mxcli#1023). - Syntax: "CALL MICROFLOW Module.MF [AS ] [COMMENT '']\n [WITH ( = '', ...)]\n [OUTCOMES '' -> { } ...];", - Example: "CALL MICROFLOW HR.SendNotification\n COMMENT 'Notify manager';\n\n-- Parameter values are quoted, and named by their BARE parameter name:\nCALL MICROFLOW HR.Escalate AS callMicroflow1\n WITH (Request = '$WorkflowContext');", + // R4 (ako/mxcli#751): arguments are bound as at every call site, right + // after the callee, the expression bare. The older `WITH (P = '')` + // after the comment is the deprecated alias MDL-DEPR008. + Syntax: "CALL MICROFLOW Module.MF[( = , ...)] [AS ] [COMMENT '']\n" + + " [OUTCOMES '' -> { } ...];\n\n" + + "-- WITH ( = ''), the expression in a string, is the\n" + + "-- deprecated spelling of the argument list (MDL-DEPR008).", + Example: "CALL MICROFLOW HR.SendNotification\n COMMENT 'Notify manager';\n\n-- Arguments are named by their BARE parameter name:\nCALL MICROFLOW HR.Escalate(Request = $WorkflowContext) AS callMicroflow1;", SeeAlso: []string{"workflow.create", "workflow.call-workflow", "workflow.ai-agent-task"}, }) @@ -280,8 +283,7 @@ func init() { // Measured on mxbuild 11.13.0 against the same activity written as a call // microflow: the only difference in what builds is CE1590 for a microflow // with no parameters. - Syntax: "CALL AGENT MICROFLOW Module.MF [AS ] [COMMENT '']\n" + - " [WITH ( = '', ...)]\n" + + Syntax: "CALL AGENT MICROFLOW Module.MF[( = , ...)] [AS ] [COMMENT '']\n" + " [OUTCOMES -> { } ...]\n" + " [BOUNDARY EVENT ...];\n\n" + "-- The same statement as CALL MICROFLOW, stored as an AI agent task. The microflow\n" + @@ -290,8 +292,8 @@ func init() { "-- branch on the agent's answer with OUTCOMES. Needs Mendix 11.9+.", Example: "CREATE MICROFLOW HR.ACT_ClassifyRequest ($Request: HR.LeaveRequest)\n" + "RETURNS Boolean AS $Urgent\nBEGIN\n -- call the agent here\n RETURN false;\nEND;\n\n" + - "CALL AGENT MICROFLOW HR.ACT_ClassifyRequest AS aiAgentTask1 COMMENT 'Classify the request'\n" + - " WITH (Request = '$WorkflowContext')\n" + + "CALL AGENT MICROFLOW HR.ACT_ClassifyRequest(Request = $WorkflowContext) AS aiAgentTask1\n" + + " COMMENT 'Classify the request'\n" + " OUTCOMES true -> { USER TASK Expedite 'Expedite' PAGE HR.TaskPage OUTCOMES 'Done' { }; }\n" + " false -> { };", SeeAlso: []string{"workflow.call-microflow", "agents"}, @@ -303,8 +305,8 @@ func init() { Keywords: []string{ "call workflow", "sub-workflow", "nested workflow", }, - Syntax: "CALL WORKFLOW Module.WF [AS ] [COMMENT '']\n [WITH ( = '', ...)];", - Example: "CALL WORKFLOW HR.SubApproval COMMENT 'Delegate to sub-process';\n\n-- Parameter values are quoted:\nCALL WORKFLOW HR.SubApproval AS callWf1\n WITH (Request = '$WorkflowContext');", + Syntax: "CALL WORKFLOW Module.WF[( = , ...)] [AS ] [COMMENT ''];", + Example: "CALL WORKFLOW HR.SubApproval COMMENT 'Delegate to sub-process';\n\nCALL WORKFLOW HR.SubApproval(Request = $WorkflowContext) AS callWf1;", SeeAlso: []string{"workflow.create", "workflow.call-microflow"}, }) diff --git a/docs-site/src/appendixes/quick-reference.md b/docs-site/src/appendixes/quick-reference.md index 5747f21e2..27a699438 100644 --- a/docs-site/src/appendixes/quick-reference.md +++ b/docs-site/src/appendixes/quick-reference.md @@ -156,7 +156,7 @@ AUTHENTICATION Basic, Session | Retrieve (Assoc) | `RETRIEVE $List FROM $Parent/Module.AssocName;` | Retrieve by association | | Call microflow | `$Result = CALL MICROFLOW Module.Name (Param = $value);` | | | Call nanoflow | `$Result = CALL NANOFLOW Module.Name (Param = $value);` | | -| Show page | `LIST PAGE Module.PageName ($Param = $value);` | Also accepts `(Param: $value)` | +| Show page | `SHOW PAGE Module.PageName (Param = $value);` | `($Param = …)` and `(Param: …)` are deprecated | | Close page | `CLOSE PAGE;` | | | Validation | `VALIDATION FEEDBACK $Entity/Attribute MESSAGE 'message';` | Requires attribute path + MESSAGE | | Log | `LOG INFO\|WARNING\|ERROR [NODE 'name'] 'message';` | | @@ -354,7 +354,7 @@ MDL uses explicit property declarations for pages: | Attribute binding | `Attribute: AttrName` | `TEXTBOX txt (Label: 'Name', Attribute: Name)` | | Variable binding | `DataSource: $Var` | `DATAVIEW dv (DataSource: $Product) { ... }` | | Action binding | `Action: TYPE` | `ACTIONBUTTON btn (Caption: 'Save', Action: SAVE_CHANGES)` | -| Microflow action | `Action: MICROFLOW Name(Param: val)` | `Action: MICROFLOW Mod.ACT_Process(Order: $Order)` | +| Microflow action | `Action: MICROFLOW Name(Param = val)` | `Action: MICROFLOW Mod.ACT_Process(Order = $Order)` | | Database source | `DataSource: DATABASE Entity` | `DATAGRID dg (DataSource: DATABASE Module.Entity)` | | Selection binding | `DataSource: SELECTION widget` | `DATAVIEW dv (DataSource: SELECTION galleryList)` | | CSS class | `Class: 'classes'` | `CONTAINER c (Class: 'card mx-spacing-top-large')` | diff --git a/docs-site/src/examples/crm-module.md b/docs-site/src/examples/crm-module.md index ca5980c00..00a1c1cde 100644 --- a/docs-site/src/examples/crm-module.md +++ b/docs-site/src/examples/crm-module.md @@ -96,7 +96,7 @@ END; CREATE MICROFLOW CRM.ACT_Customer_Save ($Customer: CRM.Customer) RETURNS Boolean AS $IsValid BEGIN - $IsValid = CALL MICROFLOW CRM.VAL_Customer($param = $Customer); + $IsValid = CALL MICROFLOW CRM.VAL_Customer(param = $Customer); IF $IsValid THEN COMMIT $Customer; diff --git a/docs-site/src/examples/validation.md b/docs-site/src/examples/validation.md index 91f952aa0..779e18822 100644 --- a/docs-site/src/examples/validation.md +++ b/docs-site/src/examples/validation.md @@ -54,7 +54,7 @@ The action microflow calls validation, and only saves if it passes: CREATE MICROFLOW Sales.ACT_Order_Save ($Order: Sales.Order) RETURNS Boolean AS $IsValid BEGIN - $IsValid = CALL MICROFLOW Sales.VAL_Order($param = $Order); + $IsValid = CALL MICROFLOW Sales.VAL_Order(param = $Order); IF $IsValid THEN COMMIT $Order; diff --git a/docs-site/src/language/activity-types.md b/docs-site/src/language/activity-types.md index 9ea237b1d..73cc4695b 100644 --- a/docs-site/src/language/activity-types.md +++ b/docs-site/src/language/activity-types.md @@ -171,23 +171,20 @@ Java action parameters follow the same `Name = value` syntax. Opens a page, passing parameters: ```sql -SHOW PAGE Sales.Order_Edit ($Order = $Order); +SHOW PAGE Sales.Order_Edit (Order = $Order); ``` -The parameter syntax uses `$PageParam = $MicroflowVar`. Multiple parameters are comma-separated: +The parameter syntax is `PageParam = $MicroflowVar`, with no `$` on the page's parameter name. Multiple parameters are comma-separated: ```sql SHOW PAGE Sales.OrderDetail ( - $Order = $Order, - $Customer = $Customer + Order = $Order, + Customer = $Customer ); ``` -An alternate syntax using colon notation is also supported: - -```sql -SHOW PAGE Sales.Order_Edit (Order: $Order); -``` +The older spellings `($Order = $Order)` and `(Order: $Order)` still parse but are +deprecated (MDL-DEPR006, MDL-DEPR007); `mxcli fmt --upgrade` rewrites them. ### CLOSE PAGE @@ -246,8 +243,7 @@ $Result = EXECUTE DATABASE QUERY MyModule.MyConn.SearchQuery DYNAMIC 'SELECT * FROM customers WHERE name LIKE ?'; -- With parameters -$Result = EXECUTE DATABASE QUERY MyModule.MyConn.GetByEmail - PARAMETERS ($EmailParam = $Email); +$Result = EXECUTE DATABASE QUERY MyModule.MyConn.GetByEmail (EmailParam = $Email); -- With runtime connection override $Result = EXECUTE DATABASE QUERY MyModule.MyConn.GetData @@ -272,7 +268,7 @@ The query name follows a three-part naming convention: `Module.ConnectionName.Qu | Call microflow | `$Var = CALL MICROFLOW Module.Name (Param = $val);` | Any type | | Call nanoflow | `$Var = CALL NANOFLOW Module.Name (Param = $val);` | Any type | | Call Java action | `$Var = CALL JAVA ACTION Module.Name (Param = val);` | Any type | -| Show page | `SHOW PAGE Module.Page ($Param = $val);` | -- | +| Show page | `SHOW PAGE Module.Page (Param = $val);` | -- | | Close page | `CLOSE PAGE;` | -- | | Validation | `VALIDATION FEEDBACK $Var/Attr MESSAGE 'msg';` | -- | | Log | `LOG INFO\|WARNING\|ERROR [NODE 'name'] 'msg';` | -- | diff --git a/docs-site/src/language/data-binding.md b/docs-site/src/language/data-binding.md index cb2409563..2200aaa89 100644 --- a/docs-site/src/language/data-binding.md +++ b/docs-site/src/language/data-binding.md @@ -144,7 +144,7 @@ Action buttons can pass the current data context to microflows and pages: DATAVIEW dvOrder (DataSource: $Order) { ACTIONBUTTON btnProcess ( Caption: 'Process Order', - Action: MICROFLOW Sales.ACT_ProcessOrder(Order: $Order), + Action: MICROFLOW Sales.ACT_ProcessOrder(Order = $Order), ButtonStyle: Primary ) diff --git a/docs-site/src/language/microflow-patterns.md b/docs-site/src/language/microflow-patterns.md index 469041b48..26b2fccb9 100644 --- a/docs-site/src/language/microflow-patterns.md +++ b/docs-site/src/language/microflow-patterns.md @@ -22,7 +22,7 @@ BEGIN ); COMMIT $Customer; - SHOW PAGE Sales.Customer_Edit ($Customer = $Customer); + SHOW PAGE Sales.Customer_Edit (Customer = $Customer); RETURN $Customer; END; ``` diff --git a/docs-site/src/language/microflows.md b/docs-site/src/language/microflows.md index 8e516f9b3..347dacb02 100644 --- a/docs-site/src/language/microflows.md +++ b/docs-site/src/language/microflows.md @@ -171,7 +171,7 @@ BEGIN Status = 'Draft' ); COMMIT $Order; - SHOW PAGE Sales.Order_Edit ($Order = $Order); + SHOW PAGE Sales.Order_Edit (Order = $Order); RETURN $Order; END; ``` diff --git a/docs-site/src/language/nanoflows.md b/docs-site/src/language/nanoflows.md index 672e88fa1..4dfa53c9a 100644 --- a/docs-site/src/language/nanoflows.md +++ b/docs-site/src/language/nanoflows.md @@ -79,7 +79,7 @@ $HasNetwork = CALL JAVASCRIPT ACTION NanoflowCommons.HasConnectivity(); ```sql -- Show a page -SHOW PAGE Sales.CartDetail ($Cart = $Cart); +SHOW PAGE Sales.CartDetail (Cart = $Cart); -- Close the current page CLOSE PAGE; @@ -104,7 +104,7 @@ IF $Cart/ItemCount = 0 THEN VALIDATION FEEDBACK $Cart/ItemCount MESSAGE 'Cart is empty'; RETURN false; ELSE - SHOW PAGE Sales.Checkout ($Cart = $Cart); + SHOW PAGE Sales.Checkout (Cart = $Cart); RETURN true; END IF; ``` @@ -184,7 +184,7 @@ CREATE NANOFLOW Sales.NAV_GoToOrderDetail BEGIN DECLARE $Order Sales.Order; - SHOW PAGE Sales.Order_Detail ($Order = $Order); + SHOW PAGE Sales.Order_Detail (Order = $Order); END; ``` diff --git a/docs-site/src/language/page-patterns.md b/docs-site/src/language/page-patterns.md index 89be95155..639c768e7 100644 --- a/docs-site/src/language/page-patterns.md +++ b/docs-site/src/language/page-patterns.md @@ -41,7 +41,7 @@ BEGIN $Customer = CREATE MyModule.Customer ( IsActive = true ); - SHOW PAGE MyModule.Customer_Edit ($Customer = $Customer); + SHOW PAGE MyModule.Customer_Edit (Customer = $Customer); RETURN $Customer; END; ``` @@ -273,7 +273,7 @@ BEGIN $Employee = CREATE HR.Employee ( HireDate = [%CurrentDateTime%] ); - SHOW PAGE HR.Employee_Edit ($Employee = $Employee); + SHOW PAGE HR.Employee_Edit (Employee = $Employee); RETURN $Employee; END; ``` diff --git a/docs-site/src/language/qualified-names.md b/docs-site/src/language/qualified-names.md index 326f2fe56..29f633749 100644 --- a/docs-site/src/language/qualified-names.md +++ b/docs-site/src/language/qualified-names.md @@ -83,7 +83,7 @@ CREATE ASSOCIATION Sales.Order_Customer Status: Enumeration(Sales.OrderStatus) DEFAULT 'Active' -- Microflow call -$Result = CALL MICROFLOW Sales.ACT_ProcessOrder ($Order = $Order); +$Result = CALL MICROFLOW Sales.ACT_ProcessOrder (Order = $Order); ``` ## See Also diff --git a/docs-site/src/language/widget-types.md b/docs-site/src/language/widget-types.md index 8e13a2248..354e84cd0 100644 --- a/docs-site/src/language/widget-types.md +++ b/docs-site/src/language/widget-types.md @@ -364,7 +364,7 @@ ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger) ```sql ACTIONBUTTON btnProcess ( Caption: 'Process', - Action: MICROFLOW Sales.ACT_ProcessOrder(Order: $Order), + Action: MICROFLOW Sales.ACT_ProcessOrder(Order = $Order), ButtonStyle: Primary ) ``` diff --git a/docs-site/src/language/workflow-activities.md b/docs-site/src/language/workflow-activities.md index bc5b3f30f..bc2e58e63 100644 --- a/docs-site/src/language/workflow-activities.md +++ b/docs-site/src/language/workflow-activities.md @@ -83,7 +83,7 @@ builds and never completes. Execute a microflow as part of the workflow. Optionally specify a comment and outcomes: ```sql -CALL MICROFLOW . [COMMENT ''] +CALL MICROFLOW . [( = , ...)] [COMMENT ''] [OUTCOMES '' { } ...]; ``` @@ -91,8 +91,15 @@ Example: ```sql CALL MICROFLOW HR.ACT_SendNotification COMMENT 'Notify the applicant'; +CALL MICROFLOW HR.ACT_Escalate(Request = $WorkflowContext) COMMENT 'Escalate'; ``` +Arguments are bound like every other call in MDL: `Param = expression` right +after the callee, the expression written bare. The older spelling after the +comment, `WITH (Param = '')` with the expression inside a string, +still parses with the same meaning but is deprecated (MDL-DEPR008); +`mxcli fmt --upgrade` rewrites it. + ## AI Agent Task A step that runs an AI agent (Mendix 11.9 or later). It is written like `CALL @@ -101,16 +108,15 @@ mappings, outcomes and boundary events. The microflow is where the agent is invoked — build agents with the Studio Pro Agent Editor, or `CREATE AGENT`. ```sql -CALL AGENT MICROFLOW . [AS ] [COMMENT ''] - [WITH ( = '', ...)] +CALL AGENT MICROFLOW . [( = , ...)] [AS ] [COMMENT ''] [OUTCOMES -> { } ...]; ``` Example — branch on the agent's answer: ```sql -CALL AGENT MICROFLOW HR.ACT_ClassifyRequest AS aiAgentTask1 COMMENT 'Classify the request' - WITH (Request = '$WorkflowContext') +CALL AGENT MICROFLOW HR.ACT_ClassifyRequest(Request = $WorkflowContext) AS aiAgentTask1 + COMMENT 'Classify the request' OUTCOMES true -> { USER TASK Expedite 'Expedite the request' PAGE HR.TaskPage OUTCOMES 'Done' { }; } false -> { }; @@ -125,7 +131,7 @@ branch with `OUTCOMES`; return nothing for a single path. Start a sub-workflow: ```sql -CALL WORKFLOW . [COMMENT '']; +CALL WORKFLOW . [( = , ...)] [COMMENT '']; ``` Example: diff --git a/docs-site/src/migration/validation.md b/docs-site/src/migration/validation.md index 0c173d2ac..efeddfae8 100644 --- a/docs-site/src/migration/validation.md +++ b/docs-site/src/migration/validation.md @@ -65,7 +65,7 @@ $Line = CREATE Sales.OrderLine (Price = 10.00, Quantity = 3); CHANGE $Line (Sales.OrderLine_Order = $Order); COMMIT $Line; -CALL MICROFLOW Sales.ACT_Order_CalculateTotal ($Order = $Order); +CALL MICROFLOW Sales.ACT_Order_CalculateTotal (Order = $Order); -- @assert: $Order/TotalAmount = 30.00 ``` diff --git a/docs-site/src/reference/microflow/create-microflow.md b/docs-site/src/reference/microflow/create-microflow.md index 24d4e4be4..ec6a2c1df 100644 --- a/docs-site/src/reference/microflow/create-microflow.md +++ b/docs-site/src/reference/microflow/create-microflow.md @@ -86,7 +86,7 @@ Call another microflow, nanoflow, or Java action. Parameters are passed by name. **UI Actions** ```sql -SHOW PAGE Module.PageName ( $Param = $value ); +SHOW PAGE Module.PageName ( Param = $value ); CLOSE PAGE; ``` @@ -199,7 +199,7 @@ BEGIN Status = 'Draft' ); COMMIT $Order; - SHOW PAGE Sales.Order_Edit ($Order = $Order); + SHOW PAGE Sales.Order_Edit (Order = $Order); RETURN $Order; END; ``` diff --git a/docs-site/src/reference/microflow/create-nanoflow.md b/docs-site/src/reference/microflow/create-nanoflow.md index 99d5666fc..e92d8a033 100644 --- a/docs-site/src/reference/microflow/create-nanoflow.md +++ b/docs-site/src/reference/microflow/create-nanoflow.md @@ -101,7 +101,7 @@ CREATE NANOFLOW Sales.NAV_SubmitOrder BEGIN CHANGE $Order (Status = 'Submitted'); $Result = CALL MICROFLOW Sales.ACT_ProcessOrder (Order = $Order); - SHOW PAGE Sales.Order_Confirmation ($Order = $Order); + SHOW PAGE Sales.Order_Confirmation (Order = $Order); END; ``` diff --git a/docs-site/src/reference/page/create-page.md b/docs-site/src/reference/page/create-page.md index 6c2750094..406d04862 100644 --- a/docs-site/src/reference/page/create-page.md +++ b/docs-site/src/reference/page/create-page.md @@ -121,8 +121,8 @@ values, including the argument list. |--------|--------|-------------| | Save | `Action: SAVE_CHANGES` | Commits and closes | | Cancel | `Action: CANCEL_CHANGES` | Rolls back and closes | -| Microflow | `Action: MICROFLOW Module.Name(Param: val)` | Calls a microflow | -| Nanoflow | `Action: NANOFLOW Module.Name(Param: val)` | Calls a nanoflow | +| Microflow | `Action: MICROFLOW Module.Name(Param = val)` | Calls a microflow | +| Nanoflow | `Action: NANOFLOW Module.Name(Param = val)` | Calls a nanoflow | | Page | `Action: PAGE Module.PageName` | Opens a page | | Close | `Action: CLOSE_PAGE` | Closes the current page | | Delete | `Action: DELETE` | Deletes the context object | @@ -150,7 +150,7 @@ DATAGRID dgOrders (DataSource: DATABASE FROM Sales.Order, Selection: Single) { COLUMN colNr (Attribute: Number, Caption: 'Order #') CONTROLBAR cb { CONTAINER cShip (Class: 'command', - Action: NANOFLOW Sales.ACT_Ship($Order = $dgOrders)) { + Action: NANOFLOW Sales.ACT_Ship(Order = $dgOrders)) { ACTIONBUTTON btnShip (Caption: 'Ship') } } diff --git a/docs-site/src/reference/query/describe-microflow.md b/docs-site/src/reference/query/describe-microflow.md index 955e7c663..75b72d2e6 100644 --- a/docs-site/src/reference/query/describe-microflow.md +++ b/docs-site/src/reference/query/describe-microflow.md @@ -37,7 +37,7 @@ BEGIN Status = 'Draft' ); COMMIT $Order; - SHOW PAGE Sales.Order_Edit ($Order = $Order); + SHOW PAGE Sales.Order_Edit (Order = $Order); RETURN $Order; END; ``` diff --git a/docs-site/src/tutorial/create-page.md b/docs-site/src/tutorial/create-page.md index ced0ca607..8c5d6cd35 100644 --- a/docs-site/src/tutorial/create-page.md +++ b/docs-site/src/tutorial/create-page.md @@ -133,7 +133,7 @@ DYNAMICTEXT dynName (Attribute: Name) ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger) -ACTIONBUTTON btnProcess (Caption: 'Process', Action: MICROFLOW MyModule.ACT_ProcessProduct(Product: $Product)) +ACTIONBUTTON btnProcess (Caption: 'Process', Action: MICROFLOW MyModule.ACT_ProcessProduct(Product = $Product)) ``` ### Layout widgets diff --git a/docs/01-project/MDL_QUICK_REFERENCE.md b/docs/01-project/MDL_QUICK_REFERENCE.md index f9add0af0..517241798 100644 --- a/docs/01-project/MDL_QUICK_REFERENCE.md +++ b/docs/01-project/MDL_QUICK_REFERENCE.md @@ -556,13 +556,13 @@ it is for pages. | REST call (mapping single) | `$Var = rest call get '' returns mapping Module.IMM as Module.Entity;` | Single object — Studio Pro emits `ForceSingleOccurrence=true` | | REST call (mapping list) | `$Var = rest call get '' returns mapping Module.IMM as list of Module.Entity;` | List result | | REST call (none) | `rest call get '' returns nothing;` | Discard response | -| Show page | `show page Module.PageName ($Param = $value);` | Also accepts `(Param: $value)` | +| Show page | `show page Module.PageName (Param = $value);` | `Param = expression`, as at every call site. `($Param = …)` and `(Param: …)` are deprecated (MDL-DEPR006/007) | | Close page | `close page;` | | | Download file | `download file $FileDocument [show in browser];` | Streams a `System.FileDocument` | -| Show message | `show message 'text' [type Information\|Warning\|Error] [objects [$a, $b]] [blocking];` | `blocking` halts the client until the user dismisses it — Studio Pro's checkbox. It goes after `objects` and before `on error`. Without it, a describe → exec round trip turned a blocking message into a non-blocking one (16 microflows measured) | +| Show message | `show message 'text' [type Information\|Warning\|Error] [with ({1} = $a, {2} = $b)] [blocking];` | `blocking` halts the client until the user dismisses it — Studio Pro's checkbox. It goes after the `with` list and before `on error`. `with ({1} = $a, {2} = $b)` is the deprecated spelling of the list (MDL-DEPR009). Without it, a describe → exec round trip turned a blocking message into a non-blocking one (16 microflows measured) | | Database connection credentials | `connection string @Mod.Const`, `username @Mod.Const`, `password @Mod.Const` | Constant **references** only. A literal writes an unopenable project — MDL058 | | Synchronize (nanoflow only) | `synchronize all;` / `synchronize unsynchronized;` / `synchronize $Obj, $List;` | Offline sync. `unsynchronized` needs Mendix 9.4+. In a microflow this is MDL057 / CE0009 | -| Validation | `validation feedback $entity/attribute message 'message';` | Requires attribute path + MESSAGE | +| Validation | `validation feedback $entity/attribute message 'message {1}' [with ({1} = $a)];` | Requires attribute path + MESSAGE | | Log | `log info\|warning\|error [node 'name'] 'message';` | | | Apply entity access | `@applyentityaccess` / `@applyentityaccess(false)` before `create microflow` or `create rule` | Runs the flow under the **current user's** entity access rules instead of with full access. A **security** setting and only ever narrowing, so an ABSENT annotation **preserves** what is stored rather than clearing it — the same rule as `@excluded`. Not available on a nanoflow: it runs in the client and Mendix stores no such property | | Position | `@position(x, y)` | Canvas position (before activity) | @@ -714,9 +714,9 @@ mandatory and a misplaced clause failed with a token error - A **targeting microflow** takes exactly `System.Workflow` + the context entity (or a generalization of it), in either order — anything else is CE6677. Users targeting returns a list of `System.User`, groups a list of `System.WorkflowGroup`. - An **on-created microflow** takes exactly `System.WorkflowUserTask` + the context entity, in either order (CE6683), and returns nothing (CE5012). - `check --references` reports these before anything is written; `exec` refuses the workflow statement itself (Mendix 11+). -- `call microflow Mod.MF [as ] [comment ''] [with ( = '', ...)] [outcomes '' -> { } ...];` +- `call microflow Mod.MF[( = , ...)] [as ] [comment ''] [outcomes '' -> { } ...];` - `call agent microflow Mod.MF [as ] [comment ''] [with ( = '', ...)] [outcomes … -> { } ...];` — an **AI agent task** (Mendix 11.9+): the call-microflow statement stored as `Workflows$AIAgentTaskActivity`. Its microflow must take at least one parameter (CE1590). -- `call workflow Mod.WF [as ] [comment ''] [with ( = '', ...)];` +- `call workflow Mod.WF[( = , ...)] [as ] [comment ''];` - `decision [] [''] outcomes -> { } ...;` - `parallel split [] path 1 { } path 2 { };` - `jump to ;` @@ -766,8 +766,10 @@ the build fails `CE6686`). Anything shorter is refused as `MDL-WF03`, and by module — is not a build error but a `StorageLoadException` that leaves the project unopenable in Studio Pro and mxbuild. -**Parameter values in `with (...)` are quoted strings**, not bare variables: -`call microflow Mod.MF with (Request = '$WorkflowContext')`. +**Arguments go right after the callee**, bound as at every call site with a bare +expression: `call microflow Mod.MF(Request = $WorkflowContext)`. The older +`with (Request = '$WorkflowContext')`, the expression in a string, is a deprecated +alias with the same meaning (MDL-DEPR008). **An enumeration decision also needs an empty outcome.** Mendix generates one outcome per enumeration value **plus one for the empty value**, and MxBuild @@ -1455,11 +1457,11 @@ MDL uses explicit property declarations for pages: | Variable binding | `datasource: $Var` | `dataview dv (datasource: $Product) { ... }` | | Action binding | `action: type` | `actionbutton btn (caption: 'Save', action: save_changes)` — the forms are a closed set (`mxcli syntax page.action`); anything else is **MDL-WIDGET28** | | No action | `action: nothing` | `actionbutton btn (caption: 'Decorative', action: nothing)` — an explicitly inert control. Write it deliberately: an action keyword **short its argument** (`action: open_link` with no URL) is now an error rather than a widget silently written with no action at all | -| Microflow action | `action: microflow Name(Param: val)` | `action: microflow Mod.ACT_Process(Order: $Order)` | +| Microflow action | `action: microflow Name(Param = val)` | `action: microflow Mod.ACT_Process(Order = $Order)` | | Button icon | `icon: 'Module.IconCollection.IconName'` | `linkbutton btn (caption: 'Edit', action: nothing, icon: 'Atlas_Core.Atlas_Filled.pencil')` — the **icon-collection** icon; MxBuild rejects an unknown name (CE1613) | | Image icon | `icon: image Module.ImageCollection.Name` | `actionbutton btn (caption: 'Logo', action: nothing, icon: image MyMod.Images.logo)` — an **image** collection is a different document from an icon collection, and the names are spelled the same, so the keyword is what separates them. Written without `image` it is stored as a custom-icon reference and the build fails **CE1613** | | Glyph icon | `icon: glyph ` | `actionbutton btn (caption: 'Home', action: nothing, icon: glyph 57377)` — a font code point with no name. Codes are sparse; an undefined one fails only at `mxbuild --target=deploy`, naming the **page**, so **MDL078** checks it. Browse with `show glyphs` | -| Clickable container | `onclick: action` (alias of `action:`) | `container card (onclick: microflow Mod.ACT_Open) { ... }` — takes an argument list like a button: `action: nanoflow Mod.ACT_Ship($Order = $dgOrders)` | +| Clickable container | `onclick: action` (alias of `action:`) | `container card (onclick: microflow Mod.ACT_Open) { ... }` — takes an argument list like a button: `action: nanoflow Mod.ACT_Ship(Order = $dgOrders)` | | Action arguments | every parameter needs one | A flow action with an unfilled parameter is **CE1571**. An enclosing data container of its type supplies it; a data grid's **control bar** does not (not row-scoped) — pass the grid's selection, `$dgOrders` | | Database source | `datasource: database entity` | `datagrid dg (datasource: database Module.Entity)` | | Database source, constrained and sorted | `datasource: database entity where [...] sort by Attr asc` | `listview lv (datasource: database from Mod.Vehicle where [Brand != ''] sort by Brand asc)` | diff --git a/mdl/deprecation/deprecation.go b/mdl/deprecation/deprecation.go index 7ae080b8b..a5be726a0 100644 --- a/mdl/deprecation/deprecation.go +++ b/mdl/deprecation/deprecation.go @@ -99,6 +99,20 @@ const ( // name for: a layout grid's rows and columns, a data grid's columns and // control bar, a gallery's template and filter (R12, ako/mxcli#749). UnstoredWidgetName = "MDL-DEPR005" + // DollarArgumentName is `$Param = expr` at a call site: the parameter + // named with the `$` of a variable (R4, ako/mxcli#751). + DollarArgumentName = "MDL-DEPR006" + // ColonArgument is `Param: expr` at a call site (`show page`, a page + // action or data source): `:` sets a model property, `=` binds a value + // (R3/R4, ako/mxcli#751). + ColonArgument = "MDL-DEPR007" + // WorkflowStringArgument is a workflow call's `with (Param = '')`: + // the argument expression written inside a string (R4, ako/mxcli#751). + WorkflowStringArgument = "MDL-DEPR008" + // PositionalTemplateArguments is `objects [a, b]` / `parameters [a, b]` + // on a text template: the placeholders bound by position (R4, + // ako/mxcli#751). + PositionalTemplateArguments = "MDL-DEPR009" ) // entries is the registry. Append only: a code is never reused or renumbered, @@ -168,6 +182,55 @@ var entries = []Entry{ Example: "create page M.P (Title: 'P', Layout: Atlas_Core.Atlas_Default) { datagrid dg (DataSource: database from M.E) { column Name (Attribute: Name) } };", CanonicalExample: "create page M.P (Title: 'P', Layout: Atlas_Core.Atlas_Default) { datagrid dg (DataSource: database from M.E) { column (Attribute: Name) } };", }, + { + Code: DollarArgumentName, + Old: "call microflow M.F($Param = expr)", + Canonical: "call microflow M.F(Param = expr)", + Rewrite: Rewrite{Structural: "drop the `$` from the parameter name (quoted when the bare name is a keyword)"}, + RemovedIn: 2, + Note: "Every call site binds an argument as `Param = expression` (R4): call microflow, nanoflow, java " + + "action, javascript action, external action, web service operation, execute database query, " + + "send rest request, show page, and page/button actions and data sources.", + Example: "create microflow M.F ($O: M.E) begin call microflow M.G($Order = $O); end;", + CanonicalExample: "create microflow M.F ($O: M.E) begin call microflow M.G(Order = $O); end;", + }, + { + Code: ColonArgument, + Old: "show page M.P(Param: expr)", + Canonical: "show page M.P(Param = expr)", + Rewrite: Rewrite{Structural: "`Param: expr` -> `Param = expr`"}, + RemovedIn: 2, + Note: "`:` sets a model property and `=` binds a runtime value (R3). An argument binds a value, so " + + "it takes `=` wherever the call appears: show page, and page/button actions and data sources " + + "(`Action: microflow M.F(Param = expr)`).", + Example: "create microflow M.F ($O: M.E) begin show page M.P(Order: $O); end;", + CanonicalExample: "create microflow M.F ($O: M.E) begin show page M.P(Order = $O); end;", + }, + { + Code: WorkflowStringArgument, + Old: "call microflow M.F with (Param = '')", + Canonical: "call microflow M.F(Param = )", + Rewrite: Rewrite{Structural: "move the list after the callee and write each string's content as the bare expression"}, + RemovedIn: 2, + Note: "In a workflow. The string form keeps its meaning — its content is the expression — so it is an alias, not a " + + "change of meaning. A string whose content does not parse as an MDL expression is left in place " + + "and reported by fmt --upgrade.", + Example: "create workflow M.W parameter $WorkflowContext: M.E begin " + + "call microflow M.F with (Order = '$WorkflowContext'); end workflow;", + CanonicalExample: "create workflow M.W parameter $WorkflowContext: M.E begin " + + "call microflow M.F(Order = $WorkflowContext); end workflow;", + }, + { + Code: PositionalTemplateArguments, + Old: "objects [$a, $b] / parameters ['a', 'b']", + Canonical: "with ({1} = $a, {2} = $b)", + Rewrite: Rewrite{Structural: "number the list: `objects [a, b]` -> `with ({1} = a, {2} = b)`"}, + RemovedIn: 2, + Note: "One text-template form everywhere: show message, validation feedback, log, and REST " + + "url and body templates.", + Example: "create microflow M.F ($N: String) begin show message 'Hi {1}' type Information objects [$N]; end;", + CanonicalExample: "create microflow M.F ($N: String) begin show message 'Hi {1}' type Information with ({1} = $N); end;", + }, } // All returns every registered entry, in code order. diff --git a/mdl/executor/cmd_microflows_format_action.go b/mdl/executor/cmd_microflows_format_action.go index b7600d7e3..74d7efd38 100644 --- a/mdl/executor/cmd_microflows_format_action.go +++ b/mdl/executor/cmd_microflows_format_action.go @@ -777,7 +777,7 @@ func formatAction( // Extract just the parameter name from the qualified name parts := strings.Split(pm.Parameter, ".") paramName := parts[len(parts)-1] - params = append(params, fmt.Sprintf("$%s = %s", paramName, describeExpr(pm.Argument))) + params = append(params, fmt.Sprintf("%s = %s", paramName, describeExpr(pm.Argument))) } // Build the statement @@ -807,11 +807,7 @@ func formatAction( } result := fmt.Sprintf("show message %s type %s", message, msgType) if len(a.TemplateParameters) > 0 { - objs := make([]string, len(a.TemplateParameters)) - for i, p := range a.TemplateParameters { - objs[i] = describeExpr(p) - } - result += " objects [" + strings.Join(objs, ", ") + "]" + result += templateArgsClause(a.TemplateParameters) } // Without this, a describe -> exec round trip turned a BLOCKING message // box into a non-blocking one. The model carried Blocking on both @@ -856,7 +852,7 @@ func formatAction( } else if a.AssociationName != "" { attrPath = varName + "/" + a.AssociationName } - return fmt.Sprintf("validation feedback %s message %s;", attrPath, msgText) + return fmt.Sprintf("validation feedback %s message %s%s;", attrPath, msgText, templateArgsClause(a.TemplateParameters)) case *microflows.RestCallAction: return formatRestCallAction(ctx, a) @@ -1466,7 +1462,6 @@ func formatRestOperationCallAction(ctx *ExecContext, a *microflows.RestOperation if i > 0 { sb.WriteString(", ") } - sb.WriteString("$") sb.WriteString(p.name) sb.WriteString(" = ") sb.WriteString(p.value) @@ -2061,3 +2056,17 @@ func mdlAggregateKeyword(fn microflows.AggregateFunction) (string, bool) { return "", false } } + +// templateArgsClause renders a text template's arguments as ` with ({1} = a, +// {2} = b)` — R4's one text-template form (ako/mxcli#751) — or "" when there +// are none. +func templateArgsClause(args []string) string { + if len(args) == 0 { + return "" + } + parts := make([]string, len(args)) + for i, a := range args { + parts[i] = fmt.Sprintf("{%d} = %s", i+1, describeExpr(a)) + } + return " with (" + strings.Join(parts, ", ") + ")" +} diff --git a/mdl/executor/cmd_microflows_format_action_test.go b/mdl/executor/cmd_microflows_format_action_test.go index 792db3f71..7a4d45771 100644 --- a/mdl/executor/cmd_microflows_format_action_test.go +++ b/mdl/executor/cmd_microflows_format_action_test.go @@ -525,7 +525,7 @@ func TestFormatAction_ShowPage_WithParams(t *testing.T) { }, } got := e.formatAction(action, nil, nil) - want := "show page MyModule.OrderDetail($Order = $Order);" + want := "show page MyModule.OrderDetail(Order = $Order);" if got != want { t.Errorf("got %q, want %q", got, want) } diff --git a/mdl/executor/cmd_pages_describe_datasource.go b/mdl/executor/cmd_pages_describe_datasource.go index 63bb53f33..a80c95339 100644 --- a/mdl/executor/cmd_pages_describe_datasource.go +++ b/mdl/executor/cmd_pages_describe_datasource.go @@ -302,7 +302,7 @@ func dataSourceExpr(ds *rawDataSource) string { if len(ds.Args) > 0 { parts := make([]string, 0, len(ds.Args)) for _, arg := range ds.Args { - parts = append(parts, arg.Name+": "+arg.Value) + parts = append(parts, visitor.ParameterNameSpelling(arg.Name)+" = "+arg.Value) } expr += "(" + strings.Join(parts, ", ") + ")" } diff --git a/mdl/executor/cmd_pages_describe_datasource_test.go b/mdl/executor/cmd_pages_describe_datasource_test.go index bc0f7cdab..99f83a2d5 100644 --- a/mdl/executor/cmd_pages_describe_datasource_test.go +++ b/mdl/executor/cmd_pages_describe_datasource_test.go @@ -299,7 +299,7 @@ func TestDataSourceArgsRoundTrip(t *testing.T) { t.Errorf("second arg = %+v, want {Limit 10}", got.Args[1]) } - want := "microflow Mod.DS_Filtered(Term: $Term, Limit: 10)" + want := "microflow Mod.DS_Filtered(Term = $Term, Limit = 10)" if expr := dataSourceExpr(got); expr != want { t.Errorf("rendered %q, want %q", expr, want) } @@ -345,7 +345,7 @@ func TestDataSourceArgsNanoflowFlat(t *testing.T) { if got == nil { t.Fatal("nanoflow datasource not read") } - want := "nanoflow Mod.NF_Rows(Ctx: $currentObject)" + want := "nanoflow Mod.NF_Rows(Ctx = $currentObject)" if expr := dataSourceExpr(got); expr != want { t.Errorf("rendered %q, want %q", expr, want) } @@ -369,7 +369,7 @@ func TestDataSourceArgsNanoflow(t *testing.T) { if got == nil { t.Fatal("nanoflow datasource not read") } - want := "nanoflow Mod.NF_Rows(Ctx: $currentObject)" + want := "nanoflow Mod.NF_Rows(Ctx = $currentObject)" if expr := dataSourceExpr(got); expr != want { t.Errorf("rendered %q, want %q", expr, want) } diff --git a/mdl/executor/cmd_pages_describe_flow_args_test.go b/mdl/executor/cmd_pages_describe_flow_args_test.go index ec18170e0..015508847 100644 --- a/mdl/executor/cmd_pages_describe_flow_args_test.go +++ b/mdl/executor/cmd_pages_describe_flow_args_test.go @@ -85,17 +85,17 @@ func TestDataSourceArgsReadPageVariableBinding(t *testing.T) { "Parameter": "Mod.DS.Order", "Variable": map[string]any{"$Type": "Forms$PageVariable", "PageParameter": "Order"}, }, - "microflow Mod.DS(Order: $Order)", + "microflow Mod.DS(Order = $Order)", }, { "expression binding still read", map[string]any{"Parameter": "Mod.DS.Order", "Expression": "$Order"}, - "microflow Mod.DS(Order: $Order)", + "microflow Mod.DS(Order = $Order)", }, { "bare string in Variable still read", map[string]any{"Parameter": "Mod.DS.Order", "Variable": "$Order"}, - "microflow Mod.DS(Order: $Order)", + "microflow Mod.DS(Order = $Order)", }, } { t.Run(tc.name, func(t *testing.T) { diff --git a/mdl/executor/cmd_pages_describe_output.go b/mdl/executor/cmd_pages_describe_output.go index 7d3da4014..4330fc32c 100644 --- a/mdl/executor/cmd_pages_describe_output.go +++ b/mdl/executor/cmd_pages_describe_output.go @@ -5,6 +5,7 @@ package executor import ( "context" "fmt" + "github.com/mendixlabs/mxcli/mdl/visitor" "io" "regexp" "strconv" @@ -1546,7 +1547,7 @@ func pageActionParameters(ctx *ExecContext, settings map[string]any, pageName st // bound to the row object the enclosing widget supplies. var params []string for _, name := range targetPageParameterNames(ctx, pageName) { - params = append(params, mdlIdent(name)+": $currentObject") + params = append(params, visitor.ParameterNameSpelling(name)+" = $currentObject") } return strings.Join(params, ", ") } @@ -1586,7 +1587,7 @@ func targetPageParameterNames(ctx *ExecContext, qualifiedName string) []string { } // extractPageParameters extracts page parameter mappings from a FormSettings/PageSettings object. -// Returns formatted string like "Product: $currentObject" or empty string if no params. +// Returns formatted string like "Product = $currentObject" or empty string if no params. func extractPageParameters(ctx *ExecContext, settings map[string]any) string { mappings := getBsonArrayElements(settings["ParameterMappings"]) if len(mappings) == 0 { @@ -1636,7 +1637,7 @@ func extractPageParameters(ctx *ExecContext, settings map[string]any) string { } if value != "" { - params = append(params, mdlIdent(paramName)+": "+value) + params = append(params, visitor.ParameterNameSpelling(paramName)+" = "+value) } } @@ -1695,7 +1696,7 @@ func extractMicroflowParameters(ctx *ExecContext, settings map[string]any) strin if value != "" { // Canonical microflowArgV3 form is `Param: $value` (IDENTIFIER COLON expr); // emitting `Param = $value` is IDENTIFIER EQUALS, which doesn't re-parse (#640). - params = append(params, mdlIdent(paramName)+": "+value) + params = append(params, visitor.ParameterNameSpelling(paramName)+" = "+value) } } @@ -1754,7 +1755,7 @@ func extractNanoflowParameters(ctx *ExecContext, action map[string]any) string { if value != "" { // Canonical microflowArgV3 form is `Param: $value` (IDENTIFIER COLON expr); // emitting `Param = $value` is IDENTIFIER EQUALS, which doesn't re-parse (#640). - params = append(params, mdlIdent(paramName)+": "+value) + params = append(params, visitor.ParameterNameSpelling(paramName)+" = "+value) } } diff --git a/mdl/executor/cmd_pages_describe_pageparams_test.go b/mdl/executor/cmd_pages_describe_pageparams_test.go index e9c38459d..9f2b1b456 100644 --- a/mdl/executor/cmd_pages_describe_pageparams_test.go +++ b/mdl/executor/cmd_pages_describe_pageparams_test.go @@ -119,7 +119,7 @@ func TestRenderShowPageAction_ExplicitMappingWins(t *testing.T) { }, } got := renderClientActionMDL(ctx, showPageAction("Formula1Frontend.Race_Weekend", mappings)) - want := "show_page Formula1Frontend.Race_Weekend(Race: $SelectedRace)" + want := "show_page Formula1Frontend.Race_Weekend(Race = $SelectedRace)" if got != want { t.Errorf("an explicit mapping was overwritten:\n got: %s\nwant: %s", got, want) } diff --git a/mdl/executor/cmd_workflows.go b/mdl/executor/cmd_workflows.go index 80b18d9f8..3e8a91d6e 100644 --- a/mdl/executor/cmd_workflows.go +++ b/mdl/executor/cmd_workflows.go @@ -10,6 +10,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/visitor" "github.com/mendixlabs/mxcli/sdk/workflows" ) @@ -789,20 +790,8 @@ func formatCallMicroflowTask(a *workflows.CallMicroflowTask, indent string) []st asAndComment += " comment " + mdlQuoted(a.Caption) trailing = "" } - if len(a.ParameterMappings) > 0 { - var params []string - for _, pm := range a.ParameterMappings { - paramName := pm.Parameter - if idx := strings.LastIndex(paramName, "."); idx >= 0 { - paramName = paramName[idx+1:] - } - params = append(params, fmt.Sprintf("%s = %s", paramName, mdlQuoted(pm.Expression))) - } - lines = append(lines, fmt.Sprintf("%s%s %s%s with (%s)%s", indent, verb, mf, - asAndComment, strings.Join(params, ", "), trailing)) - } else { - lines = append(lines, fmt.Sprintf("%s%s %s%s%s", indent, verb, mf, asAndComment, trailing)) - } + args, legacy := workflowCallArguments(a.ParameterMappings) + lines = append(lines, fmt.Sprintf("%s%s %s%s%s%s%s", indent, verb, mf, args, asAndComment, legacy, trailing)) // Outcomes, then boundary events — the order the grammar requires // (workflowCallMicroflowStmt: … OUTCOMES? BOUNDARY EVENT?). Emitting them @@ -860,21 +849,9 @@ func formatCallWorkflowActivity(a *workflows.CallWorkflowActivity, indent string wf = "?" } - if len(a.ParameterMappings) > 0 { - var params []string - for _, pm := range a.ParameterMappings { - paramName := pm.Parameter - if idx := strings.LastIndex(paramName, "."); idx >= 0 { - paramName = paramName[idx+1:] - } - params = append(params, fmt.Sprintf("%s = %s", paramName, mdlQuoted(pm.Expression))) - } - lines = append(lines, fmt.Sprintf("%scall workflow %s%s comment %s with (%s)", indent, wf, - workflowActivityAsClause(a.Name, shortDocName(wf)), mdlQuoted(caption), strings.Join(params, ", "))) - } else { - lines = append(lines, fmt.Sprintf("%scall workflow %s%s comment %s", indent, wf, - workflowActivityAsClause(a.Name, shortDocName(wf)), mdlQuoted(caption))) - } + args, legacy := workflowCallArguments(a.ParameterMappings) + lines = append(lines, fmt.Sprintf("%scall workflow %s%s%s comment %s%s", indent, wf, args, + workflowActivityAsClause(a.Name, shortDocName(wf)), mdlQuoted(caption), legacy)) // BoundaryEvents lines = append(lines, formatBoundaryEvents(a.BoundaryEvents, indent+" ")...) @@ -1005,3 +982,42 @@ func formatConditionOutcomes(outcomes []workflows.ConditionOutcome, indent strin return lines } + +// workflowCallArguments renders a workflow call's parameter mappings in R4's +// argument form, `(Param = expression)` right after the callee +// (ako/mxcli#751). +// +// A stored expression that does not read back as itself when written bare — +// one with surrounding whitespace, or text the MDL expression grammar cannot +// parse — is rendered in the deprecated string form instead, returned as +// legacy (` with (Param = '')`, written after the comment): the +// string carries it byte for byte, and describe must never alter a stored +// expression. All mappings take the same form, since a call has one list. +func workflowCallArguments(mappings []*workflows.ParameterMapping) (args, legacy string) { + if len(mappings) == 0 { + return "", "" + } + bare := true + for _, pm := range mappings { + if !visitor.BareExpression(pm.Expression) { + bare = false + break + } + } + parts := make([]string, 0, len(mappings)) + for _, pm := range mappings { + name := pm.Parameter + if idx := strings.LastIndex(name, "."); idx >= 0 { + name = name[idx+1:] + } + if bare { + parts = append(parts, visitor.ParameterNameSpelling(name)+" = "+pm.Expression) + } else { + parts = append(parts, name+" = "+mdlQuoted(pm.Expression)) + } + } + if bare { + return "(" + strings.Join(parts, ", ") + ")", "" + } + return "", " with (" + strings.Join(parts, ", ") + ")" +} diff --git a/mdl/executor/describe_argument_binding_test.go b/mdl/executor/describe_argument_binding_test.go new file mode 100644 index 000000000..233097c26 --- /dev/null +++ b/mdl/executor/describe_argument_binding_test.go @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/visitor" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/microflows" + "github.com/mendixlabs/mxcli/sdk/workflows" +) + +// ako/mxcli#751 (R4): describe binds every argument as `Param = expression` +// and every text-template parameter as `with ({n} = expression)`. Each line +// must also parse back with no deprecation recorded. + +func assertCanonicalMicroflowLine(t *testing.T, line string) { + t.Helper() + prog, errs := visitor.Build("create microflow M.F ($O: M.E, $N: String) begin " + line + " end;") + if len(errs) > 0 { + t.Fatalf("%q does not parse: %v", line, errs[0]) + } + if len(prog.Deprecations) > 0 { + t.Errorf("%q records %s", line, prog.Deprecations[0].Code) + } +} + +func TestDescribe_ShowMessageTemplateArguments(t *testing.T) { + e := newTestExecutor() + action := µflows.ShowMessageAction{ + Type: microflows.MessageTypeWarning, + Template: &model.Text{Translations: map[string]string{"en_US": "Hi {1} {2}"}}, + TemplateParameters: []string{"$N", "$O/Name"}, + Blocking: true, + } + got := e.formatAction(action, nil, nil) + want := "show message 'Hi {1} {2}' type Warning with ({1} = $N, {2} = $O/Name) blocking;" + if got != want { + t.Errorf("got %q, want %q", got, want) + } + assertCanonicalMicroflowLine(t, got) +} + +// Describe used to drop a validation feedback's template arguments altogether, +// so describe -> exec lost them. +func TestDescribe_ValidationFeedbackTemplateArguments(t *testing.T) { + e := newTestExecutor() + action := µflows.ValidationFeedbackAction{ + ObjectVariable: "O", + AttributeName: "M.E.Name", + Template: &model.Text{Translations: map[string]string{"en_US": "{1} is wrong"}}, + TemplateParameters: []string{"$N"}, + } + got := e.formatAction(action, nil, nil) + want := "validation feedback $O/Name message '{1} is wrong' with ({1} = $N);" + if got != want { + t.Errorf("got %q, want %q", got, want) + } + assertCanonicalMicroflowLine(t, got) +} + +func TestDescribe_SendRestRequestArguments(t *testing.T) { + action := µflows.RestOperationCallAction{ + Operation: "M.C.Get", + ParameterMappings: []*microflows.RestParameterMapping{{Parameter: "M.C.Get.id", Value: "$N"}}, + } + got := formatRestOperationCallAction(nil, action) + if !strings.Contains(got, "with (id = $N)") { + t.Errorf("got %q, want the argument as `id = $N`", got) + } + assertCanonicalMicroflowLine(t, strings.ReplaceAll(got, "\n", " ")) +} + +func TestDescribe_WorkflowCallArguments(t *testing.T) { + bare := &workflows.CallMicroflowTask{ + Microflow: "M.G", + ParameterMappings: []*workflows.ParameterMapping{{Parameter: "M.G.Order", Expression: "$WorkflowContext"}}, + } + bare.Name = "act1" + bare.Caption = "Go" + got := strings.Join(formatSingleActivity(bare, " "), "\n") + if !strings.Contains(got, "call microflow M.G(Order = $WorkflowContext) as act1 comment 'Go'") { + t.Errorf("describe = %q", got) + } + assertCanonicalWorkflowLine(t, got, false) + + // A stored expression the MDL grammar cannot write bare keeps the string + // form, which carries it byte for byte. + odd := &workflows.CallWorkflowActivity{ + Workflow: "M.Sub", + ParameterMappings: []*workflows.ParameterMapping{{Parameter: "M.Sub.Order", Expression: "$WorkflowContext\n"}}, + } + odd.Name = "callWorkflow1" + odd.Caption = "Sub" + got = strings.Join(formatSingleActivity(odd, " "), "\n") + if !strings.Contains(got, "comment 'Sub' with (Order = '") { + t.Errorf("describe = %q", got) + } + prog := assertCanonicalWorkflowLine(t, got, true) + cw := prog.Statements[0].(*ast.CreateWorkflowStmt).Activities[0].(*ast.WorkflowCallWorkflowNode) + if len(cw.ParameterMappings) != 1 || cw.ParameterMappings[0].Expression != "$WorkflowContext\n" { + t.Errorf("re-parsed mappings = %+v, want the stored expression unchanged", cw.ParameterMappings) + } +} + +func assertCanonicalWorkflowLine(t *testing.T, line string, legacy bool) *ast.Program { + t.Helper() + prog, errs := visitor.Build("create workflow M.W parameter $WorkflowContext: M.E begin\n" + line + "\nend workflow;") + if len(errs) > 0 { + t.Fatalf("%q does not parse: %v", line, errs[0]) + } + if got := len(prog.Deprecations) > 0; got != legacy { + t.Errorf("%q: records a deprecation = %v, want %v", line, got, legacy) + } + return prog +} diff --git a/mdl/executor/validate_workflow_agent_test.go b/mdl/executor/validate_workflow_agent_test.go index 29212425e..4dd88be41 100644 --- a/mdl/executor/validate_workflow_agent_test.go +++ b/mdl/executor/validate_workflow_agent_test.go @@ -77,7 +77,7 @@ func TestWorkflowAgentTask_DescribeRoundTrips(t *testing.T) { out := strings.Join(formatSingleActivity(task, " "), "\n") // The caption is authored (not the microflow's name), so it is described as a // comment clause, which re-executes into the caption. - if !strings.Contains(out, "call agent microflow workflow.InvokeAgent as aiAgentTask1 comment 'AI Agent Task' with (workfow1context = '$WorkflowContext')") { + if !strings.Contains(out, "call agent microflow workflow.InvokeAgent(workfow1context = $WorkflowContext) as aiAgentTask1 comment 'AI Agent Task'") { t.Fatalf("describe = %q", out) } stmt := parseWorkflowStmt(t, "create workflow M.W\nbegin\n"+out+"\nend workflow;") diff --git a/mdl/grammar/domains/MDLMicroflow.g4 b/mdl/grammar/domains/MDLMicroflow.g4 index ff5a0c63a..480c44066 100644 --- a/mdl/grammar/domains/MDLMicroflow.g4 +++ b/mdl/grammar/domains/MDLMicroflow.g4 @@ -526,7 +526,7 @@ logLevel // Template parameters: WITH ({1} = expr, {2} = expr) or PARAMETERS [expr, expr] templateParams : WITH LPAREN templateParam (COMMA templateParam)* RPAREN // WITH ({1} = $var) - | PARAMETERS arrayLiteral // PARAMETERS ['val'] (deprecated) + | PARAMETERS /* @alias MDL-DEPR009 */ arrayLiteral // PARAMETERS ['val'] (deprecated) ; templateParam @@ -686,9 +686,11 @@ callArgumentList : callArgument (COMMA callArgument)* ; -// Named arguments: $FirstName = 'Hello' or Level = 'INFO' or OqlStatement = '...' +// Named arguments: FirstName = 'Hello' or Level = 'INFO' or OqlStatement = '...' +// (R4: `Param = expression`, no `$` on the parameter name). `$FirstName = …` is +// the deprecated spelling of the same argument. callArgument - : (VARIABLE | parameterName) EQUALS expression + : (VARIABLE /* @alias MDL-DEPR006 */ | parameterName) EQUALS expression ; showPageStatement @@ -699,9 +701,12 @@ showPageArgList : showPageArg (COMMA showPageArg)* ; +// R4: `Param = expression`, the argument form of every call site. `$Param = …` +// and `Param: …` are deprecated spellings of the same argument. showPageArg - : VARIABLE EQUALS (VARIABLE | expression) // $Param = $value (canonical) - | identifierOrKeyword COLON expression // Param: $value (widget-style, also accepted) + : parameterName EQUALS expression // Param = $value (canonical) + | VARIABLE /* @alias MDL-DEPR006 */ EQUALS (VARIABLE | expression) // $Param = $value + | identifierOrKeyword COLON /* @alias MDL-DEPR007 */ expression // Param: $value ; closePageStatement @@ -712,9 +717,12 @@ showHomePageStatement : SHOW HOME PAGE ; -// SHOW MESSAGE 'Hello {1}' TYPE Information OBJECTS [$Name]; +// SHOW MESSAGE 'Hello {1}' TYPE Information WITH ({1} = $Name); +// `OBJECTS [$Name]` is the deprecated positional spelling of the same list. showMessageStatement - : SHOW MESSAGE expression (TYPE identifierOrKeyword)? (OBJECTS LBRACKET expressionList RBRACKET)? BLOCKING? onErrorClause? + : SHOW MESSAGE expression (TYPE identifierOrKeyword)? + (OBJECTS /* @alias MDL-DEPR009 */ LBRACKET expressionList RBRACKET | templateParams)? + BLOCKING? onErrorClause? ; // SYNCHRONIZE ALL; @@ -740,7 +748,8 @@ throwStatement // VALIDATION FEEDBACK $Product/Code MESSAGE 'Product code cannot be empty'; validationFeedbackStatement - : VALIDATION FEEDBACK (attributePath | VARIABLE) MESSAGE expression (OBJECTS LBRACKET expressionList RBRACKET)? onErrorClause? + : VALIDATION FEEDBACK (attributePath | VARIABLE) MESSAGE expression + (OBJECTS /* @alias MDL-DEPR009 */ LBRACKET expressionList RBRACKET | templateParams)? onErrorClause? ; // ============================================================================= @@ -828,7 +837,7 @@ sendRestRequestWithClause ; sendRestRequestParam - : VARIABLE EQUALS expression + : (VARIABLE /* @alias MDL-DEPR006 */ | parameterName) EQUALS expression ; sendRestRequestBodyClause diff --git a/mdl/grammar/domains/MDLPage.g4 b/mdl/grammar/domains/MDLPage.g4 index 3052d578b..2e651c26b 100644 --- a/mdl/grammar/domains/MDLPage.g4 +++ b/mdl/grammar/domains/MDLPage.g4 @@ -667,16 +667,18 @@ actionExprV3 | COMPLETE_TASK STRING_LITERAL // COMPLETE_TASK 'OutcomeName' ; -// V3 Microflow arguments: (Param: value, ...) +// V3 Microflow arguments: (Param = value, ...) — R4, the argument form of every +// call site. `Param: value` and `$Param = value` are deprecated spellings. microflowArgsV3 : LPAREN microflowArgV3 (COMMA microflowArgV3)* RPAREN ; microflowArgV3 - : identifierOrKeyword COLON expression // Param: $value (identifierOrKeyword so a param - // named after a keyword — View/Source/Item/Page/ - // Entity — works unquoted, matching callArgument) - | VARIABLE EQUALS expression // $Param = $value (microflow-style, also accepted) + : parameterName EQUALS expression // Param = $value (parameterName so a param named + // after a keyword — View/Source/Item/Page/Entity — + // works unquoted, matching callArgument) + | identifierOrKeyword COLON /* @alias MDL-DEPR007 */ expression // Param: $value + | VARIABLE /* @alias MDL-DEPR006 */ EQUALS expression // $Param = $value ; // A value in `Visible: Attr in (…)`: an enumeration value name, true/false, diff --git a/mdl/grammar/domains/MDLWorkflow.g4 b/mdl/grammar/domains/MDLWorkflow.g4 index a03f3e833..26706d806 100644 --- a/mdl/grammar/domains/MDLWorkflow.g4 +++ b/mdl/grammar/domains/MDLWorkflow.g4 @@ -237,19 +237,31 @@ workflowUserTaskOutcome * invoked. */ workflowCallMicroflowStmt - : CALL AGENT? MICROFLOW qualifiedName (AS workflowActivityName)? (COMMENT STRING_LITERAL)? - (WITH LPAREN workflowParameterMapping (COMMA workflowParameterMapping)* RPAREN)? + : CALL AGENT? MICROFLOW qualifiedName workflowCallArguments? (AS workflowActivityName)? (COMMENT STRING_LITERAL)? + (WITH /* @alias MDL-DEPR008 */ LPAREN workflowParameterMapping (COMMA workflowParameterMapping)* RPAREN)? (OUTCOMES workflowConditionOutcome+)? (BOUNDARY EVENT workflowBoundaryEventClause ((BOUNDARY EVENT)? workflowBoundaryEventClause)*)? ; +// R4: a workflow call binds its arguments like every other call site, +// `(Param = expression)` right after the callee, the expression bare. +// `with (Param = '')`, the expression in a string, is the +// deprecated spelling of the same mapping. +workflowCallArguments + : LPAREN (workflowCallArgument (COMMA workflowCallArgument)*)? RPAREN + ; + +workflowCallArgument + : parameterName EQUALS expression + ; + workflowParameterMapping : qualifiedName EQUALS STRING_LITERAL ; workflowCallWorkflowStmt - : CALL WORKFLOW qualifiedName (AS workflowActivityName)? (COMMENT STRING_LITERAL)? - (WITH LPAREN workflowParameterMapping (COMMA workflowParameterMapping)* RPAREN)? + : CALL WORKFLOW qualifiedName workflowCallArguments? (AS workflowActivityName)? (COMMENT STRING_LITERAL)? + (WITH /* @alias MDL-DEPR008 */ LPAREN workflowParameterMapping (COMMA workflowParameterMapping)* RPAREN)? ; workflowDecisionStmt diff --git a/mdl/upgrade/argument_binding_test.go b/mdl/upgrade/argument_binding_test.go new file mode 100644 index 000000000..3a35e6dbf --- /dev/null +++ b/mdl/upgrade/argument_binding_test.go @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 + +package upgrade + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/deprecation" +) + +// ako/mxcli#751 (R4): fmt --upgrade rewrites every argument to `Param = +// expression` and every positional text-template list to `with ({n} = …)`, +// touching nothing else — comments, layout, keyword case and the arguments' +// own text survive. +func TestUpgrade_ArgumentBinding(t *testing.T) { + src := `create microflow M.F ($O: M.E, $N: String) begin + call microflow M.G($Order = $O, Force = false); -- keep me + show page M.P(Order: $O); + SHOW MESSAGE 'Hi {1} {2}' TYPE Warning OBJECTS [$N, $O/Name] BLOCKING; + validation feedback $O/Name message '{1}' objects [ $N ]; + log info 'x {1}' parameters ['a']; + $R = send rest request M.C.Get with ($id = $N); +end; +create page M.Q (Title: 'Q', Layout: Atlas_Core.Atlas_Default) { + dataview dv (DataSource: $O) { + actionbutton b (Caption: 'Go', Action: microflow M.G(Order:$currentObject)) + } +}; +create workflow M.W parameter $WorkflowContext: M.E begin + call microflow M.G as act1 comment 'Go' + with (M.G.Order = '$WorkflowContext'); +end workflow; +` + want := `create microflow M.F ($O: M.E, $N: String) begin + call microflow M.G(Order = $O, Force = false); -- keep me + show page M.P(Order = $O); + SHOW MESSAGE 'Hi {1} {2}' TYPE Warning WITH ({1} = $N, {2} = $O/Name) BLOCKING; + validation feedback $O/Name message '{1}' with ( {1} = $N ); + log info 'x {1}' with ({1} = 'a'); + $R = send rest request M.C.Get with (id = $N); +end; +create page M.Q (Title: 'Q', Layout: Atlas_Core.Atlas_Default) { + dataview dv (DataSource: $O) { + actionbutton b (Caption: 'Go', Action: microflow M.G(Order = $currentObject)) + } +}; +create workflow M.W parameter $WorkflowContext: M.E begin + call microflow M.G(Order = $WorkflowContext) as act1 comment 'Go'; +end workflow; +` + res := mustUpgrade(t, src, Options{}) + if res.Source != want { + t.Fatalf("got:\n%s\nwant:\n%s", res.Source, want) + } + for code, n := range map[string]int{ + deprecation.DollarArgumentName: 2, + deprecation.ColonArgument: 2, + deprecation.WorkflowStringArgument: 1, + deprecation.PositionalTemplateArguments: 3, + } { + if res.Rewritten[code] != n { + t.Errorf("Rewritten[%s] = %d, want %d (all: %v)", code, res.Rewritten[code], n, res.Rewritten) + } + } + if again := mustUpgrade(t, res.Source, Options{}); again.Changed() { + t.Errorf("a second upgrade changed the script again:\n%s", again.Source) + } +} + +// A workflow string argument that does not read back as the same bare +// expression is left as it is and reported, never guessed at. +func TestUpgrade_WorkflowStringArgumentWithoutBareForm(t *testing.T) { + src := "create workflow M.W parameter $WorkflowContext: M.E begin\n" + + " call microflow M.G with (Order = ' $WorkflowContext');\nend workflow;\n" + res := mustUpgrade(t, src, Options{}) + if res.Source != src { + t.Errorf("the script changed:\n%s", res.Source) + } + if len(res.Unrewritten) != 1 || res.Unrewritten[0].Code != deprecation.WorkflowStringArgument { + t.Errorf("Unrewritten = %+v, want the one MDL-DEPR008 use", res.Unrewritten) + } +} diff --git a/mdl/visitor/visitor_argument_binding.go b/mdl/visitor/visitor_argument_binding.go new file mode 100644 index 000000000..1b74d0f83 --- /dev/null +++ b/mdl/visitor/visitor_argument_binding.go @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: Apache-2.0 + +package visitor + +import ( + "fmt" + "sort" + "strings" + + "github.com/antlr4-go/antlr/v4" + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/deprecation" + "github.com/mendixlabs/mxcli/mdl/grammar/parser" +) + +// R4 (PROPOSAL_mdl_beta_syntax_freeze.md §3; ako/mxcli#751): every call site +// binds an argument as `Param = expression`, and every text template as +// `with ({1} = expression)`. The spellings below build exactly what the +// canonical form builds, so each is a registered alias (mdl/deprecation) whose +// use is recorded here, where its tokens are still at hand, together with the +// rewrite `fmt --upgrade` applies: +// +// - MDL-DEPR006 `$Param = e` — the `$` of a variable on a parameter name; +// - MDL-DEPR007 `Param: e` — `:` sets a model property, `=` binds a value; +// - MDL-DEPR008 a workflow call's `with (Param = '')`; +// - MDL-DEPR009 `objects [a, b]` / `parameters [a, b]`. + +// ExitCallArgument records `$Param = e` in call microflow/nanoflow/java +// action/javascript action/external action/web service/execute database query. +func (b *Builder) ExitCallArgument(ctx *parser.CallArgumentContext) { + b.recordDollarArgument(ctx.VARIABLE()) +} + +// ExitSendRestRequestParam records `$Param = e` in send rest request. +func (b *Builder) ExitSendRestRequestParam(ctx *parser.SendRestRequestParamContext) { + b.recordDollarArgument(ctx.VARIABLE()) +} + +// ExitShowPageArg records the two deprecated argument spellings of show page. +func (b *Builder) ExitShowPageArg(ctx *parser.ShowPageArgContext) { + if ctx.ParameterName() != nil { + return + } + if iok := ctx.IdentifierOrKeyword(); iok != nil { + b.recordColonArgument(iok, ctx.COLON(), ctx.Expression()) + return + } + if vars := ctx.AllVARIABLE(); len(vars) > 0 { + b.recordDollarArgument(vars[0]) + } +} + +// ExitMicroflowArgV3 records the two deprecated argument spellings of a page +// action or a microflow/nanoflow data source. +func (b *Builder) ExitMicroflowArgV3(ctx *parser.MicroflowArgV3Context) { + if ctx.ParameterName() != nil { + return + } + if iok := ctx.IdentifierOrKeyword(); iok != nil { + b.recordColonArgument(iok, ctx.COLON(), ctx.Expression()) + return + } + b.recordDollarArgument(ctx.VARIABLE()) +} + +// recordDollarArgument records MDL-DEPR006 for the parameter-name token v, with +// the rewrite that drops its `$`. +func (b *Builder) recordDollarArgument(v antlr.TerminalNode) { + if v == nil { + return + } + tok := v.GetSymbol() + name := strings.TrimPrefix(tok.GetText(), "$") + b.recordDeprecation(deprecation.DollarArgumentName, tok, "argument") + edit := ast.TextEdit{Start: tok.GetStart(), Stop: tok.GetStop() + 1, Text: ParameterNameSpelling(name)} + b.fixLastDeprecation(deprecation.DollarArgumentName, &ast.Fix{Edits: []ast.TextEdit{edit}}, "") +} + +// recordColonArgument records MDL-DEPR007 for `Param: e`, with the rewrite +// `Param = e`: the colon and the space around it become ` = `. +func (b *Builder) recordColonArgument(name parser.IIdentifierOrKeywordContext, colon antlr.TerminalNode, expr parser.IExpressionContext) { + if colon == nil { + return + } + b.recordDeprecation(deprecation.ColonArgument, colon.GetSymbol(), "argument") + nameStop := name.GetStop() + if nameStop == nil || expr == nil || expr.GetStart() == nil { + b.fixLastDeprecation(deprecation.ColonArgument, nil, "the argument is incomplete") + return + } + edit := replaceGap(nameStop.GetStop(), expr.GetStart().GetStart(), " = ") + b.fixLastDeprecation(deprecation.ColonArgument, &ast.Fix{Edits: []ast.TextEdit{edit}}, "") +} + +// ParameterNameSpelling spells a parameter name as parameterName accepts it: +// bare when it parses as one (an identifier or a keyword), quoted otherwise. +func ParameterNameSpelling(name string) string { + if parsesFully(name, func(p *parser.MDLParser) antlr.ParserRuleContext { return p.ParameterName() }) { + return name + } + return `"` + name + `"` +} + +// parsesFully reports whether text is exactly one rule, as rule parses it, +// with no syntax error and nothing left over. +func parsesFully(text string, rule func(*parser.MDLParser) antlr.ParserRuleContext) bool { + _, ok := parseRule(text, rule) + return ok +} + +func parseRule(text string, rule func(*parser.MDLParser) antlr.ParserRuleContext) (antlr.ParserRuleContext, bool) { + errs := newErrorListener() + lexer := parser.NewMDLLexer(newScriptStream(text)) + lexer.RemoveErrorListeners() + lexer.AddErrorListener(errs) + stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel) + p := parser.NewMDLParser(stream) + p.RemoveErrorListeners() + p.AddErrorListener(errs) + ctx := rule(p) + if len(errs.errors) > 0 || ctx == nil || stream.LA(1) != antlr.TokenEOF { + return nil, false + } + return ctx, true +} + +// BareExpression reports whether a stored expression can be written as a bare +// MDL expression that the visitor reads back as exactly that text. describe +// asks it before writing a workflow argument without quotes; fmt --upgrade +// asks it before unquoting one. +func BareExpression(expr string) bool { + if strings.TrimSpace(expr) != expr || expr == "" { + return false + } + ctx, ok := parseRule(expr, func(p *parser.MDLParser) antlr.ParserRuleContext { return p.Expression() }) + if !ok { + return false + } + return bareArgumentText(ctx.(parser.IExpressionContext)) == expr +} + +// bareArgumentText is the expression a bare workflow argument stores: its +// source text, as the Mendix expression editor would hold it. +func bareArgumentText(expr parser.IExpressionContext) string { + if se, ok := buildSourceExpression(expr).(*ast.SourceExpr); ok { + return se.Source + } + return stripExpressionIdentifierQuotes(expressionSourceText(expr)) +} + +// buildWorkflowCallArguments builds the canonical `(Param = expression)` list +// of a workflow call. +func buildWorkflowCallArguments(ctx parser.IWorkflowCallArgumentsContext) []ast.WorkflowParameterMappingNode { + if ctx == nil { + return nil + } + var out []ast.WorkflowParameterMappingNode + for _, a := range ctx.(*parser.WorkflowCallArgumentsContext).AllWorkflowCallArgument() { + arg := a.(*parser.WorkflowCallArgumentContext) + if arg.ParameterName() == nil || arg.Expression() == nil { + continue + } + out = append(out, ast.WorkflowParameterMappingNode{ + Parameter: parameterNameText(arg.ParameterName()), + Expression: bareArgumentText(arg.Expression()), + }) + } + return out +} + +// workflowCallCtx is what the two workflow call statements have in common. +type workflowCallCtx interface { + antlr.ParserRuleContext + QualifiedName() parser.IQualifiedNameContext + WorkflowCallArguments() parser.IWorkflowCallArgumentsContext + WITH() antlr.TerminalNode + RPAREN() antlr.TerminalNode + AllWorkflowParameterMapping() []parser.IWorkflowParameterMappingContext +} + +// ExitWorkflowCallMicroflowStmt records MDL-DEPR008 on a workflow's call +// microflow activity. +func (b *Builder) ExitWorkflowCallMicroflowStmt(ctx *parser.WorkflowCallMicroflowStmtContext) { + b.recordWorkflowStringArguments(ctx) +} + +// ExitWorkflowCallWorkflowStmt records MDL-DEPR008 on a workflow's call +// workflow activity. +func (b *Builder) ExitWorkflowCallWorkflowStmt(ctx *parser.WorkflowCallWorkflowStmtContext) { + b.recordWorkflowStringArguments(ctx) +} + +func (b *Builder) recordWorkflowStringArguments(ctx workflowCallCtx) { + with := ctx.WITH() + if with == nil { + return + } + if ctx.WorkflowCallArguments() != nil { + b.addError(fmt.Errorf("line %d: a workflow call has two argument lists; write the arguments once, "+ + "as `(Param = expression)` after the callee", with.GetSymbol().GetLine())) + return + } + b.recordDeprecation(deprecation.WorkflowStringArgument, with.GetSymbol(), "workflow call") + fix, why := workflowStringArgumentsFix(ctx) + b.fixLastDeprecation(deprecation.WorkflowStringArgument, fix, why) +} + +// workflowStringArgumentsFix moves `with (P = 'e', …)` to `(P = e, …)` after +// the callee. A string whose content is not a bare expression that reads back +// as itself has no rewrite: unquoting it would change the stored expression. +func workflowStringArgumentsFix(ctx workflowCallCtx) (*ast.Fix, string) { + qn, rparen := ctx.QualifiedName(), ctx.RPAREN() + if qn == nil || qn.GetStop() == nil || rparen == nil { + return nil, "the argument list is incomplete" + } + var args []string + for _, m := range ctx.AllWorkflowParameterMapping() { + pm := m.(*parser.WorkflowParameterMappingContext) + if pm.QualifiedName() == nil || pm.STRING_LITERAL() == nil { + return nil, "the argument list is incomplete" + } + expr := unquoteStringLit(pm.STRING_LITERAL()) + if !BareExpression(expr) { + return nil, fmt.Sprintf("the string %s does not read back as the same bare expression; "+ + "write the argument as `(Param = expression)` by hand", pm.STRING_LITERAL().GetText()) + } + name := bareWorkflowParameterName(pm.QualifiedName().GetText()) + args = append(args, ParameterNameSpelling(name)+" = "+expr) + } + // Delete ` with (…)` from the end of the token before WITH, and write the + // list after the callee. + withTok := ctx.WITH().GetSymbol() + prevStop := -1 + for i := 0; i < ctx.GetChildCount(); i++ { + if t, ok := ctx.GetChild(i).(antlr.TerminalNode); ok && t.GetSymbol() == withTok { + break + } + if _, stop := nodeSpan(ctx.GetChild(i)); stop >= 0 { + prevStop = stop + } + } + if prevStop < 0 { + return nil, "the argument list is incomplete" + } + return &ast.Fix{Edits: []ast.TextEdit{ + insertAt(qn.GetStop().GetStop()+1, "("+strings.Join(args, ", ")+")"), + {Start: prevStop + 1, Stop: rparen.GetSymbol().GetStop() + 1, Text: ""}, + }}, "" +} + +// ExitShowMessageStatement records MDL-DEPR009 for `objects [..]` and checks +// the numbering of `with ({n} = …)`. +func (b *Builder) ExitShowMessageStatement(ctx *parser.ShowMessageStatementContext) { + b.recordObjectsList(ctx.OBJECTS(), ctx.LBRACKET(), ctx.ExpressionList(), ctx.RBRACKET()) + b.checkTemplateNumbering(ctx.TemplateParams()) +} + +// ExitValidationFeedbackStatement is ExitShowMessageStatement for validation +// feedback. +func (b *Builder) ExitValidationFeedbackStatement(ctx *parser.ValidationFeedbackStatementContext) { + b.recordObjectsList(ctx.OBJECTS(), ctx.LBRACKET(), ctx.ExpressionList(), ctx.RBRACKET()) + b.checkTemplateNumbering(ctx.TemplateParams()) +} + +func (b *Builder) recordObjectsList(objects, lbracket antlr.TerminalNode, list parser.IExpressionListContext, rbracket antlr.TerminalNode) { + if objects == nil { + return + } + b.recordDeprecation(deprecation.PositionalTemplateArguments, objects.GetSymbol(), "text template") + if lbracket == nil || rbracket == nil || list == nil { + b.fixLastDeprecation(deprecation.PositionalTemplateArguments, nil, "the list is incomplete") + return + } + var items []antlr.ParserRuleContext + for _, e := range list.(*parser.ExpressionListContext).AllExpression() { + items = append(items, e) + } + b.fixLastDeprecation(deprecation.PositionalTemplateArguments, + numberedListFix(objects.GetSymbol(), lbracket.GetSymbol(), items, rbracket.GetSymbol()), "") +} + +// numberedListFix rewrites `kw [a, b]` as `with ({1} = a, {2} = b)`: the +// keyword and bracket become `with (`, each item gets its number, and the +// closing bracket becomes `)`. The items themselves are not touched. +func numberedListFix(kw, lbracket antlr.Token, items []antlr.ParserRuleContext, rbracket antlr.Token) *ast.Fix { + edits := []ast.TextEdit{replaceSpan(kw, lbracket, keywordLike(kw.GetText(), "with")+" (")} + for i, it := range items { + if it.GetStart() == nil { + return nil + } + edits = append(edits, insertAt(it.GetStart().GetStart(), fmt.Sprintf("{%d} = ", i+1))) + } + edits = append(edits, replaceSpan(rbracket, rbracket, ")")) + return &ast.Fix{Edits: edits} +} + +// ExitTemplateParams records MDL-DEPR009 for `parameters [..]`. +func (b *Builder) ExitTemplateParams(ctx *parser.TemplateParamsContext) { + kw := ctx.PARAMETERS() + if kw == nil { + return + } + b.recordDeprecation(deprecation.PositionalTemplateArguments, kw.GetSymbol(), "text template") + arr, ok := ctx.ArrayLiteral().(*parser.ArrayLiteralContext) + if !ok || arr == nil || arr.LBRACKET() == nil || arr.RBRACKET() == nil { + b.fixLastDeprecation(deprecation.PositionalTemplateArguments, nil, "the list is incomplete") + return + } + var items []antlr.ParserRuleContext + for _, l := range arr.AllLiteral() { + items = append(items, l) + } + if len(items) == 0 { + // `parameters []` binds nothing: the clause goes, with the space before it. + if prev := previousTokenStop(ctx); prev >= 0 { + edit := ast.TextEdit{Start: prev + 1, Stop: arr.RBRACKET().GetSymbol().GetStop() + 1} + b.fixLastDeprecation(deprecation.PositionalTemplateArguments, &ast.Fix{Edits: []ast.TextEdit{edit}}, "") + } + return + } + b.fixLastDeprecation(deprecation.PositionalTemplateArguments, + numberedListFix(kw.GetSymbol(), arr.LBRACKET().GetSymbol(), items, arr.RBRACKET().GetSymbol()), "") +} + +// previousTokenStop is the rune offset of the last rune before ctx's first +// token that is not hidden, or -1. +func previousTokenStop(ctx antlr.ParserRuleContext) int { + start := ctx.GetStart() + if start == nil || start.GetInputStream() == nil { + return -1 + } + return start.GetStart() - 1 - leadingSpace(start) +} + +// leadingSpace counts the whitespace runes right before tok. +func leadingSpace(tok antlr.Token) int { + in := tok.GetInputStream() + n := 0 + for i := tok.GetStart() - 1; i >= 0; i-- { + r := in.GetText(i, i) + if r != " " && r != "\t" && r != "\n" && r != "\r" { + break + } + n++ + } + return n +} + +// checkTemplateNumbering refuses a `with ({n} = …)` list on a show message or +// validation feedback whose numbers are not 1..N, each once: the model holds +// the arguments as a list, so `{3}` with no `{2}` has no place to go. +func (b *Builder) checkTemplateNumbering(ctx parser.ITemplateParamsContext) { + if ctx == nil { + return + } + tp := ctx.(*parser.TemplateParamsContext) + if tp.WITH() == nil { + return + } + var nums []int + for _, p := range tp.AllTemplateParam() { + if n := p.(*parser.TemplateParamContext).NUMBER_LITERAL(); n != nil { + var v int + fmt.Sscanf(n.GetText(), "%d", &v) + nums = append(nums, v) + } + } + sort.Ints(nums) + for i, n := range nums { + if n != i+1 { + b.addError(fmt.Errorf("line %d: the placeholders of a text template are numbered {1} to {%d}, "+ + "each once; got %v", tp.GetStart().GetLine(), len(nums), nums)) + return + } + } +} + +// templateArgsByNumber orders a `with ({n} = …)` list by its numbers, the +// positional list a show message or validation feedback stores. +func templateArgsByNumber(params []ast.TemplateParam) []ast.Expression { + sorted := append([]ast.TemplateParam(nil), params...) + sort.SliceStable(sorted, func(i, j int) bool { return sorted[i].Index < sorted[j].Index }) + out := make([]ast.Expression, 0, len(sorted)) + for _, p := range sorted { + out = append(out, p.Value) + } + return out +} diff --git a/mdl/visitor/visitor_argument_binding_test.go b/mdl/visitor/visitor_argument_binding_test.go new file mode 100644 index 000000000..47f22a918 --- /dev/null +++ b/mdl/visitor/visitor_argument_binding_test.go @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: Apache-2.0 + +package visitor + +import ( + "reflect" + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/deprecation" +) + +// R4 (ako/mxcli#751): every call site binds an argument as `Param = +// expression`, and every text template as `with ({1} = expression)`. Each old +// spelling must record its code, and build exactly the statement its canonical +// form builds — the proof that it is an alias and not a change of meaning. +func TestArgumentBindingAliases(t *testing.T) { + const page = "create page M.P (Title: 'P', Layout: Atlas_Core.Atlas_Default) { dataview dv (DataSource: $O) { %s } };" + mf := func(body string) string { + return "create microflow M.F ($O: M.E, $N: String) begin " + body + " end;" + } + wf := func(body string) string { + return "create workflow M.W parameter $WorkflowContext: M.E begin " + body + " end workflow;" + } + pg := func(widget string) string { return strings.Replace(page, "%s", widget, 1) } + cases := []struct { + name, old, canonical string + codes []string + }{ + {"call microflow", mf("call microflow M.G($Order = $O, Force = false);"), + mf("call microflow M.G(Order = $O, Force = false);"), []string{deprecation.DollarArgumentName}}, + {"call nanoflow", mf("$R = call nanoflow M.G($Order = $O);"), + mf("$R = call nanoflow M.G(Order = $O);"), []string{deprecation.DollarArgumentName}}, + {"call java action", mf("$R = call java action M.J($Amount = 1);"), + mf("$R = call java action M.J(Amount = 1);"), []string{deprecation.DollarArgumentName}}, + {"keyword-named parameter is quoted only when it must be", mf("call microflow M.G($Page = $O);"), + mf("call microflow M.G(Page = $O);"), []string{deprecation.DollarArgumentName}}, + {"send rest request", mf("$R = send rest request M.C.Get with ($id = $N);"), + mf("$R = send rest request M.C.Get with (id = $N);"), []string{deprecation.DollarArgumentName}}, + {"show page dollar", mf("show page M.P($Order = $O);"), + mf("show page M.P(Order = $O);"), []string{deprecation.DollarArgumentName}}, + {"show page colon", mf("show page M.P(Order: $O);"), + mf("show page M.P(Order = $O);"), []string{deprecation.ColonArgument}}, + {"button action colon", pg("actionbutton b (Caption: 'Go', Action: microflow M.G(Order: $currentObject))"), + pg("actionbutton b (Caption: 'Go', Action: microflow M.G(Order = $currentObject))"), []string{deprecation.ColonArgument}}, + {"button action dollar", pg("actionbutton b (Caption: 'Go', Action: nanoflow M.G($Order = $currentObject))"), + pg("actionbutton b (Caption: 'Go', Action: nanoflow M.G(Order = $currentObject))"), []string{deprecation.DollarArgumentName}}, + {"show_page action colon", pg("actionbutton b (Caption: 'Go', Action: show_page M.Q(Order: $currentObject))"), + pg("actionbutton b (Caption: 'Go', Action: show_page M.Q(Order = $currentObject))"), []string{deprecation.ColonArgument}}, + {"data source colon", pg("listview lv (DataSource: microflow M.DS(Order: $O, Limit: 10)) { }"), + pg("listview lv (DataSource: microflow M.DS(Order = $O, Limit = 10)) { }"), + []string{deprecation.ColonArgument, deprecation.ColonArgument}}, + {"workflow call microflow", wf("call microflow M.G as act1 comment 'Go' with (M.G.Order = '$WorkflowContext');"), + wf("call microflow M.G(Order = $WorkflowContext) as act1 comment 'Go';"), []string{deprecation.WorkflowStringArgument}}, + {"workflow call workflow", wf("call workflow M.Sub comment 'Sub' with (Order = '$WorkflowContext/M.E_Other');"), + wf("call workflow M.Sub(Order = $WorkflowContext/M.E_Other) comment 'Sub';"), []string{deprecation.WorkflowStringArgument}}, + {"workflow expression with a string inside", wf("call microflow M.G with (Label = 'if $WorkflowContext/Name = ''x'' then ''a'' else ''b''');"), + wf("call microflow M.G(Label = if $WorkflowContext/Name = 'x' then 'a' else 'b');"), []string{deprecation.WorkflowStringArgument}}, + {"show message objects", mf("show message 'Hi {1} {2}' type Warning objects [$N, $O/Name] blocking;"), + mf("show message 'Hi {1} {2}' type Warning with ({1} = $N, {2} = $O/Name) blocking;"), []string{deprecation.PositionalTemplateArguments}}, + {"validation feedback objects", mf("validation feedback $O/Name message '{1} is wrong' objects [$N];"), + mf("validation feedback $O/Name message '{1} is wrong' with ({1} = $N);"), []string{deprecation.PositionalTemplateArguments}}, + {"log parameters", mf("log info 'a {1} {2}' parameters ['x', 2];"), + mf("log info 'a {1} {2}' with ({1} = 'x', {2} = 2);"), []string{deprecation.PositionalTemplateArguments}}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + old := mustBuild(t, c.old) + if got := deprecationCodes(old); !reflect.DeepEqual(got, c.codes) { + t.Errorf("old form recorded %v, want %v", got, c.codes) + } + for _, d := range old.Deprecations { + if d.Fix == nil { + t.Errorf("%s at line %d has no rewrite: %s", d.Code, d.Line, d.NoFix) + } + } + canon := mustBuild(t, c.canonical) + if got := deprecationCodes(canon); len(got) != 0 { + t.Errorf("canonical form recorded %v, want none", got) + } + if !reflect.DeepEqual(old.Statements, canon.Statements) { + t.Errorf("the two forms build different statements:\n old: %#v\n canon: %#v", old.Statements, canon.Statements) + } + }) + } +} + +// The canonical workflow argument stores the bare expression's text, exactly +// what the string form stored. +func TestWorkflowCallArgumentStoresTheExpression(t *testing.T) { + prog := mustBuild(t, "create workflow M.W parameter $WorkflowContext: M.E begin "+ + "call microflow M.G(Order = $WorkflowContext, Count = 1 + 2); end workflow;") + wf := prog.Statements[0].(*ast.CreateWorkflowStmt) + cm := wf.Activities[0].(*ast.WorkflowCallMicroflowNode) + want := []ast.WorkflowParameterMappingNode{ + {Parameter: "Order", Expression: "$WorkflowContext"}, + {Parameter: "Count", Expression: "1 + 2"}, + } + if !reflect.DeepEqual(cm.ParameterMappings, want) { + t.Errorf("ParameterMappings = %+v, want %+v", cm.ParameterMappings, want) + } +} + +// A workflow string argument whose content is not an expression that reads +// back as itself keeps its string: the upgrade reports it rather than +// changing the stored expression. +func TestWorkflowStringArgumentWithoutBareFormHasNoRewrite(t *testing.T) { + for _, arg := range []string{"' $WorkflowContext'", "''", "'$WorkflowContext +'"} { + prog := mustBuild(t, "create workflow M.W parameter $WorkflowContext: M.E begin "+ + "call microflow M.G with (Order = "+arg+"); end workflow;") + if len(prog.Deprecations) != 1 || prog.Deprecations[0].Fix != nil || prog.Deprecations[0].NoFix == "" { + t.Errorf("%s: deprecations = %+v, want one with no rewrite and a reason", arg, prog.Deprecations) + } + } +} + +func TestWorkflowCallWithBothArgumentListsIsAnError(t *testing.T) { + _, errs := Build("create workflow M.W parameter $WorkflowContext: M.E begin " + + "call microflow M.G(Order = $WorkflowContext) with (Order = '$WorkflowContext'); end workflow;") + if len(errs) == 0 || !strings.Contains(errs[0].Error(), "two argument lists") { + t.Errorf("errs = %v, want the two-lists error", errs) + } +} + +// with ({n} = …) on a show message is stored by number, and the numbers must +// be 1..N: the model holds a list, so a gap has nowhere to go. +func TestTemplateArgumentsByNumber(t *testing.T) { + prog := mustBuild(t, "create microflow M.F ($A: String, $B: String) begin "+ + "show message '{1} {2}' with ({2} = $B, {1} = $A); end;") + sm := prog.Statements[0].(*ast.CreateMicroflowStmt).Body[0].(*ast.ShowMessageStmt) + if len(sm.TemplateArgs) != 2 || !strings.Contains(fmtExpr(sm.TemplateArgs[0]), "A") { + t.Errorf("TemplateArgs = %#v, want $A then $B", sm.TemplateArgs) + } + for _, bad := range []string{"with ({1} = $A, {3} = $B)", "with ({1} = $A, {1} = $B)", "with ({2} = $A)"} { + _, errs := Build("create microflow M.F ($A: String, $B: String) begin show message 'x' " + bad + "; end;") + if len(errs) == 0 || !strings.Contains(errs[0].Error(), "numbered {1}") { + t.Errorf("%s: errs = %v, want the numbering error", bad, errs) + } + } +} + +func fmtExpr(e ast.Expression) string { + if v, ok := e.(*ast.VariableExpr); ok { + return v.Name + } + return reflect.ValueOf(e).String() +} + +func TestBareExpression(t *testing.T) { + for expr, want := range map[string]bool{ + "$WorkflowContext": true, + "$WorkflowContext/M.A/Name": true, + "1 + 2": true, + "'a''b'": true, + "": false, + " $WorkflowContext": false, + "$WorkflowContext +": false, + "$WorkflowContext; drop x": false, + } { + if got := BareExpression(expr); got != want { + t.Errorf("BareExpression(%q) = %v, want %v", expr, got, want) + } + } +} + +func TestParameterNameSpelling(t *testing.T) { + for name, want := range map[string]string{ + "Order": "Order", + "Page": "Page", + "with": "with", + "a b": `"a b"`, + } { + if got := ParameterNameSpelling(name); got != want { + t.Errorf("ParameterNameSpelling(%q) = %q, want %q", name, got, want) + } + } +} diff --git a/mdl/visitor/visitor_microflow_actions.go b/mdl/visitor/visitor_microflow_actions.go index 6c3559f46..7b3cc39ea 100644 --- a/mdl/visitor/visitor_microflow_actions.go +++ b/mdl/visitor/visitor_microflow_actions.go @@ -1241,14 +1241,20 @@ func buildShowPageArgList(ctx parser.IShowPageArgListContext) []ast.ShowPageArg arg := argCtx.(*parser.ShowPageArgContext) spa := ast.ShowPageArg{} - if iok := arg.IdentifierOrKeyword(); iok != nil { - // Widget-style: Param: $value + if pn := arg.ParameterName(); pn != nil { + // Canonical (R4): Param = $value + spa.ParamName = parameterNameText(pn) + if expr := arg.Expression(); expr != nil { + spa.Value = buildSourceExpression(expr) + } + } else if iok := arg.IdentifierOrKeyword(); iok != nil { + // Deprecated (MDL-DEPR007): Param: $value spa.ParamName = identifierOrKeywordText(iok) if expr := arg.Expression(); expr != nil { spa.Value = buildSourceExpression(expr) } } else { - // Canonical: $Param = $value + // Deprecated (MDL-DEPR006): $Param = $value vars := arg.AllVARIABLE() if len(vars) >= 1 { spa.ParamName = strings.TrimPrefix(vars[0].GetText(), "$") @@ -1286,7 +1292,11 @@ func buildShowMessageStatement(ctx parser.IShowMessageStatementContext) *ast.Sho stmt.Type = id.GetText() } - // Build template arguments (optional) + // Build template arguments (optional): `with ({1} = e)`, or the deprecated + // positional `objects [e]` (MDL-DEPR009). + if tp := smCtx.TemplateParams(); tp != nil { + stmt.TemplateArgs = templateArgsByNumber(buildTemplateParams(tp)) + } if exprList := smCtx.ExpressionList(); exprList != nil { listCtx := exprList.(*parser.ExpressionListContext) allExprs := listCtx.AllExpression() @@ -1380,7 +1390,11 @@ func buildValidationFeedbackStatement(ctx parser.IValidationFeedbackStatementCon stmt.Message = buildSourceExpression(msgExpr) } - // Build template arguments (optional) + // Build template arguments (optional): `with ({1} = e)`, or the deprecated + // positional `objects [e]` (MDL-DEPR009). + if tp := vfCtx.TemplateParams(); tp != nil { + stmt.TemplateArgs = templateArgsByNumber(buildTemplateParams(tp)) + } if exprList := vfCtx.ExpressionList(); exprList != nil { listCtx := exprList.(*parser.ExpressionListContext) allExprs := listCtx.AllExpression() @@ -1678,6 +1692,8 @@ func buildSendRestRequestStatement(ctx parser.ISendRestRequestStatementContext) param := ast.SendRestParamDef{} if v := pc.VARIABLE(); v != nil { param.Name = strings.TrimPrefix(v.GetText(), "$") + } else if pn := pc.ParameterName(); pn != nil { + param.Name = parameterNameText(pn) } if expr := pc.Expression(); expr != nil { param.Expression = expressionSourceText(expr) diff --git a/mdl/visitor/visitor_page_v3.go b/mdl/visitor/visitor_page_v3.go index 99815096e..2b3336413 100644 --- a/mdl/visitor/visitor_page_v3.go +++ b/mdl/visitor/visitor_page_v3.go @@ -1192,11 +1192,14 @@ func buildMicroflowArgV3(ctx parser.IMicroflowArgV3Context) ast.FlowArgV3 { argCtx := ctx.(*parser.MicroflowArgV3Context) arg := ast.FlowArgV3{} - if v := argCtx.VARIABLE(); v != nil { - // Microflow-style: $Param = $value + if pn := argCtx.ParameterName(); pn != nil { + // Canonical (R4): Param = $value + arg.Name = parameterNameText(pn) + } else if v := argCtx.VARIABLE(); v != nil { + // Deprecated (MDL-DEPR006): $Param = $value arg.Name = strings.TrimPrefix(v.GetText(), "$") } else if iok := argCtx.IdentifierOrKeyword(); iok != nil { - // Widget-style: Param: $value. identifierOrKeyword accepts a bare + // Deprecated (MDL-DEPR007): Param: $value. identifierOrKeyword accepts a bare // keyword (View/Source/Item/Page/Entity) or a "quoted" name; // identifierOrKeywordText unquotes as needed. arg.Name = identifierOrKeywordText(iok) diff --git a/mdl/visitor/visitor_workflow.go b/mdl/visitor/visitor_workflow.go index f236c3397..595c4ca16 100644 --- a/mdl/visitor/visitor_workflow.go +++ b/mdl/visitor/visitor_workflow.go @@ -677,7 +677,10 @@ func buildWorkflowCallMicroflow(ctx parser.IWorkflowCallMicroflowStmtContext) *a } // Parameter mappings (Issue #10) - node.ParameterMappings = buildWorkflowParameterMappings(cmCtx.AllWorkflowParameterMapping()) + node.ParameterMappings = buildWorkflowCallArguments(cmCtx.WorkflowCallArguments()) + if len(node.ParameterMappings) == 0 { + node.ParameterMappings = buildWorkflowParameterMappings(cmCtx.AllWorkflowParameterMapping()) + } // BoundaryEvents (Issue #7) for _, beCtx := range cmCtx.AllWorkflowBoundaryEventClause() { @@ -743,7 +746,10 @@ func buildWorkflowCallWorkflow(ctx parser.IWorkflowCallWorkflowStmtContext) *ast } // Parameter mappings - node.ParameterMappings = buildWorkflowParameterMappings(cwCtx.AllWorkflowParameterMapping()) + node.ParameterMappings = buildWorkflowCallArguments(cwCtx.WorkflowCallArguments()) + if len(node.ParameterMappings) == 0 { + node.ParameterMappings = buildWorkflowParameterMappings(cwCtx.AllWorkflowParameterMapping()) + } return node } From e8d70e8d8e5684ebbe3cd666409bd74a711cc13b Mon Sep 17 00:00:00 2001 From: Ako Date: Sun, 27 Sep 2026 19:42:46 +0000 Subject: [PATCH 03/11] test: show_page describe expectations use Param = $currentObject (#751) Co-Authored-By: Claude Opus 5.5 --- mdl/executor/cmd_pages_describe_pageparams_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mdl/executor/cmd_pages_describe_pageparams_test.go b/mdl/executor/cmd_pages_describe_pageparams_test.go index 9f2b1b456..b127ee95d 100644 --- a/mdl/executor/cmd_pages_describe_pageparams_test.go +++ b/mdl/executor/cmd_pages_describe_pageparams_test.go @@ -56,7 +56,7 @@ func showPageAction(page string, mappings []any) map[string]any { } } -// mxcli-formula1 §39: `SHOW_PAGE P(Race: $currentObject)` described back as +// mxcli-formula1 §39: `SHOW_PAGE P(Race = $currentObject)` described back as // `show_page P`. The mapping was never in the model — mxcli stores it implicitly // on purpose — but DESCRIBE had no compensating recovery, so its output read as // a diagnosis ("the mapping was dropped, that is why the page gets an empty @@ -66,7 +66,7 @@ func TestRenderShowPageAction_RecoversTheImplicitParameter(t *testing.T) { ctx := pageParamFixture(t, "Race") got := renderClientActionMDL(ctx, showPageAction("Formula1Frontend.Race_Weekend", nil)) - want := "show_page Formula1Frontend.Race_Weekend(Race: $currentObject)" + want := "show_page Formula1Frontend.Race_Weekend(Race = $currentObject)" if got != want { t.Errorf("DESCRIBE lost the page parameter:\n got: %s\nwant: %s", got, want) } @@ -77,7 +77,7 @@ func TestRenderShowPageAction_RecoversEveryParameter(t *testing.T) { ctx := pageParamFixture(t, "Race", "Season") got := renderClientActionMDL(ctx, showPageAction("Formula1Frontend.Race_Weekend", nil)) - want := "show_page Formula1Frontend.Race_Weekend(Race: $currentObject, Season: $currentObject)" + want := "show_page Formula1Frontend.Race_Weekend(Race = $currentObject, Season = $currentObject)" if got != want { t.Errorf("got: %s\nwant: %s", got, want) } From 5b3666a2251cb2a135d8b67c64cdb65e08281a6c Mon Sep 17 00:00:00 2001 From: Ako Date: Sun, 27 Sep 2026 19:48:35 +0000 Subject: [PATCH 04/11] mdl: page actions as words and one spelling per keyword (R8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Page actions use the words a microflow uses: show page, save changes, cancel changes, close page, create object, delete, open link, sign out, complete task, call microflow, call nanoflow. A rule's message is `error message '…'` everywhere, an association's delete behaviour is `on delete …`, the many-to-many type is ReferenceSet and a REST call that ignores its response `returns nothing`. describe writes these. Every old spelling keeps parsing with the same meaning and is a registered deprecated alias with an fmt --upgrade rewrite: MDL-DEPR020 (snake-case page actions, a flow call without `call`, a menu item's sign_out), MDL-DEPR021 (error / feedback / error_message), MDL-DEPR022 (delete_behavior), MDL-DEPR023 (reference_set) and MDL-DEPR024 (returns none). `delete_object close_page`, which describe printed but the grammar refused, now parses. Refs #752 Co-Authored-By: Claude Opus 5.5 --- cmd/mxcli/lsp_completions_gen.go | 10 +- mdl/deprecation/deprecation.go | 90 +++++++++ mdl/executor/cmd_associations.go | 2 +- mdl/executor/cmd_diff_mdl.go | 24 +-- mdl/executor/cmd_entities_describe.go | 4 +- .../cmd_enumerations_caption_language_test.go | 2 +- mdl/executor/cmd_fragments.go | 25 +-- mdl/executor/cmd_microflows_format_action.go | 2 +- .../cmd_microflows_format_restcall_test.go | 2 +- mdl/executor/cmd_misc.go | 2 +- mdl/executor/cmd_navigation.go | 2 +- .../cmd_pages_describe_container_test.go | 6 +- mdl/executor/cmd_pages_describe_output.go | 50 ++--- .../cmd_pages_describe_pageparams_test.go | 10 +- mdl/executor/cmd_pages_describe_pluggable.go | 8 +- .../cmd_pages_open_link_dynamic_test.go | 2 +- mdl/executor/cmd_validationrules.go | 2 +- mdl/executor/menu_signout_test.go | 4 +- mdl/grammar/MDLLexer.g4 | 29 ++- mdl/grammar/domains/MDLDomainModel.g4 | 21 +- mdl/grammar/domains/MDLMicroflow.g4 | 7 +- mdl/grammar/domains/MDLPage.g4 | 45 +++-- mdl/upgrade/r8_spellings_test.go | 54 ++++++ mdl/visitor/r8_spellings_test.go | 131 +++++++++++++ mdl/visitor/visitor_association.go | 1 + mdl/visitor/visitor_deprecations_test.go | 2 +- mdl/visitor/visitor_entity.go | 4 +- mdl/visitor/visitor_helpers.go | 12 +- mdl/visitor/visitor_page_v3.go | 30 +-- mdl/visitor/visitor_r8_spellings.go | 180 ++++++++++++++++++ mdl/visitor/visitor_validationrule.go | 3 +- 31 files changed, 635 insertions(+), 131 deletions(-) create mode 100644 mdl/upgrade/r8_spellings_test.go create mode 100644 mdl/visitor/r8_spellings_test.go create mode 100644 mdl/visitor/visitor_r8_spellings.go diff --git a/cmd/mxcli/lsp_completions_gen.go b/cmd/mxcli/lsp_completions_gen.go index 19568e7d7..0b72282c2 100644 --- a/cmd/mxcli/lsp_completions_gen.go +++ b/cmd/mxcli/lsp_completions_gen.go @@ -15,7 +15,7 @@ var mdlGeneratedKeywords = []protocol.CompletionItem{ {Label: "SORT BY", Kind: protocol.CompletionItemKindKeyword, Detail: "Multi-word keyword"}, {Label: "SEARCH BY", Kind: protocol.CompletionItemKindKeyword, Detail: "Multi-word keyword"}, {Label: "NON-PERSISTENT", Kind: protocol.CompletionItemKindKeyword, Detail: "Multi-word keyword"}, - {Label: "REFERENCE_SET", Kind: protocol.CompletionItemKindKeyword, Detail: "Multi-word keyword"}, + {Label: "REFERENCESET", Kind: protocol.CompletionItemKindKeyword, Detail: "Multi-word keyword"}, {Label: "LIST OF", Kind: protocol.CompletionItemKindKeyword, Detail: "Multi-word keyword"}, {Label: "DELETE AND REFERENCES", Kind: protocol.CompletionItemKindKeyword, Detail: "Multi-word keyword"}, {Label: "DELETE BUT KEEP REFERENCES", Kind: protocol.CompletionItemKindKeyword, Detail: "Multi-word keyword"}, @@ -314,8 +314,8 @@ var mdlGeneratedKeywords = []protocol.CompletionItem{ {Label: "GROUPBOX", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, {Label: "VISIBLE", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, {Label: "SAVECHANGES", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, - {Label: "SAVE_CHANGES", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, - {Label: "CANCEL_CHANGES", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, + {Label: "SAVE CHANGES", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, + {Label: "CANCEL CHANGES", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, {Label: "CLOSE_PAGE", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, {Label: "SHOW_PAGE", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, {Label: "DELETE_ACTION", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, @@ -324,7 +324,7 @@ var mdlGeneratedKeywords = []protocol.CompletionItem{ {Label: "CALL_MICROFLOW", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, {Label: "CALL_NANOFLOW", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, {Label: "OPEN_LINK", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, - {Label: "SIGN_OUT", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, + {Label: "SIGN OUT", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, {Label: "CANCEL", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, {Label: "PRIMARY", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, {Label: "SUCCESS", Kind: protocol.CompletionItemKindKeyword, Detail: "Widget keyword"}, @@ -663,7 +663,7 @@ var mdlGeneratedKeywords = []protocol.CompletionItem{ {Label: "UNLOCK", Kind: protocol.CompletionItemKindKeyword, Detail: "Keyword"}, {Label: "REASON", Kind: protocol.CompletionItemKindKeyword, Detail: "Keyword"}, {Label: "OPEN", Kind: protocol.CompletionItemKindKeyword, Detail: "Keyword"}, - {Label: "COMPLETE_TASK", Kind: protocol.CompletionItemKindKeyword, Detail: "Keyword"}, + {Label: "COMPLETE TASK", Kind: protocol.CompletionItemKindKeyword, Detail: "Keyword"}, // Operator {Label: "MOD", Kind: protocol.CompletionItemKindKeyword, Detail: "Operator"}, diff --git a/mdl/deprecation/deprecation.go b/mdl/deprecation/deprecation.go index 7ae080b8b..3e57400d9 100644 --- a/mdl/deprecation/deprecation.go +++ b/mdl/deprecation/deprecation.go @@ -99,6 +99,30 @@ const ( // name for: a layout grid's rows and columns, a data grid's columns and // control bar, a gallery's template and filter (R12, ako/mxcli#749). UnstoredWidgetName = "MDL-DEPR005" + + // Codes 020–029 are R8's (ako/mxcli#752, PROPOSAL_mdl_beta_syntax_freeze.md + // §3 R8): words, not SCREAMING_SNAKE, and one spelling per keyword. They + // start at 020 rather than 006 because the other phase-3 issues add entries + // in parallel; a gap in the numbering means nothing. + + // PageActionWord is a page action written as one snake-case token — + // `show_page`, `save_changes`, `close_page`, `create_object`, + // `delete_object`, `open_link`, `sign_out`, `complete_task`, + // `cancel_changes` — or a flow call without `call` (`microflow M.F`). + PageActionWord = "MDL-DEPR020" + // ErrorMessageKeyword is the user-facing message of a validation, spelled + // anything but `error message`: `not null error '…'` (also after unique + // and required), a validation rule's `feedback '…'`, and `error_message` / + // `errormessage`. + ErrorMessageKeyword = "MDL-DEPR021" + // DeleteBehaviorClause is an association's `delete_behavior ` + // clause, in any of its spellings; `on delete …` says the same thing. + DeleteBehaviorClause = "MDL-DEPR022" + // ReferenceSetUnderscore is `reference_set` for the `ReferenceSet` type. + ReferenceSetUnderscore = "MDL-DEPR023" + // ReturnsNone is a REST call's `returns none`, the second spelling of + // `returns nothing`. + ReturnsNone = "MDL-DEPR024" ) // entries is the registry. Append only: a code is never reused or renumbered, @@ -170,6 +194,72 @@ var entries = []Entry{ }, } +func init() { + entries = append(entries, r8Entries...) +} + +// r8Entries are R8's spellings (ako/mxcli#752). Kept apart from the list above +// only so the parallel phase-3 changes do not all edit its last lines. +var r8Entries = []Entry{ + { + Code: PageActionWord, + Old: "show_page, save_changes, close_page, microflow M.F, …", + Canonical: "show page, save changes, close page, call microflow M.F, …", + Rewrite: Rewrite{Structural: "page action as words: the underscore becomes a space (`show_page` -> " + + "`show page`, also `save_changes`, `cancel_changes`, `close_page`, `create_object`, `open_link`, " + + "`sign_out`, `complete_task`); `delete_object` -> `delete`; `microflow M.F` / `nanoflow M.F` -> " + + "`call microflow M.F` / `call nanoflow M.F`"}, + RemovedIn: 2, + Note: "The words are the ones a microflow uses for the same activity. A navigation menu's " + + "`sign_out` is the same keyword. Arguments are unchanged.", + Example: "create page M.P (Title: 'P', Layout: Atlas_Core.Atlas_Default) { actionbutton b (Caption: 'Save', Action: sign_out) };", + CanonicalExample: "create page M.P (Title: 'P', Layout: Atlas_Core.Atlas_Default) { actionbutton b (Caption: 'Save', Action: sign out) };", + }, + { + Code: ErrorMessageKeyword, + Old: "error '…' / feedback '…' / error_message '…'", + Canonical: "error message '…'", + Rewrite: Rewrite{Structural: "message keyword as `error message`: `not null error '…'` (and after " + + "`unique` or `required`), a validation rule's `feedback '…'`, and `error_message` / `errormessage`"}, + RemovedIn: 2, + Note: "One keyword for the text a user sees when a rule refuses a change.", + Example: "create entity M.E (Name: String(100) not null error 'Name is required');", + CanonicalExample: "create entity M.E (Name: String(100) not null error message 'Name is required');", + }, + { + Code: DeleteBehaviorClause, + Old: "delete_behavior …", + Canonical: "on delete cascade|restrict|set null", + Rewrite: Rewrite{Structural: "delete behaviour as the SQL referential action: " + + "`delete_behavior cascade` / `delete_and_references` -> `on delete cascade`; " + + "`prevent` / `delete_if_no_references` -> `on delete restrict`; " + + "`delete_but_keep_references` -> `on delete set null`"}, + RemovedIn: 2, + Note: "Also in `alter association … set delete_behavior …`. The three compound behaviour keywords " + + "had three spellings each; the SQL referential actions have one.", + Example: "create association M.Order_Customer from M.Order to M.Customer type Reference delete_behavior prevent;", + CanonicalExample: "create association M.Order_Customer from M.Order to M.Customer type Reference on delete restrict;", + }, + { + Code: ReferenceSetUnderscore, + Old: "type reference_set", + Canonical: "type ReferenceSet", + Rewrite: Rewrite{Structural: "type name as Mendix writes it: `reference_set` -> `ReferenceSet`"}, + RemovedIn: 2, + Example: "create association M.Order_Tag from M.Order to M.Tag type reference_set;", + CanonicalExample: "create association M.Order_Tag from M.Order to M.Tag type ReferenceSet;", + }, + { + Code: ReturnsNone, + Old: "rest call … returns none", + Canonical: "rest call … returns nothing", + Rewrite: Rewrite{Token: "none", Replacement: "nothing"}, + RemovedIn: 2, + Example: "create microflow M.F () begin rest call get 'https://example.com' returns none; end;", + CanonicalExample: "create microflow M.F () begin rest call get 'https://example.com' returns nothing; end;", + }, +} + // All returns every registered entry, in code order. func All() []Entry { out := make([]Entry, len(entries)) diff --git a/mdl/executor/cmd_associations.go b/mdl/executor/cmd_associations.go index 4a245ae8d..cc3c8b16e 100644 --- a/mdl/executor/cmd_associations.go +++ b/mdl/executor/cmd_associations.go @@ -768,7 +768,7 @@ func describeDeleteClause(db *domainmodel.DeleteBehavior) string { } } if db != nil && db.ErrorMessage != "" { - return action + " error_message " + mdlQuote(db.ErrorMessage) + return action + " error message " + mdlQuote(db.ErrorMessage) } return action } diff --git a/mdl/executor/cmd_diff_mdl.go b/mdl/executor/cmd_diff_mdl.go index 77b1f2d54..cec30ca6c 100644 --- a/mdl/executor/cmd_diff_mdl.go +++ b/mdl/executor/cmd_diff_mdl.go @@ -52,13 +52,13 @@ func entityStmtToMDL(ctx *ExecContext, s *ast.CreateEntityStmt) string { if attr.NotNull { constraints += " not null" if attr.NotNullError != "" { - constraints += fmt.Sprintf(" error '%s'", attr.NotNullError) + constraints += fmt.Sprintf(" error message '%s'", attr.NotNullError) } } if attr.Unique { constraints += " unique" if attr.UniqueError != "" { - constraints += fmt.Sprintf(" error '%s'", attr.UniqueError) + constraints += fmt.Sprintf(" error message '%s'", attr.UniqueError) } } if attr.HasDefault { @@ -181,14 +181,14 @@ func associationStmtToMDL(ctx *ExecContext, s *ast.CreateAssociationStmt) string } lines = append(lines, fmt.Sprintf("owner %s", owner)) - deleteBehavior := "DELETE_BUT_KEEP_REFERENCES" + deleteBehavior := "set null" switch s.DeleteBehavior { case ast.DeleteCascade: - deleteBehavior = "DELETE_AND_REFERENCES" + deleteBehavior = "cascade" case ast.DeleteIfNoReferences: - deleteBehavior = "DELETE_IF_NO_REFERENCES" + deleteBehavior = "restrict" } - lines = append(lines, fmt.Sprintf("delete_behavior %s;", deleteBehavior)) + lines = append(lines, fmt.Sprintf("on delete %s;", deleteBehavior)) return strings.Join(lines, "\n") } @@ -252,13 +252,13 @@ func entityToMDL(ctx *ExecContext, moduleName string, entity *domainmodel.Entity if vr.Type == "Required" { constraints.WriteString(" not null") if errMsg := pickTextTranslation(vr.ErrorMessage, lang); errMsg != "" { - constraints.WriteString(fmt.Sprintf(" error '%s'", errMsg)) + constraints.WriteString(fmt.Sprintf(" error message '%s'", errMsg)) } } if vr.Type == "Unique" { constraints.WriteString(" unique") if errMsg := pickTextTranslation(vr.ErrorMessage, lang); errMsg != "" { - constraints.WriteString(fmt.Sprintf(" error '%s'", errMsg)) + constraints.WriteString(fmt.Sprintf(" error message '%s'", errMsg)) } } } @@ -405,16 +405,16 @@ func associationToMDL(ctx *ExecContext, moduleName string, assoc *domainmodel.As } lines = append(lines, fmt.Sprintf("owner %s", owner)) - deleteBehavior := "DELETE_BUT_KEEP_REFERENCES" + deleteBehavior := "set null" if assoc.ChildDeleteBehavior != nil { switch assoc.ChildDeleteBehavior.Type { case domainmodel.DeleteBehaviorTypeDeleteMeAndReferences: - deleteBehavior = "DELETE_AND_REFERENCES" + deleteBehavior = "cascade" case domainmodel.DeleteBehaviorTypeDeleteMeIfNoReferences: - deleteBehavior = "DELETE_IF_NO_REFERENCES" + deleteBehavior = "restrict" } } - lines = append(lines, fmt.Sprintf("delete_behavior %s;", deleteBehavior)) + lines = append(lines, fmt.Sprintf("on delete %s;", deleteBehavior)) return strings.Join(lines, "\n") } diff --git a/mdl/executor/cmd_entities_describe.go b/mdl/executor/cmd_entities_describe.go index 2a0f97788..431223da1 100644 --- a/mdl/executor/cmd_entities_describe.go +++ b/mdl/executor/cmd_entities_describe.go @@ -293,13 +293,13 @@ func describeEntity(ctx *ExecContext, name ast.QualifiedName) error { if vr.Type == "Required" { constraints.WriteString(" not null") if errMsg := pickTextTranslation(vr.ErrorMessage, lang); errMsg != "" { - constraints.WriteString(" error " + mdlQuoted(errMsg)) + constraints.WriteString(" error message " + mdlQuoted(errMsg)) } } if vr.Type == "Unique" { constraints.WriteString(" unique") if errMsg := pickTextTranslation(vr.ErrorMessage, lang); errMsg != "" { - constraints.WriteString(" error " + mdlQuoted(errMsg)) + constraints.WriteString(" error message " + mdlQuoted(errMsg)) } } } diff --git a/mdl/executor/cmd_enumerations_caption_language_test.go b/mdl/executor/cmd_enumerations_caption_language_test.go index 86f7967ee..242f661bd 100644 --- a/mdl/executor/cmd_enumerations_caption_language_test.go +++ b/mdl/executor/cmd_enumerations_caption_language_test.go @@ -185,7 +185,7 @@ func TestDescribeEntity_ValidationMessageFollowsProjectLanguage_Issue1113(t *tes assertNoError(t, describeEntity(ctx, ast.QualifiedName{Module: "Sales", Name: "Order"})) out := buf.String() - if !strings.Contains(out, "not null error 'Referentie is verplicht'") { + if !strings.Contains(out, "not null error message 'Referentie is verplicht'") { t.Errorf("validation feedback lost to the en_US lookup (issue #1113):\n%s", out) } } diff --git a/mdl/executor/cmd_fragments.go b/mdl/executor/cmd_fragments.go index 7f50781fb..88861a055 100644 --- a/mdl/executor/cmd_fragments.go +++ b/mdl/executor/cmd_fragments.go @@ -273,28 +273,31 @@ func formatActionV3(a *ast.ActionV3) string { return "nothing" case "save": if a.ClosePage { - return "save_changes close_page" + return "save changes close page" } - return "save_changes" + return "save changes" case "cancel": if a.ClosePage { - return "cancel_changes close_page" + return "cancel changes close page" } - return "cancel_changes" + return "cancel changes" case "close": - return "close_page" + return "close page" case "delete": - return "delete_object" + if a.ClosePage { + return "delete close page" + } + return "delete" case "showPage": - return "show_page " + a.Target + return "show page " + a.Target case "microflow": - return "microflow " + a.Target + return "call microflow " + a.Target case "nanoflow": - return "nanoflow " + a.Target + return "call nanoflow " + a.Target case "signOut": - return "sign_out" + return "sign out" case "completeTask": - return "complete_task '" + strings.ReplaceAll(a.OutcomeValue, "'", "''") + "'" + return "complete task '" + strings.ReplaceAll(a.OutcomeValue, "'", "''") + "'" default: return a.Type } diff --git a/mdl/executor/cmd_microflows_format_action.go b/mdl/executor/cmd_microflows_format_action.go index b7600d7e3..5ebece93e 100644 --- a/mdl/executor/cmd_microflows_format_action.go +++ b/mdl/executor/cmd_microflows_format_action.go @@ -1412,7 +1412,7 @@ func formatRestCallAction(ctx *ExecContext, a *microflows.RestCallAction) string // reported nothing. sb.WriteString(rh.EntityRef) case *microflows.ResultHandlingNone: - sb.WriteString("Nothing") + sb.WriteString("nothing") default: // Refuse rather than guess. The previous "String" fallback here and // below is what turned an unread result handling into a silent diff --git a/mdl/executor/cmd_microflows_format_restcall_test.go b/mdl/executor/cmd_microflows_format_restcall_test.go index a772f5b25..5e77efb4a 100644 --- a/mdl/executor/cmd_microflows_format_restcall_test.go +++ b/mdl/executor/cmd_microflows_format_restcall_test.go @@ -61,7 +61,7 @@ func TestFormatRestCallAction_POST_CustomBody(t *testing.T) { got := e.formatRestCallAction(action) assertContains(t, got, "rest call post") assertContains(t, got, "body '{\"name\": \"test\"}'") - assertContains(t, got, "returns Nothing") + assertContains(t, got, "returns nothing") } func TestFormatRestCallAction_WithHeaders(t *testing.T) { diff --git a/mdl/executor/cmd_misc.go b/mdl/executor/cmd_misc.go index 5b0cf3e15..ee9a3d46b 100644 --- a/mdl/executor/cmd_misc.go +++ b/mdl/executor/cmd_misc.go @@ -121,7 +121,7 @@ Domain Model - Associations: to Module.Child type Reference|ReferenceSet [owner Default|Both|Parent|Child] - [delete_behavior DELETE_BUT_KEEP_REFERENCES|DELETE_AND_REFERENCES|DELETE_IF_NO_REFERENCES]; + [on delete set null|cascade|restrict [error message '...']]; / drop association Module.Name; diff --git a/mdl/executor/cmd_navigation.go b/mdl/executor/cmd_navigation.go index 02f929b00..632c6f353 100644 --- a/mdl/executor/cmd_navigation.go +++ b/mdl/executor/cmd_navigation.go @@ -435,7 +435,7 @@ func printMenuMDL(w io.Writer, items []*types.NavMenuItem, depth int, reproducer } else if item.Microflow != "" { fmt.Fprintf(w, "%smenu item '%s' microflow %s%s;\n", indent, item.Caption, item.Microflow, icon) } else if item.ActionType == "SignOutAction" { - fmt.Fprintf(w, "%smenu item '%s' sign_out%s;\n", indent, item.Caption, icon) + fmt.Fprintf(w, "%smenu item '%s' sign out%s;\n", indent, item.Caption, icon) } else { fmt.Fprintf(w, "%smenu item '%s'%s;\n", indent, item.Caption, icon) } diff --git a/mdl/executor/cmd_pages_describe_container_test.go b/mdl/executor/cmd_pages_describe_container_test.go index c613c87fb..de37b1d23 100644 --- a/mdl/executor/cmd_pages_describe_container_test.go +++ b/mdl/executor/cmd_pages_describe_container_test.go @@ -243,7 +243,7 @@ func TestParseRawWidget_DivContainerExtractsOnClickAction(t *testing.T) { if len(got) != 1 { t.Fatalf("expected 1 widget, got %d", len(got)) } - if want := "microflow MyFirstModule.MyFirstLogic"; got[0].Action != want { + if want := "call microflow MyFirstModule.MyFirstLogic"; got[0].Action != want { t.Errorf("Action: got %q, want %q", got[0].Action, want) } if len(got[0].Children) != 1 || got[0].Children[0].Name != "t" { @@ -276,7 +276,7 @@ func TestOutputWidgetMDLV3_DivContainerEmitsAction(t *testing.T) { box := rawWidget{ Type: "Forms$DivContainer", Name: "box", - Action: "microflow MyFirstModule.MyFirstLogic", + Action: "call microflow MyFirstModule.MyFirstLogic", Children: []rawWidget{ {Type: "Forms$DynamicText", Name: "t"}, }, @@ -286,7 +286,7 @@ func TestOutputWidgetMDLV3_DivContainerEmitsAction(t *testing.T) { out := buf.String() for _, want := range []string{ "container box", - "Action: microflow MyFirstModule.MyFirstLogic", + "Action: call microflow MyFirstModule.MyFirstLogic", } { if !strings.Contains(out, want) { t.Errorf("output missing %q\nfull output:\n%s", want, out) diff --git a/mdl/executor/cmd_pages_describe_output.go b/mdl/executor/cmd_pages_describe_output.go index 7d3da4014..60c14aacb 100644 --- a/mdl/executor/cmd_pages_describe_output.go +++ b/mdl/executor/cmd_pages_describe_output.go @@ -1353,7 +1353,7 @@ func actionMapForKey(w map[string]any, key string) map[string]any { } // renderClientActionMDL renders a client-action map (a Forms$*ClientAction) back -// to its MDL form (microflow/nanoflow/show_page/save_changes/…). Returns "" for a +// to its MDL form (call microflow/call nanoflow/show page/save changes/…). Returns "" for a // nil action or a NoClientAction. func renderClientActionMDL(ctx *ExecContext, action map[string]any) string { if action == nil { @@ -1362,27 +1362,27 @@ func renderClientActionMDL(ctx *ExecContext, action map[string]any) string { typeName, _ := action["$Type"].(string) switch typeName { case "Forms$SaveChangesClientAction", "Pages$SaveChangesClientAction": - result := "save_changes" + result := "save changes" if closePage, ok := action["ClosePage"].(bool); ok && closePage { - result += " close_page" + result += " close page" } return result case "Forms$CancelChangesClientAction", "Pages$CancelChangesClientAction": - result := "cancel_changes" + result := "cancel changes" if closePage, ok := action["ClosePage"].(bool); ok && closePage { - result += " close_page" + result += " close page" } return result case "Forms$ClosePageClientAction", "Pages$ClosePageClientAction": - return "close_page" + return "close page" case "Forms$DeleteClientAction", "Pages$DeleteClientAction": - result := "delete_object" + result := "delete" if closePage, ok := action["ClosePage"].(bool); ok && closePage { - result += " close_page" + result += " close page" } return result case "Forms$CreateObjectClientAction", "Pages$CreateObjectClientAction": - result := "create_object" + result := "create object" // Extract entity reference if entityRef, ok := action["EntityRef"].(map[string]any); ok { if entityName, ok := entityRef["Entity"].(string); ok && entityName != "" { @@ -1393,7 +1393,7 @@ func renderClientActionMDL(ctx *ExecContext, action map[string]any) string { if pageSettings, ok := action["PageSettings"].(map[string]any); ok { // The page is stored in "Form" field as a qualified name string (BY_NAME_REFERENCE) if pageName, ok := pageSettings["Form"].(string); ok && pageName != "" { - pageAction := "show_page " + pageName + pageAction := "show page " + pageName // Extract page parameters params := extractPageParameters(ctx, pageSettings) if params != "" { @@ -1408,7 +1408,7 @@ func renderClientActionMDL(ctx *ExecContext, action map[string]any) string { // or PageSettings.Form, or Page field (binary ID for legacy) if formSettings, ok := action["FormSettings"].(map[string]any); ok { if pageName, ok := formSettings["Form"].(string); ok && pageName != "" { - result := "show_page " + pageName + result := "show page " + pageName params := pageActionParameters(ctx, formSettings, pageName) if params != "" { result += "(" + params + ")" @@ -1418,7 +1418,7 @@ func renderClientActionMDL(ctx *ExecContext, action map[string]any) string { } if pageSettings, ok := action["PageSettings"].(map[string]any); ok { if pageName, ok := pageSettings["Form"].(string); ok && pageName != "" { - result := "show_page " + pageName + result := "show page " + pageName params := pageActionParameters(ctx, pageSettings, pageName) if params != "" { result += "(" + params + ")" @@ -1430,15 +1430,15 @@ func renderClientActionMDL(ctx *ExecContext, action map[string]any) string { if pageID := extractBinaryID(action["Page"]); pageID != "" { pageName := getPageQualifiedName(ctx, model.ID(pageID)) if pageName != "" { - return "show_page " + pageName + return "show page " + pageName } } - return "show_page" + return "show page" case "Forms$MicroflowAction", "Pages$MicroflowClientAction": // Extract microflow reference from MicroflowSettings if settings, ok := action["MicroflowSettings"].(map[string]any); ok { if mfName, ok := settings["Microflow"].(string); ok && mfName != "" { - result := "microflow " + mfName + result := "call microflow " + mfName // Extract parameter mappings params := extractMicroflowParameters(ctx, settings) if params != "" { @@ -1447,10 +1447,10 @@ func renderClientActionMDL(ctx *ExecContext, action map[string]any) string { return result } } - return "microflow" + return "call microflow" case "Forms$CallNanoflowClientAction", "Pages$CallNanoflowClientAction": if nfName, ok := action["Nanoflow"].(string); ok && nfName != "" { - result := "nanoflow " + nfName + result := "call nanoflow " + nfName // Extract parameter mappings (directly in the action) params := extractNanoflowParameters(ctx, action) if params != "" { @@ -1458,21 +1458,21 @@ func renderClientActionMDL(ctx *ExecContext, action map[string]any) string { } return result } - return "nanoflow" + return "call nanoflow" case "Forms$SetTaskOutcomeClientAction", "Pages$SetTaskOutcomeClientAction": outcomeValue, _ := action["OutcomeValue"].(string) - return "complete_task '" + strings.ReplaceAll(outcomeValue, "'", "''") + "'" + return "complete task '" + strings.ReplaceAll(outcomeValue, "'", "''") + "'" case "Forms$SignOutClientAction", "Pages$SignOutClientAction": - return "sign_out" + return "sign out" case "Forms$OpenLinkClientAction", "Pages$OpenLinkClientAction": // The address is a nested Forms$StaticOrDynamicString: a literal, or — // DYNAMIC, 6 of the 31 Studio Pro references — an attribute read at - // runtime, spelled `open_link $currentObject/Attr`. It used to render + // runtime, spelled `open link $currentObject/Attr`. It used to render // as an inline `--` note, which left `Action:` without a value and made // the describe output unparseable. addr := actionMapForKey(action, "Address") if addr == nil { - return "open_link ''" + return "open link ''" } if isDynamic, _ := addr["IsDynamic"].(bool); isDynamic { attr := "" @@ -1482,16 +1482,16 @@ func renderClientActionMDL(ctx *ExecContext, action map[string]any) string { overAssociation = actionMapForKey(ref, "EntityRef") != nil } if attr != "" && !overAssociation { - return "open_link $currentObject/" + shortAttributeName(attr) + return "open link $currentObject/" + shortAttributeName(attr) } // No MDL spelling: a note, which actionProp puts on its own line. // CREATE OR REPLACE PAGE rebuilds the page, so say plainly that // re-running drops the action rather than implying it survives. - return "-- NOT re-executable: open_link with a dynamic address over an association (" + + return "-- NOT re-executable: open link with a dynamic address over an association (" + attr + ") — re-running this script would drop the button's action" } value, _ := addr["Value"].(string) - return "open_link '" + strings.ReplaceAll(value, "'", "''") + "'" + return "open link '" + strings.ReplaceAll(value, "'", "''") + "'" case "Forms$NoClientAction", "Pages$NoClientAction": return "" default: diff --git a/mdl/executor/cmd_pages_describe_pageparams_test.go b/mdl/executor/cmd_pages_describe_pageparams_test.go index e9c38459d..bf6d44c2b 100644 --- a/mdl/executor/cmd_pages_describe_pageparams_test.go +++ b/mdl/executor/cmd_pages_describe_pageparams_test.go @@ -66,7 +66,7 @@ func TestRenderShowPageAction_RecoversTheImplicitParameter(t *testing.T) { ctx := pageParamFixture(t, "Race") got := renderClientActionMDL(ctx, showPageAction("Formula1Frontend.Race_Weekend", nil)) - want := "show_page Formula1Frontend.Race_Weekend(Race: $currentObject)" + want := "show page Formula1Frontend.Race_Weekend(Race: $currentObject)" if got != want { t.Errorf("DESCRIBE lost the page parameter:\n got: %s\nwant: %s", got, want) } @@ -77,7 +77,7 @@ func TestRenderShowPageAction_RecoversEveryParameter(t *testing.T) { ctx := pageParamFixture(t, "Race", "Season") got := renderClientActionMDL(ctx, showPageAction("Formula1Frontend.Race_Weekend", nil)) - want := "show_page Formula1Frontend.Race_Weekend(Race: $currentObject, Season: $currentObject)" + want := "show page Formula1Frontend.Race_Weekend(Race: $currentObject, Season: $currentObject)" if got != want { t.Errorf("got: %s\nwant: %s", got, want) } @@ -88,7 +88,7 @@ func TestRenderShowPageAction_NoParametersStaysBare(t *testing.T) { ctx := pageParamFixture(t) got := renderClientActionMDL(ctx, showPageAction("Formula1Frontend.Race_Weekend", nil)) - if want := "show_page Formula1Frontend.Race_Weekend"; got != want { + if want := "show page Formula1Frontend.Race_Weekend"; got != want { t.Errorf("got: %s\nwant: %s", got, want) } } @@ -100,7 +100,7 @@ func TestRenderShowPageAction_UnknownPageInventsNothing(t *testing.T) { ctx := pageParamFixture(t, "Race") got := renderClientActionMDL(ctx, showPageAction("OtherModule.Gone", nil)) - if want := "show_page OtherModule.Gone"; got != want { + if want := "show page OtherModule.Gone"; got != want { t.Errorf("got: %s\nwant: %s", got, want) } } @@ -119,7 +119,7 @@ func TestRenderShowPageAction_ExplicitMappingWins(t *testing.T) { }, } got := renderClientActionMDL(ctx, showPageAction("Formula1Frontend.Race_Weekend", mappings)) - want := "show_page Formula1Frontend.Race_Weekend(Race: $SelectedRace)" + want := "show page Formula1Frontend.Race_Weekend(Race: $SelectedRace)" if got != want { t.Errorf("an explicit mapping was overwritten:\n got: %s\nwant: %s", got, want) } diff --git a/mdl/executor/cmd_pages_describe_pluggable.go b/mdl/executor/cmd_pages_describe_pluggable.go index 78626d435..ec5e5898b 100644 --- a/mdl/executor/cmd_pages_describe_pluggable.go +++ b/mdl/executor/cmd_pages_describe_pluggable.go @@ -1360,7 +1360,7 @@ func customWidgetActionForSource(ctx *ExecContext, w map[string]any, source stri } // extractCustomWidgetPropertyAction extracts an action description from a CustomWidget property. -// Returns a formatted string like "CALL_MICROFLOW Module.Flow" or "SHOW_PAGE Module.Page". +// Returns a formatted string like "call microflow Module.Flow" or "show page Module.Page". func extractCustomWidgetPropertyAction(ctx *ExecContext, w map[string]any, propertyKey string) string { obj, ok := w["Object"].(map[string]any) if !ok { @@ -1393,19 +1393,19 @@ func extractCustomWidgetPropertyAction(ctx *ExecContext, w map[string]any, prope case "Forms$MicroflowAction", "Pages$MicroflowClientAction": if settings, ok := action["MicroflowSettings"].(map[string]any); ok { if mf := extractString(settings["Microflow"]); mf != "" { - return "microflow " + mf + return "call microflow " + mf } } case "Forms$CallNanoflowClientAction", "Pages$CallNanoflowClientAction": if settings, ok := action["NanoflowSettings"].(map[string]any); ok { if nf := extractString(settings["Nanoflow"]); nf != "" { - return "nanoflow " + nf + return "call nanoflow " + nf } } case "Forms$FormAction", "Pages$FormAction": if settings, ok := action["PageSettings"].(map[string]any); ok { if page := extractString(settings["Page"]); page != "" { - return "show_page " + page + return "show page " + page } } case "Forms$NoAction", "Pages$NoAction": diff --git a/mdl/executor/cmd_pages_open_link_dynamic_test.go b/mdl/executor/cmd_pages_open_link_dynamic_test.go index 95a887b30..167f28fb7 100644 --- a/mdl/executor/cmd_pages_open_link_dynamic_test.go +++ b/mdl/executor/cmd_pages_open_link_dynamic_test.go @@ -73,7 +73,7 @@ func TestRenderClientActionMDL_OpenLinkDynamicAddress(t *testing.T) { }, "LinkType": "Web", } - if got, want := renderClientActionMDL(ctx, action), "open_link $currentObject/URL"; got != want { + if got, want := renderClientActionMDL(ctx, action), "open link $currentObject/URL"; got != want { t.Errorf("renderClientActionMDL = %q, want %q", got, want) } diff --git a/mdl/executor/cmd_validationrules.go b/mdl/executor/cmd_validationrules.go index d41573b39..7a7f1fd31 100644 --- a/mdl/executor/cmd_validationrules.go +++ b/mdl/executor/cmd_validationrules.go @@ -165,7 +165,7 @@ func outputEntityValidationRules(ctx *ExecContext, entity *domainmodel.Entity, m } feedback := pickTextTranslation(vr.ErrorMessage, lang) - fmt.Fprintf(ctx.Output, "\ncreate or modify validation rule for %s\n %s\n feedback '%s';\n", + fmt.Fprintf(ctx.Output, "\ncreate or modify validation rule for %s\n %s\n error message '%s';\n", target, constraint, escapeMDLString(feedback)) } } diff --git a/mdl/executor/menu_signout_test.go b/mdl/executor/menu_signout_test.go index 06fc80c94..24bfa1cf7 100644 --- a/mdl/executor/menu_signout_test.go +++ b/mdl/executor/menu_signout_test.go @@ -103,11 +103,11 @@ func TestPrintMenuMDL_RendersSignOut(t *testing.T) { }, 0, "CREATE NAVIGATION") out := b.String() - if !strings.Contains(out, "menu item 'Sign out' sign_out;") { + if !strings.Contains(out, "menu item 'Sign out' sign out;") { t.Errorf("describe output does not round-trip the sign-out item:\n%s", out) } // CONTROL: a plain item must not gain an action. - if strings.Contains(out, "'Plain' sign_out") { + if strings.Contains(out, "'Plain' sign out") { t.Errorf("a plain item was rendered as sign-out:\n%s", out) } } diff --git a/mdl/grammar/MDLLexer.g4 b/mdl/grammar/MDLLexer.g4 index e28cccad7..26b7ac6c3 100644 --- a/mdl/grammar/MDLLexer.g4 +++ b/mdl/grammar/MDLLexer.g4 @@ -98,7 +98,10 @@ SORT_BY: S O R T WS+ B Y; // deliberately: same position, same comma list, so one example teaches both. SEARCH_BY: S E A R C H WS+ B Y; NON_PERSISTENT: N O N '-' P E R S I S T E N T; -REFERENCE_SET: R E F E R E N C E '_'? S E T; +// `ReferenceSet`, the Mendix type name describe writes; `reference_set` is the +// old second spelling (R8, ako/mxcli#752). +REFERENCE_SET: R E F E R E N C E S E T + | R E F E R E N C E '_' S E T /* @alias MDL-DEPR023 */; LIST_OF: L I S T WS+ O F; // Delete behavior compound keywords @@ -188,9 +191,12 @@ DELETE_BEHAVIOR: D E L E T E '_'? B E H A V I O R; // deleted"). SQL's RESTRICT has no custom message, so this clause is a Mendix // extension rather than something borrowed. RESTRICT: R E S T R I C T; +// +// `error message` is the one spelling (R8, ako/mxcli#752); the other two are +// aliases, and so are `error`, `feedback` where a rule takes a message. ERROR_MESSAGE: E R R O R WS+ M E S S A G E - | E R R O R '_' M E S S A G E - | E R R O R M E S S A G E; + | E R R O R '_' M E S S A G E /* @alias MDL-DEPR021 */ + | E R R O R M E S S A G E /* @alias MDL-DEPR021 */; CASCADE: C A S C A D E; PREVENT: P R E V E N T; @@ -506,8 +512,15 @@ TABPAGE: T A B P A G E; GROUPBOX: G R O U P B O X; VISIBLE: V I S I B L E; SAVECHANGES: S A V E C H A N G E S; -SAVE_CHANGES: S A V E '_' C H A N G E S; -CANCEL_CHANGES: C A N C E L '_' C H A N G E S; +// Page actions are the words a microflow uses (R8, ako/mxcli#752). The +// two-word actions whose words are not tokens of their own are one token that +// admits both spellings; the snake-case one is the deprecated alias, told +// apart by its underscore. The rest (show page, close page, create object, +// open link, call microflow) are token sequences in actionExprV3. +SAVE_CHANGES: S A V E WS+ C H A N G E S + | S A V E '_' C H A N G E S /* @alias MDL-DEPR020 */; +CANCEL_CHANGES: C A N C E L WS+ C H A N G E S + | C A N C E L '_' C H A N G E S /* @alias MDL-DEPR020 */; CLOSE_PAGE: C L O S E '_' P A G E; SHOW_PAGE: S H O W '_' P A G E; DELETE_ACTION: D E L E T E '_' A C T I O N; @@ -516,7 +529,8 @@ CREATE_OBJECT: C R E A T E '_' O B J E C T; CALL_MICROFLOW: C A L L '_' M I C R O F L O W; CALL_NANOFLOW: C A L L '_' N A N O F L O W; OPEN_LINK: O P E N '_' L I N K; -SIGN_OUT: S I G N '_' O U T; +SIGN_OUT: S I G N WS+ O U T + | S I G N '_' O U T /* @alias MDL-DEPR020 */; CANCEL: C A N C E L; // Button styles @@ -908,7 +922,8 @@ LOCK: L O C K; UNLOCK: U N L O C K; REASON: R E A S O N; OPEN: O P E N; -COMPLETE_TASK: C O M P L E T E '_' T A S K; +COMPLETE_TASK: C O M P L E T E WS+ T A S K + | C O M P L E T E '_' T A S K /* @alias MDL-DEPR020 */; // ============================================================================= // COMPARISON OPERATORS (multi-char before single-char) diff --git a/mdl/grammar/domains/MDLDomainModel.g4 b/mdl/grammar/domains/MDLDomainModel.g4 index 501bae460..acc7f2f78 100644 --- a/mdl/grammar/domains/MDLDomainModel.g4 +++ b/mdl/grammar/domains/MDLDomainModel.g4 @@ -79,15 +79,22 @@ attributeName ; attributeConstraint - : NOT_NULL (ERROR STRING_LITERAL)? - | NOT NULL (ERROR STRING_LITERAL)? + : NOT_NULL (constraintErrorKeyword STRING_LITERAL)? + | NOT NULL (constraintErrorKeyword STRING_LITERAL)? | NULLABLE // explicit: clear NOT NULL (MODIFY ATTRIBUTE, Bug 12a) - | UNIQUE (ERROR STRING_LITERAL)? + | UNIQUE (constraintErrorKeyword STRING_LITERAL)? | DEFAULT (literal | expression) - | REQUIRED (ERROR STRING_LITERAL)? + | REQUIRED (constraintErrorKeyword STRING_LITERAL)? | CALCULATED (BY? qualifiedName)? ; +// `error message '…'` is the one spelling of a rule's user-facing message +// (R8, ako/mxcli#752); a bare `error` is the old one. +constraintErrorKeyword + : ERROR_MESSAGE + | ERROR /* @alias MDL-DEPR021 */ + ; + /** * Specifies the data type for an attribute. */ @@ -185,7 +192,7 @@ associationOption : TYPE COLON? (REFERENCE | REFERENCE_SET) | OWNER COLON? (DEFAULT | BOTH) | STORAGE COLON? (COLUMN | TABLE) - | DELETE_BEHAVIOR deleteBehavior errorMessageClause? + | DELETE_BEHAVIOR /* @alias MDL-DEPR022 */ deleteBehavior errorMessageClause? | onDeleteClause | COMMENT STRING_LITERAL ; @@ -297,7 +304,7 @@ ifExists ; alterAssociationAction - : SET DELETE_BEHAVIOR deleteBehavior errorMessageClause? + : SET DELETE_BEHAVIOR /* @alias MDL-DEPR022 */ deleteBehavior errorMessageClause? | SET onDeleteClause | SET OWNER (DEFAULT | BOTH) | SET STORAGE (COLUMN | TABLE) @@ -908,7 +915,7 @@ exportMappingChild createValidationRuleStatement : VALIDATION RULE FOR qualifiedName validationRuleConstraint - FEEDBACK STRING_LITERAL + (ERROR_MESSAGE | FEEDBACK /* @alias MDL-DEPR021 */) STRING_LITERAL ; validationRuleConstraint diff --git a/mdl/grammar/domains/MDLMicroflow.g4 b/mdl/grammar/domains/MDLMicroflow.g4 index ff5a0c63a..f347ce1ac 100644 --- a/mdl/grammar/domains/MDLMicroflow.g4 +++ b/mdl/grammar/domains/MDLMicroflow.g4 @@ -209,7 +209,8 @@ microflowConcurrencyClause // ERROR_MESSAGE is one token, not ERROR + MESSAGE — it already exists for an // association's delete behaviour, and re-splitting it here would make the lexer -// ambiguous. It accepts `error message`, `error_message` and `errormessage`. +// ambiguous. `error message` is its spelling; `error_message` and +// `errormessage` still lex as deprecated aliases (MDL-DEPR021). microflowConcurrencyError : ERROR_MESSAGE STRING_LITERAL | ERROR MICROFLOW qualifiedName @@ -808,8 +809,8 @@ restCallReturnsClause | RETURNS RESPONSE // Return HttpResponse object | RETURNS MAPPING qualifiedName AS LIST_OF qualifiedName // Import mapping → list result | RETURNS MAPPING qualifiedName AS qualifiedName // Import mapping → single object - | RETURNS NONE // Ignore response - | RETURNS NOTHING // Ignore response (alias) + | RETURNS NOTHING // Ignore response + | RETURNS NONE /* @alias MDL-DEPR024 */ // Ignore response (old second spelling) | RETURNS qualifiedName // Store in file document (a System.FileDocument specialization) ; diff --git a/mdl/grammar/domains/MDLPage.g4 b/mdl/grammar/domains/MDLPage.g4 index 3052d578b..f52aea101 100644 --- a/mdl/grammar/domains/MDLPage.g4 +++ b/mdl/grammar/domains/MDLPage.g4 @@ -652,19 +652,38 @@ associationPathV3 actionExprV3 : VARIABLE // $handler — a fragment action parameter (see fragmentParam) | NOTHING // NOTHING — an explicitly inert widget (Forms$NoAction) - | SAVE_CHANGES (CLOSE_PAGE)? // SAVE_CHANGES or SAVE_CHANGES CLOSE_PAGE - | CANCEL_CHANGES (CLOSE_PAGE)? // CANCEL_CHANGES - | CLOSE_PAGE // CLOSE_PAGE - | DELETE_OBJECT // DELETE_OBJECT - | DELETE (CLOSE_PAGE)? // DELETE (legacy) - | CREATE_OBJECT qualifiedName (THEN actionExprV3)? // CREATE_OBJECT Entity THEN SHOW_PAGE ... - | SHOW_PAGE qualifiedName microflowArgsV3? // SHOW_PAGE Module.Page (Param: val) - | MICROFLOW qualifiedName microflowArgsV3? // MICROFLOW Module.Flow - | NANOFLOW qualifiedName microflowArgsV3? // NANOFLOW Module.Flow - | OPEN_LINK STRING_LITERAL // OPEN_LINK 'https://...' - | OPEN_LINK VARIABLE SLASH attributePathV3 // OPEN_LINK $currentObject/URL (address read from an attribute) - | SIGN_OUT // SIGN_OUT - | COMPLETE_TASK STRING_LITERAL // COMPLETE_TASK 'OutcomeName' + | SAVE_CHANGES closePageV3? // save changes [close page] + | CANCEL_CHANGES closePageV3? // cancel changes [close page] + | closePageV3 // close page + | DELETE closePageV3? // delete [close page] + | DELETE_OBJECT /* @alias MDL-DEPR020 */ closePageV3? + | CREATE OBJECT qualifiedName (THEN actionExprV3)? // create object Entity then show page ... + | CREATE_OBJECT /* @alias MDL-DEPR020 */ qualifiedName (THEN actionExprV3)? + | SHOW PAGE qualifiedName microflowArgsV3? // show page Module.Page (Param: val) + | SHOW_PAGE /* @alias MDL-DEPR020 */ qualifiedName microflowArgsV3? + | CALL MICROFLOW qualifiedName microflowArgsV3? // call microflow Module.Flow + | MICROFLOW /* @alias MDL-DEPR020 */ qualifiedName microflowArgsV3? + | CALL NANOFLOW qualifiedName microflowArgsV3? // call nanoflow Module.Flow + | NANOFLOW /* @alias MDL-DEPR020 */ qualifiedName microflowArgsV3? + | openLinkV3 STRING_LITERAL // open link 'https://...' + | openLinkV3 VARIABLE SLASH attributePathV3 // open link $currentObject/URL (address read from an attribute) + | SIGN_OUT // sign out + | COMPLETE_TASK STRING_LITERAL // complete task 'OutcomeName' + ; + +// The page actions are the words a microflow uses (R8, ako/mxcli#752): +// `show page`, `close page`, `create object`, `call microflow`, `open link`. +// The snake-case tokens are the deprecated second spellings; `save changes`, +// `cancel changes`, `sign out` and `complete task` are single lexer tokens that +// admit both (MDLLexer.g4). +closePageV3 + : CLOSE PAGE + | CLOSE_PAGE /* @alias MDL-DEPR020 */ + ; + +openLinkV3 + : OPEN LINK + | OPEN_LINK /* @alias MDL-DEPR020 */ ; // V3 Microflow arguments: (Param: value, ...) diff --git a/mdl/upgrade/r8_spellings_test.go b/mdl/upgrade/r8_spellings_test.go new file mode 100644 index 000000000..a83050954 --- /dev/null +++ b/mdl/upgrade/r8_spellings_test.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 + +package upgrade + +import ( + "testing" +) + +// R8 (ako/mxcli#752): every old spelling upgrades to exactly its canonical +// form, keeping the letter case it was written in. The visitor test +// (mdl/visitor/r8_spellings_test.go) proves each pair builds the same +// statements; this one proves the rewrite produces that pair. +func TestUpgrade_R8Spellings(t *testing.T) { + page := func(action string) string { + return "create page M.P (Title: 'P', Layout: Atlas_Core.Atlas_Default) {\n" + + " actionbutton b (Caption: 'Go', Action: " + action + ")\n};\n" + } + cases := []struct{ old, want string }{ + {page("save_changes close_page"), page("save changes close page")}, + {page("SAVE_CHANGES CLOSE_PAGE"), page("SAVE CHANGES CLOSE PAGE")}, + {page("cancel_changes"), page("cancel changes")}, + {page("delete_object close_page"), page("delete close page")}, + {page("DELETE_OBJECT"), page("DELETE")}, + {page("create_object M.E then show_page M.Edit(Item: $currentObject)"), page("create object M.E then show page M.Edit(Item: $currentObject)")}, + {page("microflow M.ACT(X: 1)"), page("call microflow M.ACT(X: 1)")}, + {page("NANOFLOW M.NF"), page("CALL NANOFLOW M.NF")}, + {page("open_link $currentObject/Url"), page("open link $currentObject/Url")}, + {page("sign_out"), page("sign out")}, + {page("complete_task 'Approve'"), page("complete task 'Approve'")}, + {"create navigation Responsive home page M.Home menu (menu item 'Out' sign_out;);\n", + "create navigation Responsive home page M.Home menu (menu item 'Out' sign out;);\n"}, + {"create entity M.E (\n Name: String(100) not null error 'Required',\n Code: String(9) UNIQUE ERROR 'Taken'\n);\n", + "create entity M.E (\n Name: String(100) not null error message 'Required',\n Code: String(9) UNIQUE ERROR MESSAGE 'Taken'\n);\n"}, + {"create validation rule for M.E.Email regex M.Pattern\n feedback 'Bad';\n", + "create validation rule for M.E.Email regex M.Pattern\n error message 'Bad';\n"}, + {"create association M.A_B from M.A to M.B type reference_set on delete restrict error_message 'In use';\n", + "create association M.A_B from M.A to M.B type ReferenceSet on delete restrict error message 'In use';\n"}, + {"create association M.A_B from M.A to M.B DELETE_BEHAVIOR DELETE_AND_REFERENCES;\n", + "create association M.A_B from M.A to M.B ON DELETE CASCADE;\n"}, + {"alter association M.A_B set delete_behavior delete but keep references error_message 'x';\n", + "alter association M.A_B set on delete set null error message 'x';\n"}, + {"create microflow M.F () begin rest call get 'https://x.org' returns none; end;\n", + "create microflow M.F () begin rest call get 'https://x.org' returns nothing; end;\n"}, + } + for _, c := range cases { + res := mustUpgrade(t, c.old, Options{}) + if res.Source != c.want { + t.Errorf("upgrade of\n%s got:\n%s want:\n%s", c.old, res.Source, c.want) + } + if again := mustUpgrade(t, res.Source, Options{}); again.Changed() { + t.Errorf("upgrade is not idempotent on\n%s", res.Source) + } + } +} diff --git a/mdl/visitor/r8_spellings_test.go b/mdl/visitor/r8_spellings_test.go new file mode 100644 index 000000000..5c63dfc72 --- /dev/null +++ b/mdl/visitor/r8_spellings_test.go @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 + +package visitor + +import ( + "reflect" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/deprecation" +) + +// r8Pair is one old spelling and its canonical form (R8, ako/mxcli#752). Both +// must parse, build the same statements, and only the old one may record code. +type r8Pair struct { + name, old, canon, code string +} + +func pageWith(action string) string { + return "create page M.P (Title: 'P', Layout: Atlas_Core.Atlas_Default) { actionbutton b (Caption: 'Go', Action: " + action + ") };" +} + +var r8Pairs = []r8Pair{ + {"save", pageWith("save_changes"), pageWith("save changes"), deprecation.PageActionWord}, + {"save close", pageWith("save_changes close_page"), pageWith("save changes close page"), deprecation.PageActionWord}, + {"save close upper", pageWith("SAVE_CHANGES CLOSE_PAGE"), pageWith("SAVE CHANGES CLOSE PAGE"), deprecation.PageActionWord}, + {"cancel close", pageWith("cancel_changes close_page"), pageWith("cancel changes close page"), deprecation.PageActionWord}, + {"close", pageWith("close_page"), pageWith("close page"), deprecation.PageActionWord}, + {"delete", pageWith("delete_object"), pageWith("delete"), deprecation.PageActionWord}, + {"delete close", pageWith("delete_object close_page"), pageWith("delete close page"), deprecation.PageActionWord}, + {"create then show", pageWith("create_object M.E then show_page M.Edit"), pageWith("create object M.E then show page M.Edit"), deprecation.PageActionWord}, + {"show page args", pageWith("show_page M.Edit(Item: $currentObject)"), pageWith("show page M.Edit(Item: $currentObject)"), deprecation.PageActionWord}, + {"microflow", pageWith("microflow M.ACT(X: 1)"), pageWith("call microflow M.ACT(X: 1)"), deprecation.PageActionWord}, + {"nanoflow", pageWith("nanoflow M.NF"), pageWith("call nanoflow M.NF"), deprecation.PageActionWord}, + {"open link", pageWith("open_link 'https://x.org'"), pageWith("open link 'https://x.org'"), deprecation.PageActionWord}, + {"open link attr", pageWith("open_link $currentObject/Url"), pageWith("open link $currentObject/Url"), deprecation.PageActionWord}, + {"sign out", pageWith("sign_out"), pageWith("sign out"), deprecation.PageActionWord}, + {"complete task", pageWith("complete_task 'Approve'"), pageWith("complete task 'Approve'"), deprecation.PageActionWord}, + {"menu sign out", "create navigation Responsive home page M.Home menu (menu item 'Out' sign_out;);", + "create navigation Responsive home page M.Home menu (menu item 'Out' sign out;);", deprecation.PageActionWord}, + {"alter page set action", "alter page M.P { set Action = show_page M.Q on b };", + "alter page M.P { set Action = show page M.Q on b };", deprecation.PageActionWord}, + + {"not null error", "create entity M.E (Name: String(100) not null error 'Required');", + "create entity M.E (Name: String(100) not null error message 'Required');", deprecation.ErrorMessageKeyword}, + {"unique error", "create entity M.E (Code: String(10) unique error 'Taken');", + "create entity M.E (Code: String(10) unique error message 'Taken');", deprecation.ErrorMessageKeyword}, + {"validation feedback", "create validation rule for M.E.Email regex M.Pattern feedback 'Bad';", + "create validation rule for M.E.Email regex M.Pattern error message 'Bad';", deprecation.ErrorMessageKeyword}, + {"association error_message", "create association M.A_B from M.A to M.B on delete restrict error_message 'In use';", + "create association M.A_B from M.A to M.B on delete restrict error message 'In use';", deprecation.ErrorMessageKeyword}, + {"association errormessage", "create association M.A_B from M.A to M.B on delete restrict errormessage 'In use';", + "create association M.A_B from M.A to M.B on delete restrict error message 'In use';", deprecation.ErrorMessageKeyword}, + + {"delete_behavior cascade", "create association M.A_B from M.A to M.B delete_behavior cascade;", + "create association M.A_B from M.A to M.B on delete cascade;", deprecation.DeleteBehaviorClause}, + {"delete_behavior delete_and_references", "create association M.A_B from M.A to M.B delete_behavior delete_and_references;", + "create association M.A_B from M.A to M.B on delete cascade;", deprecation.DeleteBehaviorClause}, + {"deletebehavior deleteandreferences", "create association M.A_B from M.A to M.B DELETEBEHAVIOR DELETEANDREFERENCES;", + "create association M.A_B from M.A to M.B ON DELETE CASCADE;", deprecation.DeleteBehaviorClause}, + {"delete_behavior delete but keep", "create association M.A_B from M.A to M.B delete_behavior delete but keep references;", + "create association M.A_B from M.A to M.B on delete set null;", deprecation.DeleteBehaviorClause}, + {"delete_behavior if no refs", "create association M.A_B from M.A to M.B delete_behavior delete_if_no_references;", + "create association M.A_B from M.A to M.B on delete restrict;", deprecation.DeleteBehaviorClause}, + {"alter set delete_behavior", "alter association M.A_B set delete_behavior prevent;", + "alter association M.A_B set on delete restrict;", deprecation.DeleteBehaviorClause}, + + {"reference_set", "create association M.A_B from M.A to M.B type reference_set;", + "create association M.A_B from M.A to M.B type ReferenceSet;", deprecation.ReferenceSetUnderscore}, + + {"returns none", "create microflow M.F () begin rest call get 'https://x.org' returns none; end;", + "create microflow M.F () begin rest call get 'https://x.org' returns nothing; end;", deprecation.ReturnsNone}, +} + +func TestR8OldSpellingsAreAliases(t *testing.T) { + for _, p := range r8Pairs { + t.Run(p.name, func(t *testing.T) { + old := mustBuild(t, p.old) + canon := mustBuild(t, p.canon) + if got := deprecationCodes(canon); len(got) != 0 { + t.Errorf("canonical %q recorded %v, want none", p.canon, got) + } + got := deprecationCodes(old) + if len(got) == 0 { + t.Fatalf("old %q recorded nothing, want %s", p.old, p.code) + } + for _, c := range got { + if c != p.code { + t.Errorf("old %q recorded %v, want only %s", p.old, got, p.code) + } + } + for _, d := range old.Deprecations { + if d.Code == p.code && d.Fix == nil && d.Code != deprecation.ReturnsNone { + t.Errorf("old %q: %s recorded without a rewrite (%s)", p.old, d.Code, d.NoFix) + } + } + if !reflect.DeepEqual(old.Statements, canon.Statements) { + t.Errorf("old and canonical build different statements:\n old: %#v\n canon: %#v", old.Statements, canon.Statements) + } + }) + } +} + +// The words are not a new meaning: every canonical page action builds the +// action its snake-case spelling always built. +func TestR8PageActionWordsBuildTheirAction(t *testing.T) { + cases := map[string]ast.ActionV3{ + "save changes close page": {Type: "save", ClosePage: true}, + "cancel changes": {Type: "cancel"}, + "close page": {Type: "close"}, + "delete close page": {Type: "delete", ClosePage: true}, + "sign out": {Type: "signOut"}, + "complete task 'Approve'": {Type: "completeTask", OutcomeValue: "Approve"}, + "open link 'https://x.org'": {Type: "openLink", LinkURL: "https://x.org"}, + "call microflow M.ACT": {Type: "microflow", Target: "M.ACT"}, + "call nanoflow M.NF": {Type: "nanoflow", Target: "M.NF"}, + "show page M.Edit": {Type: "showPage", Target: "M.Edit"}, + } + for src, want := range cases { + prog := mustBuild(t, pageWith(src)) + stmt := prog.Statements[0].(*ast.CreatePageStmtV3) + got, ok := stmt.Widgets[0].Properties["Action"].(*ast.ActionV3) + if !ok { + t.Errorf("%s: Action is %T, want *ast.ActionV3", src, stmt.Widgets[0].Properties["Action"]) + continue + } + if !reflect.DeepEqual(*got, want) { + t.Errorf("%s: built %+v, want %+v", src, *got, want) + } + } +} diff --git a/mdl/visitor/visitor_association.go b/mdl/visitor/visitor_association.go index d059f06ad..a009a72f5 100644 --- a/mdl/visitor/visitor_association.go +++ b/mdl/visitor/visitor_association.go @@ -195,6 +195,7 @@ func anchorCoord(text string) (int, bool) { // ExitAlterAssociationAction handles ALTER ASSOCIATION ... SET ... actions. func (b *Builder) ExitAlterAssociationAction(ctx *parser.AlterAssociationActionContext) { + b.recordDeleteBehavior(ctx.DELETE_BEHAVIOR(), ctx.DeleteBehavior()) // Walk up to the parent AlterStatement to get the association's qualified name parent := ctx.GetParent() for parent != nil { diff --git a/mdl/visitor/visitor_deprecations_test.go b/mdl/visitor/visitor_deprecations_test.go index a75e35ce0..a45260bd3 100644 --- a/mdl/visitor/visitor_deprecations_test.go +++ b/mdl/visitor/visitor_deprecations_test.go @@ -79,7 +79,7 @@ var createOrReplaceCases = map[string]string{ "layout": "layout M.App_Default (layouttype: 'Responsive') { placeholder Main }", "snippet": "snippet M.CustomerInfo { dynamictext t (Content: 'x') }", "enumeration": "enumeration M.Color (Red 'Red');", - "validationrule": "validation rule for M.Customer.Email regex M.EmailPattern feedback 'Invalid';", + "validationrule": "validation rule for M.Customer.Email regex M.EmailPattern error message 'Invalid';", "databaseconnection": "database connection M.Erp type 'PostgreSQL' connection string @M.DbUrl username @M.DbUser password @M.DbPass;", "constant": "constant M.ApiBaseUrl type String default 'https://api.example.com';", "restclient": "rest client M.PetStore (BaseUrl: 'https://petstore.example.com', Authentication: NONE) { };", diff --git a/mdl/visitor/visitor_entity.go b/mdl/visitor/visitor_entity.go index 98f23d61a..6c9b9fccc 100644 --- a/mdl/visitor/visitor_entity.go +++ b/mdl/visitor/visitor_entity.go @@ -675,12 +675,12 @@ func (b *Builder) ExitAlterEntityAction(ctx *parser.AlterEntityActionContext) { stmt.ModifyNotNull = &false_ case c.NOT_NULL() != nil || (c.NOT() != nil && c.NULL() != nil) || c.REQUIRED() != nil: stmt.ModifyNotNull = &true_ - if c.ERROR() != nil && c.STRING_LITERAL() != nil { + if c.ConstraintErrorKeyword() != nil && c.STRING_LITERAL() != nil { stmt.ModifyNotNullError = unquoteStringLit(c.STRING_LITERAL()) } case c.UNIQUE() != nil: stmt.ModifyUnique = &true_ - if c.ERROR() != nil && c.STRING_LITERAL() != nil { + if c.ConstraintErrorKeyword() != nil && c.STRING_LITERAL() != nil { stmt.ModifyUniqueError = unquoteStringLit(c.STRING_LITERAL()) } case c.DEFAULT() != nil: diff --git a/mdl/visitor/visitor_helpers.go b/mdl/visitor/visitor_helpers.go index 4a3a52f57..4ef945f20 100644 --- a/mdl/visitor/visitor_helpers.go +++ b/mdl/visitor/visitor_helpers.go @@ -191,14 +191,14 @@ func buildAttributes(ctx parser.IAttributeDefinitionListContext, b *Builder) []a if c.NOT() != nil && c.NULL() != nil || c.NOT_NULL() != nil { attr.NotNull = true // Extract error message if present - if c.ERROR() != nil && c.STRING_LITERAL() != nil { + if c.ConstraintErrorKeyword() != nil && c.STRING_LITERAL() != nil { attr.NotNullError = unquoteStringLit(c.STRING_LITERAL()) } } if c.UNIQUE() != nil { attr.Unique = true // Extract error message if present - if c.ERROR() != nil && c.STRING_LITERAL() != nil { + if c.ConstraintErrorKeyword() != nil && c.STRING_LITERAL() != nil { attr.UniqueError = unquoteStringLit(c.STRING_LITERAL()) } } @@ -214,7 +214,7 @@ func buildAttributes(ctx parser.IAttributeDefinitionListContext, b *Builder) []a if c.REQUIRED() != nil { attr.NotNull = true // Extract error message if present - if c.ERROR() != nil && c.STRING_LITERAL() != nil { + if c.ConstraintErrorKeyword() != nil && c.STRING_LITERAL() != nil { attr.NotNullError = unquoteStringLit(c.STRING_LITERAL()) } } @@ -252,13 +252,13 @@ func buildSingleAttribute(a *parser.AttributeDefinitionContext) *ast.Attribute { c := constraintCtx.(*parser.AttributeConstraintContext) if c.NOT() != nil && c.NULL() != nil || c.NOT_NULL() != nil { attr.NotNull = true - if c.ERROR() != nil && c.STRING_LITERAL() != nil { + if c.ConstraintErrorKeyword() != nil && c.STRING_LITERAL() != nil { attr.NotNullError = unquoteStringLit(c.STRING_LITERAL()) } } if c.UNIQUE() != nil { attr.Unique = true - if c.ERROR() != nil && c.STRING_LITERAL() != nil { + if c.ConstraintErrorKeyword() != nil && c.STRING_LITERAL() != nil { attr.UniqueError = unquoteStringLit(c.STRING_LITERAL()) } } @@ -272,7 +272,7 @@ func buildSingleAttribute(a *parser.AttributeDefinitionContext) *ast.Attribute { } if c.REQUIRED() != nil { attr.NotNull = true - if c.ERROR() != nil && c.STRING_LITERAL() != nil { + if c.ConstraintErrorKeyword() != nil && c.STRING_LITERAL() != nil { attr.NotNullError = unquoteStringLit(c.STRING_LITERAL()) } } diff --git a/mdl/visitor/visitor_page_v3.go b/mdl/visitor/visitor_page_v3.go index 99815096e..c078be764 100644 --- a/mdl/visitor/visitor_page_v3.go +++ b/mdl/visitor/visitor_page_v3.go @@ -1089,9 +1089,13 @@ func buildActionV3(ctx parser.IActionExprV3Context) *ast.ActionV3 { actCtx := ctx.(*parser.ActionExprV3Context) action := &ast.ActionV3{} - if v := actCtx.VARIABLE(); v != nil && actCtx.OPEN_LINK() == nil { + // Each page action has a canonical spelling in words and a deprecated + // snake-case one (R8, ako/mxcli#752); both build the same action, and + // ExitActionExprV3 reports the old one. + closePage := actCtx.ClosePageV3() != nil + if v := actCtx.VARIABLE(); v != nil && actCtx.OpenLinkV3() == nil { // $handler — a fragment action parameter; resolved at expansion. - // (OPEN_LINK $currentObject/Attr also carries a VARIABLE.) + // (open link $currentObject/Attr also carries a VARIABLE.) action.Type = "param" action.Target = strings.TrimPrefix(v.GetText(), "$") } else if actCtx.NOTHING() != nil { @@ -1103,18 +1107,16 @@ func buildActionV3(ctx parser.IActionExprV3Context) *ast.ActionV3 { action.Type = "none" } else if actCtx.SAVE_CHANGES() != nil { action.Type = "save" - action.ClosePage = actCtx.CLOSE_PAGE() != nil + action.ClosePage = closePage } else if actCtx.CANCEL_CHANGES() != nil { action.Type = "cancel" - action.ClosePage = actCtx.CLOSE_PAGE() != nil - } else if actCtx.CLOSE_PAGE() != nil && actCtx.SAVE_CHANGES() == nil && actCtx.CANCEL_CHANGES() == nil { - action.Type = "close" - } else if actCtx.DELETE_OBJECT() != nil { - action.Type = "delete" - } else if actCtx.DELETE() != nil { + action.ClosePage = closePage + } else if actCtx.DELETE_OBJECT() != nil || actCtx.DELETE() != nil { action.Type = "delete" - action.ClosePage = actCtx.CLOSE_PAGE() != nil - } else if actCtx.CREATE_OBJECT() != nil { + action.ClosePage = closePage + } else if closePage { + action.Type = "close" + } else if actCtx.CREATE_OBJECT() != nil || actCtx.CREATE() != nil { action.Type = "create" if qn := actCtx.QualifiedName(); qn != nil { action.Target = getQualifiedNameText(qn) @@ -1123,7 +1125,7 @@ func buildActionV3(ctx parser.IActionExprV3Context) *ast.ActionV3 { if thenCtx := actCtx.ActionExprV3(); thenCtx != nil { action.ThenAction = buildActionV3(thenCtx) } - } else if actCtx.SHOW_PAGE() != nil { + } else if actCtx.SHOW_PAGE() != nil || actCtx.SHOW() != nil { action.Type = "showPage" if qn := actCtx.QualifiedName(); qn != nil { action.Target = getQualifiedNameText(qn) @@ -1147,12 +1149,12 @@ func buildActionV3(ctx parser.IActionExprV3Context) *ast.ActionV3 { if argsCtx := actCtx.MicroflowArgsV3(); argsCtx != nil { action.Args = buildMicroflowArgsV3(argsCtx) } - } else if actCtx.OPEN_LINK() != nil { + } else if actCtx.OpenLinkV3() != nil { action.Type = "openLink" if str := actCtx.STRING_LITERAL(); str != nil { action.LinkURL = unquoteStringLit(str) } - // A dynamic address: `open_link $currentObject/URL`. + // A dynamic address: `open link $currentObject/URL`. if v := actCtx.VARIABLE(); v != nil { action.LinkVariable = v.GetText() if pathCtx := actCtx.AttributePathV3(); pathCtx != nil { diff --git a/mdl/visitor/visitor_r8_spellings.go b/mdl/visitor/visitor_r8_spellings.go new file mode 100644 index 000000000..21b9b44ff --- /dev/null +++ b/mdl/visitor/visitor_r8_spellings.go @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: Apache-2.0 + +package visitor + +import ( + "strings" + + "github.com/antlr4-go/antlr/v4" + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/deprecation" + "github.com/mendixlabs/mxcli/mdl/grammar/parser" +) + +// R8 (PROPOSAL_mdl_beta_syntax_freeze.md §3, ako/mxcli#752): words, not +// SCREAMING_SNAKE, and one spelling per keyword. Every old spelling here is a +// respelling — the grammar builds the same statement from both — so each use +// is recorded as a deprecation with the edit that rewrites it, and nothing is +// gated on the language version. + +// recordRespelling records code at tok with a fix that replaces tok's text. +// The subject names the use and its rewrite (`show_page -> show page`), since +// the registry entry covers a family of spellings. +func (b *Builder) recordRespelling(code string, tok antlr.Token, text string) { + if tok == nil { + return + } + b.recordDeprecation(code, tok, tok.GetText()+" -> "+text) + b.fixLastDeprecation(code, &ast.Fix{Edits: []ast.TextEdit{{Start: tok.GetStart(), Stop: tok.GetStop() + 1, Text: text}}}, "") +} + +// recordSnakeWord records a page-action token spelled with an underscore +// (`save_changes`), whose canonical spelling is the same words with a space. +// The lexer tokens that admit both spellings are told apart by the underscore. +func (b *Builder) recordSnakeWord(n antlr.TerminalNode) { + if n == nil { + return + } + tok := n.GetSymbol() + if text := tok.GetText(); strings.Contains(text, "_") { + b.recordRespelling(deprecation.PageActionWord, tok, strings.ReplaceAll(text, "_", " ")) + } +} + +// ExitActionExprV3 reports a page action written in its snake-case spelling, +// or a flow call written without `call`. +func (b *Builder) ExitActionExprV3(ctx *parser.ActionExprV3Context) { + for _, n := range []antlr.TerminalNode{ctx.SAVE_CHANGES(), ctx.CANCEL_CHANGES(), ctx.SHOW_PAGE(), + ctx.CREATE_OBJECT(), ctx.SIGN_OUT(), ctx.COMPLETE_TASK()} { + b.recordSnakeWord(n) + } + if n := ctx.DELETE_OBJECT(); n != nil { + tok := n.GetSymbol() + b.recordRespelling(deprecation.PageActionWord, tok, keywordLike(tok.GetText(), "delete")) + } + if ctx.CALL() == nil { + for _, n := range []antlr.TerminalNode{ctx.MICROFLOW(), ctx.NANOFLOW()} { + if n == nil { + continue + } + tok := n.GetSymbol() + b.recordDeprecation(deprecation.PageActionWord, tok, tok.GetText()+" -> "+keywordLike(tok.GetText(), "call")+" "+tok.GetText()) + b.fixLastDeprecation(deprecation.PageActionWord, + &ast.Fix{Edits: []ast.TextEdit{insertAt(tok.GetStart(), keywordLike(tok.GetText(), "call")+" ")}}, "") + } + } +} + +// ExitClosePageV3 reports `close_page`. +func (b *Builder) ExitClosePageV3(ctx *parser.ClosePageV3Context) { + b.recordSnakeWord(ctx.CLOSE_PAGE()) +} + +// ExitOpenLinkV3 reports `open_link`. +func (b *Builder) ExitOpenLinkV3(ctx *parser.OpenLinkV3Context) { b.recordSnakeWord(ctx.OPEN_LINK()) } + +// ExitNavMenuItemDef reports a menu item's `sign_out`: menus use the page +// action vocabulary. +func (b *Builder) ExitNavMenuItemDef(ctx *parser.NavMenuItemDefContext) { + b.recordSnakeWord(ctx.SIGN_OUT()) +} + +// errorMessageWord is the canonical spelling of the message keyword, in the +// letter case of the spelling it replaces. +func errorMessageWord(like string) string { return keywordLike(like, "error message") } + +// recordErrorMessageToken reports an ERROR_MESSAGE token spelled `error_message` +// or `errormessage`. Only where the token is the message keyword: an attribute +// or key named ErrorMessage lexes as the same token. +func (b *Builder) recordErrorMessageToken(n antlr.TerminalNode, subject string) { + if n == nil { + return + } + tok := n.GetSymbol() + if text := tok.GetText(); !strings.ContainsAny(text, " \t\r\n") { + b.recordRespelling(deprecation.ErrorMessageKeyword, tok, errorMessageWord(text)) + } +} + +// ExitConstraintErrorKeyword reports `not null error '…'` (and after unique +// and required). +func (b *Builder) ExitConstraintErrorKeyword(ctx *parser.ConstraintErrorKeywordContext) { + if n := ctx.ERROR(); n != nil { + tok := n.GetSymbol() + b.recordRespelling(deprecation.ErrorMessageKeyword, tok, errorMessageWord(tok.GetText())) + return + } + b.recordErrorMessageToken(ctx.ERROR_MESSAGE(), "attribute constraint") +} + +// ExitErrorMessageClause reports an association's `error_message '…'`. +func (b *Builder) ExitErrorMessageClause(ctx *parser.ErrorMessageClauseContext) { + b.recordErrorMessageToken(ctx.ERROR_MESSAGE(), "association") +} + +// ExitMicroflowConcurrencyError reports `disallow concurrent execution +// error_message '…'`. +func (b *Builder) ExitMicroflowConcurrencyError(ctx *parser.MicroflowConcurrencyErrorContext) { + b.recordErrorMessageToken(ctx.ERROR_MESSAGE(), "microflow") +} + +// recordValidationRuleFeedback reports a validation rule's `feedback '…'`. +func (b *Builder) recordValidationRuleFeedback(ctx *parser.CreateValidationRuleStatementContext) { + if n := ctx.FEEDBACK(); n != nil { + tok := n.GetSymbol() + b.recordRespelling(deprecation.ErrorMessageKeyword, tok, errorMessageWord(tok.GetText())) + return + } + b.recordErrorMessageToken(ctx.ERROR_MESSAGE(), "validation rule") +} + +// onDeleteFor is the `on delete` action a delete_behavior behaviour means; the +// mapping is buildDeleteBehavior's, read against buildReferentialAction. +func onDeleteFor(ctx parser.IDeleteBehaviorContext) string { + db, ok := ctx.(*parser.DeleteBehaviorContext) + if !ok { + return "" + } + switch { + case db.CASCADE() != nil, db.DELETE_AND_REFERENCES() != nil: + return "on delete cascade" + case db.PREVENT() != nil, db.DELETE_IF_NO_REFERENCES() != nil: + return "on delete restrict" + case db.DELETE_BUT_KEEP_REFERENCES() != nil: + return "on delete set null" + } + return "" +} + +// recordDeleteBehavior reports `delete_behavior `, rewritten to the +// `on delete` action that means the same. +func (b *Builder) recordDeleteBehavior(kw antlr.TerminalNode, behaviour parser.IDeleteBehaviorContext) { + if kw == nil || behaviour == nil { + return + } + tok := kw.GetSymbol() + action := onDeleteFor(behaviour) + b.recordDeprecation(deprecation.DeleteBehaviorClause, tok, "-> "+action) + if action == "" || behaviour.GetStop() == nil { + b.fixLastDeprecation(deprecation.DeleteBehaviorClause, nil, "unrecognised delete behaviour") + return + } + edit := replaceSpan(tok, behaviour.GetStop(), keywordLike(tok.GetText(), action)) + b.fixLastDeprecation(deprecation.DeleteBehaviorClause, &ast.Fix{Edits: []ast.TextEdit{edit}}, "") +} + +// ExitAssociationOption reports `delete_behavior …` and `reference_set`. +func (b *Builder) ExitAssociationOption(ctx *parser.AssociationOptionContext) { + b.recordDeleteBehavior(ctx.DELETE_BEHAVIOR(), ctx.DeleteBehavior()) + if n := ctx.REFERENCE_SET(); n != nil && strings.Contains(n.GetText(), "_") { + b.recordRespelling(deprecation.ReferenceSetUnderscore, n.GetSymbol(), "ReferenceSet") + } +} + +// ExitRestCallReturnsClause reports `returns none`. The registry entry is a +// keyword swap, so no per-use fix is attached. +func (b *Builder) ExitRestCallReturnsClause(ctx *parser.RestCallReturnsClauseContext) { + if n := ctx.NONE(); n != nil { + b.recordDeprecation(deprecation.ReturnsNone, n.GetSymbol(), "rest call") + } +} diff --git a/mdl/visitor/visitor_validationrule.go b/mdl/visitor/visitor_validationrule.go index 0a34b03a5..ba39ec9ba 100644 --- a/mdl/visitor/visitor_validationrule.go +++ b/mdl/visitor/visitor_validationrule.go @@ -8,8 +8,9 @@ import ( ) // ExitCreateValidationRuleStatement builds a CreateValidationRuleStmt from -// CREATE VALIDATION RULE FOR Module.Entity.Attribute FEEDBACK '...'. +// CREATE VALIDATION RULE FOR Module.Entity.Attribute error message '...' (`feedback '...'` is the deprecated spelling). func (b *Builder) ExitCreateValidationRuleStatement(ctx *parser.CreateValidationRuleStatementContext) { + b.recordValidationRuleFeedback(ctx) stmt := &ast.CreateValidationRuleStmt{ Attribute: buildQualifiedName(ctx.QualifiedName()), } From 8c56f776d046de1fcdecf5350890ca2e153587d9 Mon Sep 17 00:00:00 2001 From: Ako Date: Sun, 27 Sep 2026 19:48:46 +0000 Subject: [PATCH 05/11] fmt: lowercase keywords from the parse tree, never names or keys (R8) formatter.Format upper-cased every word on a keyword list, names included: Issue64.User became Issue64.USER and an attribute Title became TITLE, so fmt changed what 374 of the example scripts build. It now lowercases only the tokens the parse tree shows are keywords, and keeps a property key, a name, a CamelCase or digit-bearing value, a data type and an annotation as written. Text the model stores as written (expressions, XPath, OQL, catalog and SQL queries, template parameters, multi-line literals) keeps its case and its layout. TestFormatterKeepsMeaning replaces TestFormatterChangesMeaning: every example script formats to one that builds the same statements. describe writes lowercase keywords where it did not: an enumeration's `folder`, a workflow's `default ->` outcome and a published REST operation's method. alter settings keeps the section lowercase in the AST, so its keyword case no longer changes what a script builds. TestPedApp/TestAppDescribeUsesCanonicalSpellings pin describe to the R8 spellings and lowercase keywords. Refs #752 Co-Authored-By: Claude Opus 5.5 --- cmd/mxcli/cmd_fmt.go | 7 +- mdl/executor/cmd_enumerations.go | 2 +- mdl/executor/cmd_published_rest.go | 2 +- mdl/executor/cmd_workflows.go | 3 + mdl/formatter/format.go | 187 +++++------------ .../describe_canonical_spelling_test.go | 82 ++++++++ mdl/upgrade/examples_test.go | 40 ++-- mdl/visitor/keyword_case.go | 189 ++++++++++++++++++ mdl/visitor/visitor_settings.go | 7 +- mdl/visitor/visitor_settings_test.go | 2 +- 10 files changed, 366 insertions(+), 155 deletions(-) create mode 100644 mdl/roundtrip/describe_canonical_spelling_test.go create mode 100644 mdl/visitor/keyword_case.go diff --git a/cmd/mxcli/cmd_fmt.go b/cmd/mxcli/cmd_fmt.go index 1bf949c83..cb1146459 100644 --- a/cmd/mxcli/cmd_fmt.go +++ b/cmd/mxcli/cmd_fmt.go @@ -21,7 +21,12 @@ var fmtCmd = &cobra.Command{ Use: "fmt [file.mdl | -]", Short: "Format an MDL file", Long: `Format an MDL script file with consistent styling: - - Uppercase MDL keywords + - Lowercase MDL keywords, the canonical case. Only words the parse tree shows + are keywords change: a name spelled like a keyword (Issue64.User, an + attribute Title), a property key (Folder:) and a CamelCase value + (ButtonStyle: Success) keep their case, and so do expressions, XPath, OQL + and SQL, which are stored as written. Formatting never changes what a + script builds. - Normalize indentation (2-space units) - Remove trailing whitespace - Normalize blank lines diff --git a/mdl/executor/cmd_enumerations.go b/mdl/executor/cmd_enumerations.go index 0a803a7f9..a3e02f782 100644 --- a/mdl/executor/cmd_enumerations.go +++ b/mdl/executor/cmd_enumerations.go @@ -451,7 +451,7 @@ func describeEnumeration(ctx *ExecContext, name ast.QualifiedName) error { // folder path when the enum lives in a folder. folderClause := "" if fp := h.BuildFolderPath(enum.ContainerID); fp != "" && fp != modName { - folderClause = fmt.Sprintf(" FOLDER '%s'", fp) + folderClause = fmt.Sprintf(" folder '%s'", fp) } fmt.Fprintf(ctx.Output, ")%s;\n", folderClause) return nil diff --git a/mdl/executor/cmd_published_rest.go b/mdl/executor/cmd_published_rest.go index 59e561ce5..ef8517408 100644 --- a/mdl/executor/cmd_published_rest.go +++ b/mdl/executor/cmd_published_rest.go @@ -135,7 +135,7 @@ func describePublishedRestService(ctx *ExecContext, name ast.QualifiedName) erro opPath = " " + mdlQuoted(op.Path) } fmt.Fprintf(ctx.Output, " %s%s%s%s;%s\n", - strings.ToUpper(op.HTTPMethod), opPath, mf, deprecated, summary) + strings.ToLower(op.HTTPMethod), opPath, mf, deprecated, summary) } fmt.Fprintln(ctx.Output, " }") } diff --git a/mdl/executor/cmd_workflows.go b/mdl/executor/cmd_workflows.go index 80b18d9f8..d94a1a111 100644 --- a/mdl/executor/cmd_workflows.go +++ b/mdl/executor/cmd_workflows.go @@ -992,6 +992,9 @@ func formatConditionOutcomes(outcomes []workflows.ConditionOutcome, indent strin lines = append(lines, fmt.Sprintf("%s outcomes", indent)) for _, outcome := range outcomes { name := outcome.GetName() + if _, ok := outcome.(*workflows.VoidConditionOutcome); ok { + name = "default" // a keyword: lowercase is canonical (R8) + } flow := outcome.GetFlow() if flow != nil && len(flow.Activities) > 0 { lines = append(lines, fmt.Sprintf("%s %s -> {", indent, name)) diff --git a/mdl/formatter/format.go b/mdl/formatter/format.go index dda52fca0..e7828cbda 100644 --- a/mdl/formatter/format.go +++ b/mdl/formatter/format.go @@ -1,69 +1,55 @@ // SPDX-License-Identifier: Apache-2.0 -// Package formatter provides heuristic MDL code formatting. +// Package formatter provides MDL code formatting. package formatter import ( + "sort" "strings" -) - -// mdlKeywords are MDL/SQL keywords that should be uppercased. -var mdlKeywords = []string{ - "CREATE", "OR", "MODIFY", "REPLACE", "DROP", "ALTER", "SHOW", "DESCRIBE", - "ENTITY", "ASSOCIATION", "ENUMERATION", "CONSTANT", "MODULE", "MICROFLOW", - "NANOFLOW", "PAGE", "SNIPPET", "LAYOUT", "WORKFLOW", "INDEX", - "ATTRIBUTE", "FROM", "TO", "TYPE", "DEFAULT", "OWNER", - "PERSISTENT", "NON_PERSISTENT", "SYSTEM_MEMBER", "STORED_VALUE", "CALCULATED", - "REQUIRED", "UNIQUE", "INDEXED", - "BEGIN", "END", "IF", "THEN", "ELSE", "LOOP", "IN", "RETURN", - "RETRIEVE", "WHERE", "LIMIT", "FIRST", "LIST", "OF", - "CHANGE", "DELETE", "COMMIT", "ROLLBACK", "DOWNLOAD", "BROWSER", - "SHOW_PAGE", "CLOSE_PAGE", "SHOW_MESSAGE", - "PARAMETER", "PARAMETERS", "VARIABLE", "DECLARE", - "JAVA_ACTION", "CALL", "CALL_MICROFLOW", "CALL_NANOFLOW", - "LOG_MESSAGE", "LEVEL", "MESSAGE", "NODE", - "ONE_TO_MANY", "MANY_TO_MANY", "ONE_TO_ONE", "REFERENCE", "REFERENCE_SET", - "EXPOSED", "CLIENT", "COMMENT", "FOLDER", - "ON_DELETE", "PREVENT", "CASCADE", - "GRANT", "REVOKE", "ACCESS", "ALLOW", "DENY", - "NOT", "AND", "NULL", "EMPTY", "TRUE", "FALSE", - "SET", "INSERT", "BEFORE", "AFTER", "REPLACE", - "BOOLEAN", "INTEGER", "LONG", "DECIMAL", "STRING", "DATETIME", "BINARY", - "AUTO_NUMBER", "HASHED_STRING", - "WIDGET", "COLUMN", "ROW", "CONTAINER", "DATAVIEW", "LISTVIEW", - "BUTTON", "TEXT", "LABEL", "TITLE", "INPUT", "DROPDOWN", "CHECKBOX", - "ENUMERATION_SELECTOR", "REFERENCE_SELECTOR", "DATE_PICKER", - "DATA_SOURCE", "DIRECT", "XPATH", - "NAVIGATION", "HOME", "MENU", "ITEM", - "ROLE", "ROLES", "USER", "SECURITY", "PASSWORD", - "REFRESH", "CATALOG", - "SELECT", "AS", "TABLE", "TABLES", - "SQL", "CONNECT", "QUERY", "IMPORT", "INTO", "MAP", - "MOVE", "IMAGE", "COLLECTION", -} -// keywordSet for O(1) lookup. -var keywordSet map[string]string + "github.com/mendixlabs/mxcli/mdl/visitor" +) -func init() { - keywordSet = make(map[string]string, len(mdlKeywords)) - for _, kw := range mdlKeywords { - keywordSet[strings.ToUpper(kw)] = kw +// Format formats MDL source code: +// - Lowercase MDL keywords, the canonical case (R8, ako/mxcli#752). Only the +// words the parse tree shows are keywords change: a name spelled like a +// keyword (`Issue64.User`, an attribute `Title`), a property key +// (`Folder:`) and a CamelCase value (`ButtonStyle: Success`) keep the +// author's case. +// - Normalize indentation to 2 spaces. +// - Remove trailing whitespace. +// - Normalize blank lines (max 1 consecutive). +// +// Text the model stores as written — expressions, XPath, OQL, queries, and +// string literals or code blocks that span lines — keeps its case and layout, +// so formatting never changes what a script builds. A script that does not +// parse keeps its case and is only re-indented. +func Format(input string) string { + spans, ok := visitor.FormatSpans(input) + if ok { + input = visitor.LowercaseKeywords(input, spans.Keywords) } -} + inside := verbatimIndex(spans.Verbatim) -// Format applies heuristic formatting to MDL source code: -// - Uppercase MDL keywords -// - Normalize indentation to 2 spaces -// - Remove trailing whitespace -// - Normalize blank lines (max 1 consecutive) -func Format(input string) string { lines := strings.Split(input, "\n") var result []string prevBlank := false + offset := 0 // rune offset of the current line's start for _, line := range lines { - trimmed := strings.TrimRight(line, " \t\r") + start, end := offset, offset+len([]rune(line)) // end: the '\n' after the line + offset = end + 1 + + // A line that starts inside verbatim text is part of that text. + if inside(start) { + result = append(result, line) + prevBlank = false + continue + } + trimmed := line + if !inside(end) { + trimmed = strings.TrimRight(line, " \t\r") + } // Collapse multiple blank lines if trimmed == "" { @@ -80,23 +66,17 @@ func Format(input string) string { // Count effective indent (tabs = 2 spaces) indent := 0 for _, ch := range trimmed { - switch ch { - case ' ': + if ch == ' ' { indent++ - case '\t': + } else if ch == '\t' { indent += 2 - default: - goto indentDone + } else { + break } } - indentDone: // Round to nearest 2-space unit - indentLevel := indent / 2 - normalizedIndent := strings.Repeat(" ", indentLevel) - - // Uppercase keywords (but not inside quoted strings) - formatted := uppercaseKeywords(stripped) - result = append(result, normalizedIndent+formatted) + normalizedIndent := strings.Repeat(" ", indent/2) + result = append(result, normalizedIndent+stripped) } // Remove trailing blank line @@ -107,77 +87,20 @@ func Format(input string) string { return strings.Join(result, "\n") + "\n" } -// uppercaseKeywords uppercases MDL keywords while preserving quoted strings. -func uppercaseKeywords(line string) string { - // Track whether we're inside a string literal - var result strings.Builder - inString := false - i := 0 - runes := []rune(line) - - for i < len(runes) { - ch := runes[i] - - if ch == '\'' { - // Toggle string mode, handle escaped quotes ('') - result.WriteRune(ch) - i++ - inString = !inString - continue - } - - if inString { - result.WriteRune(ch) - i++ - continue - } - - // Check for line comment (--) - if ch == '-' && i+1 < len(runes) && runes[i+1] == '-' { - // Rest of line is comment, write as-is - for i < len(runes) { - result.WriteRune(runes[i]) - i++ - } - continue - } - - // Check for block comment start - if ch == '/' && i+1 < len(runes) && runes[i+1] == '*' { - // Write rest as-is (simplification: assume comment ends on same line or later) - for i < len(runes) { - result.WriteRune(runes[i]) - i++ - } - continue - } - - // Try to match a word - if isWordStart(ch) { - wordStart := i - for i < len(runes) && isWordChar(runes[i]) { - i++ +// verbatimIndex returns whether a rune offset lies strictly inside one of the +// spans: after its first rune and before its end. +func verbatimIndex(spans [][2]int) func(int) bool { + sorted := append([][2]int(nil), spans...) + sort.Slice(sorted, func(i, j int) bool { return sorted[i][0] < sorted[j][0] }) + return func(off int) bool { + for _, s := range sorted { + if s[0] >= off { + return false } - word := string(runes[wordStart:i]) - if _, ok := keywordSet[strings.ToUpper(word)]; ok { - result.WriteString(strings.ToUpper(word)) - } else { - result.WriteString(word) + if off < s[1] { + return true } - continue } - - result.WriteRune(ch) - i++ + return false } - - return result.String() -} - -func isWordStart(ch rune) bool { - return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' -} - -func isWordChar(ch rune) bool { - return isWordStart(ch) || (ch >= '0' && ch <= '9') } diff --git a/mdl/roundtrip/describe_canonical_spelling_test.go b/mdl/roundtrip/describe_canonical_spelling_test.go new file mode 100644 index 000000000..b284a3b0b --- /dev/null +++ b/mdl/roundtrip/describe_canonical_spelling_test.go @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 + +//go:build integration + +package roundtrip + +import ( + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/deprecation" + "github.com/mendixlabs/mxcli/mdl/visitor" +) + +// TestPedAppDescribeUsesCanonicalSpellings holds describe to R8 (#752) and +// R12: it never emits a registered deprecated spelling (`show_page`, +// `error_message`, `delete_behavior`, …), and every keyword it writes is +// already lowercase — `mxcli fmt` over describe output changes no letter. +func TestPedAppDescribeUsesCanonicalSpellings(t *testing.T) { + describeUsesCanonicalSpellings(t, pedApp) +} + +// TestTestAppDescribeUsesCanonicalSpellings is the same over TestApp, whose +// pages carry every page action (#759). +func TestTestAppDescribeUsesCanonicalSpellings(t *testing.T) { + describeUsesCanonicalSpellings(t, testApp) +} + +func describeUsesCanonicalSpellings(t *testing.T, fx fixture) { + h := newFixtureHarness(t, fx) + defer h.close() + + checked := 0 + for _, target := range describeTargets(h) { + t.Run(target, func(t *testing.T) { + out, err := h.describe(target) + if err != nil || strings.TrimSpace(out) == "" { + t.Skipf("describe %s: err=%v (judged by TestPedAppRoundTrip)", target, err) + } + prog, errs := visitor.Build(out) + if len(errs) > 0 { + t.Skipf("does not parse (judged by TestPedAppDescribeIsValidMdl1): %v", errs[0]) + } + checked++ + for _, d := range prog.Deprecations { + if !r8Codes[d.Code] { + continue // the list-operation call forms are #737's (describe keeps them under mdl 0) + } + t.Errorf("line %d: describe emits the deprecated spelling %s\n--- describe output ---\n%s", d.Line, d.Code, out) + } + spans, ok := visitor.FormatSpans(out) + if !ok { + t.Fatalf("FormatSpans refused output that parses") + } + if lower := visitor.LowercaseKeywords(out, spans.Keywords); lower != out { + t.Errorf("describe emits an upper-case keyword:\n%s", firstDifferentLine(out, lower)) + } + }) + } + if checked == 0 { + t.Fatal("no describe output was checked — the test proves nothing") + } +} + +// r8Codes are the registry codes R8 (#752) owns. +var r8Codes = map[string]bool{ + deprecation.PageActionWord: true, + deprecation.ErrorMessageKeyword: true, + deprecation.DeleteBehaviorClause: true, + deprecation.ReferenceSetUnderscore: true, + deprecation.ReturnsNone: true, +} + +func firstDifferentLine(a, b string) string { + al, bl := strings.Split(a, "\n"), strings.Split(b, "\n") + for i := range al { + if i < len(bl) && al[i] != bl[i] { + return " describe: " + al[i] + "\n canonical: " + bl[i] + } + } + return "" +} diff --git a/mdl/upgrade/examples_test.go b/mdl/upgrade/examples_test.go index 6187a4afa..7aff1a46e 100644 --- a/mdl/upgrade/examples_test.go +++ b/mdl/upgrade/examples_test.go @@ -46,21 +46,20 @@ func exampleScripts(t *testing.T) []string { } // fmtUpgrade is what `mxcli fmt --upgrade` writes: the upgrade rewrites and -// nothing else. It deliberately does not run formatter.Format, whose keyword -// upper-casing changes identifiers (see TestFormatterChangesMeaning). +// nothing else. It does not run formatter.Format, whose line normalisation is +// a separate choice (`fmt` without --upgrade). func fmtUpgrade(src string, opts Options) (string, Result, error) { res, err := Upgrade(src, opts) return res.Source, res, err } -// TestFormatterChangesMeaning records why `fmt --upgrade` does not compose with -// the heuristic formatter: formatter.Format upper-cases every word on its -// keyword list, identifiers included (`Issue64.User` becomes `Issue64.USER`), -// so it changes what a script builds. Plan item 3.2 (lowercase canonical) -// replaces it. When this test starts failing, the formatter is safe and -// fmt --upgrade may format as well. -func TestFormatterChangesMeaning(t *testing.T) { - changed := 0 +// TestFormatterKeepsMeaning: formatter.Format must build exactly what the +// script built. It used to upper-case every word on a keyword list, names +// included (`Issue64.User` became `Issue64.USER`), and changed what 50-odd +// example scripts built; lowercase-canonical keywords from the parse tree +// (R8, ako/mxcli#752) change only the case of syntax. +func TestFormatterKeepsMeaning(t *testing.T) { + checked := 0 for _, path := range exampleScripts(t) { b, err := os.ReadFile(path) if err != nil { @@ -70,14 +69,23 @@ func TestFormatterChangesMeaning(t *testing.T) { if len(errs) > 0 { continue } - got, errs := visitor.Build(formatter.Format(string(b))) - if len(errs) > 0 || !reflect.DeepEqual(want.Statements, got.Statements) { - changed++ + checked++ + formatted := formatter.Format(string(b)) + got, errs := visitor.Build(formatted) + if len(errs) > 0 { + t.Errorf("%s: formatted script does not parse: %v", path, errs[0]) + continue + } + if !reflect.DeepEqual(want.Statements, got.Statements) { + t.Errorf("%s: formatter.Format changes what the script builds", path) + continue + } + if again := formatter.Format(formatted); again != formatted { + t.Errorf("%s: formatter.Format is not idempotent", path) } } - t.Logf("formatter.Format changes what %d example scripts build", changed) - if changed == 0 { - t.Error("formatter.Format no longer changes any example's statements: let fmt --upgrade format too, and drop this test") + if checked < 100 { + t.Fatalf("checked only %d scripts", checked) } } diff --git a/mdl/visitor/keyword_case.go b/mdl/visitor/keyword_case.go new file mode 100644 index 000000000..157df7e4c --- /dev/null +++ b/mdl/visitor/keyword_case.go @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: Apache-2.0 + +package visitor + +import ( + "strings" + + "github.com/antlr4-go/antlr/v4" + "github.com/mendixlabs/mxcli/mdl/grammar/parser" +) + +// Lowercase keywords are canonical (R8, ako/mxcli#752): describe writes them +// and `mxcli fmt` normalises to them. What makes that safe is telling a +// keyword apart from a NAME that happens to be spelled like one. The lexer +// cannot: `Title`, `Folder` and `User` are keyword tokens wherever they occur, +// and the old formatter upper-cased them all, turning `Issue64.User` into +// `Issue64.USER` — a different module member. Only the parse tree knows, so +// the spans come from it. + +// nameRules are the rules through which a keyword token is used as a name, or +// whose text is carried into the model verbatim (an expression, XPath or OQL +// is stored as its source text, so its letter case is the author's). A data +// type (`String(200)`) is a Mendix type name, not a keyword. +var nameRules = map[string]bool{ + "keyword": true, + "identifierOrKeyword": true, + "qualifiedName": true, + "dataType": true, +} + +// verbatimRules are the rules whose source text the visitor stores as written +// (extractOriginalText and friends): letter case AND layout are the author's. +var verbatimRules = map[string]bool{ + "expression": true, + "catalogSelectQuery": true, + "sqlPassthrough": true, + // A template parameter keeps the whitespace after its expression + // (appendTemplateParamTrailingWhitespace), so the whole list is kept. + "templateParams": true, +} + +// verbatimRulePrefixes are rule-name prefixes whose text is kept verbatim (an +// annotation, `@Position(…)`, is a name too). +var verbatimRulePrefixes = []string{"xpath", "oql", "annotation"} + +// Spans is what the formatter needs from the parse tree. +type Spans struct { + // Keywords are the rune spans [start, stop) of every keyword that is + // syntax: not a name, not inside an expression, XPath or OQL, not a + // property key (a keyword directly followed by `:` or `=` is a key, and + // keys are case-preserved identifiers), and not a value written the way + // Mendix writes names and enumeration values — in CamelCase + // (`ReferenceSet`, `ButtonStyle: Success`) or with a digit + // (`RenderMode: H2`). An all-caps or all-lower word is a keyword however + // it is used. + Keywords [][2]int + // Verbatim are the rune spans of text stored as written — expressions, + // XPath, OQL, catalog and SQL queries, and every token that spans lines + // (a string literal, a code block) — whose layout must not change either. + Verbatim [][2]int +} + +// FormatSpans parses src and returns its Spans. ok is false when src does not +// parse; the caller must then leave it alone. +func FormatSpans(src string) (spans Spans, ok bool) { + errs := newErrorListener() + lexer := parser.NewMDLLexer(newScriptStream(src)) + lexer.RemoveErrorListeners() + lexer.AddErrorListener(errs) + stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel) + p := parser.NewMDLParser(stream) + p.RemoveErrorListeners() + p.AddErrorListener(errs) + tree := p.Program() + if len(errs.errors) > 0 { + return Spans{}, false + } + ruleNames := p.GetRuleNames() + isVerbatimRule := func(name string) bool { + if verbatimRules[name] { + return true + } + for _, pre := range verbatimRulePrefixes { + if strings.HasPrefix(name, pre) { + return true + } + } + return false + } + isKey := func(tok antlr.Token) bool { + i := tok.GetTokenIndex() + 1 + if i >= stream.Size() { + return false + } + switch stream.Get(i).GetTokenType() { + case parser.MDLLexerCOLON, parser.MDLLexerEQUALS: + return true + } + return false + } + // walk visits t; kept says a rule above t keeps its text as written. + var walk func(t antlr.Tree, kept bool) + walk = func(t antlr.Tree, kept bool) { + switch n := t.(type) { + case antlr.TerminalNode: + tok := n.GetSymbol() + if tok.GetTokenType() == antlr.TokenEOF { + return + } + if strings.ContainsAny(tok.GetText(), "\n\r") { + spans.Verbatim = append(spans.Verbatim, [2]int{tok.GetStart(), tok.GetStop() + 1}) + } + if kept || !isKeywordToken(tok) || isKey(tok) || writtenAsAName(tok.GetText()) { + return + } + spans.Keywords = append(spans.Keywords, [2]int{tok.GetStart(), tok.GetStop() + 1}) + case antlr.ParserRuleContext: + name := ruleNames[n.GetRuleIndex()] + if !kept && isVerbatimRule(name) { + if start, stop := n.GetStart(), n.GetStop(); start != nil && stop != nil && stop.GetStop() >= start.GetStart() { + spans.Verbatim = append(spans.Verbatim, [2]int{start.GetStart(), stop.GetStop() + 1}) + } + kept = true + } + kept = kept || nameRules[name] + for i := 0; i < n.GetChildCount(); i++ { + walk(n.GetChild(i), kept) + } + } + } + walk(tree, false) + return spans, true +} + +// isKeywordToken reports whether tok is a keyword: a token other than an +// identifier whose text is a word (or words, for the multi-word tokens). +func isKeywordToken(tok antlr.Token) bool { + switch tok.GetTokenType() { + case antlr.TokenEOF, parser.MDLLexerIDENTIFIER, parser.MDLLexerQUOTED_IDENTIFIER: + return false + } + text := tok.GetText() + if text == "" || !(text[0] >= 'a' && text[0] <= 'z' || text[0] >= 'A' && text[0] <= 'Z') { + return false + } + for _, r := range text { + switch { + case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_', + r == ' ', r == '\t', r == '\r', r == '\n': + default: + return false + } + } + return true +} + +// writtenAsAName reports whether a keyword token is spelled like a Mendix name +// or enumeration value: mixed case, or containing a digit. +func writtenAsAName(text string) bool { + var upper, lower bool + for _, r := range text { + switch { + case r >= '0' && r <= '9': + return true + case r >= 'A' && r <= 'Z': + upper = true + case r >= 'a' && r <= 'z': + lower = true + } + } + return upper && lower +} + +// LowercaseKeywords returns src with every syntax keyword (Spans.Keywords) in +// lower case and everything else untouched. +func LowercaseKeywords(src string, keywords [][2]int) string { + if len(keywords) == 0 { + return src + } + runes := []rune(src) + for _, s := range keywords { + for i := s[0]; i < s[1] && i < len(runes); i++ { + if r := runes[i]; r >= 'A' && r <= 'Z' { + runes[i] = r + ('a' - 'A') + } + } + } + return string(runes) +} diff --git a/mdl/visitor/visitor_settings.go b/mdl/visitor/visitor_settings.go index 708e26a26..94044d58e 100644 --- a/mdl/visitor/visitor_settings.go +++ b/mdl/visitor/visitor_settings.go @@ -4,6 +4,7 @@ package visitor import ( "strconv" + "strings" "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/grammar/parser" @@ -69,7 +70,7 @@ func (b *Builder) ExitAlterSettingsClause(ctx *parser.AlterSettingsClauseContext // ALTER SETTINGS WORKFLOWS ADD [OR MODIFY] GROUP 'Approvers' [( Description: '…' )] // ALTER SETTINGS WORKFLOWS MODIFY GROUP 'Approvers' ( Description: '…' ) // ALTER SETTINGS WORKFLOWS REMOVE GROUP 'Approvers' - stmt.Section = ctx.SettingsSection().GetText() + stmt.Section = strings.ToLower(ctx.SettingsSection().GetText()) stmt.UpsertGroup = ctx.ADD() != nil && ctx.OR() != nil && ctx.MODIFY() != nil stmt.AddGroup = ctx.ADD() != nil && !stmt.UpsertGroup stmt.ModifyGroup = ctx.MODIFY() != nil && !stmt.UpsertGroup @@ -82,7 +83,7 @@ func (b *Builder) ExitAlterSettingsClause(ctx *parser.AlterSettingsClauseContext // ALTER SETTINGS LANGUAGE ADD 'ar_SD' [( key: value, … )] // ALTER SETTINGS LANGUAGE MODIFY 'ar_SD' ( key: value, … ) // ALTER SETTINGS LANGUAGE REMOVE 'ar_SD' - stmt.Section = ctx.SettingsSection().GetText() + stmt.Section = strings.ToLower(ctx.SettingsSection().GetText()) stmt.UpsertLanguage = ctx.ADD() != nil && ctx.OR() != nil && ctx.MODIFY() != nil stmt.AddLanguage = ctx.ADD() != nil && !stmt.UpsertLanguage stmt.ModifyLanguage = ctx.MODIFY() != nil && !stmt.UpsertLanguage @@ -93,7 +94,7 @@ func (b *Builder) ExitAlterSettingsClause(ctx *parser.AlterSettingsClauseContext collectSettingsItemOptions(ctx.SettingsItemOptions(), stmt.Properties) } else if ctx.SettingsSection() != nil { // ALTER SETTINGS MODEL|LANGUAGE|WORKFLOWS Key = Value, ... - stmt.Section = ctx.SettingsSection().GetText() + stmt.Section = strings.ToLower(ctx.SettingsSection().GetText()) for _, assignCtx := range ctx.AllSettingsAssignment() { assign, ok := assignCtx.(*parser.SettingsAssignmentContext) if !ok || assign == nil { diff --git a/mdl/visitor/visitor_settings_test.go b/mdl/visitor/visitor_settings_test.go index 071c4f3dd..bb5b7e98c 100644 --- a/mdl/visitor/visitor_settings_test.go +++ b/mdl/visitor/visitor_settings_test.go @@ -38,7 +38,7 @@ func TestAlterSettings_Model(t *testing.T) { if !ok { t.Fatalf("Expected AlterSettingsStmt, got %T", prog.Statements[0]) } - if stmt.Section != "MODEL" { + if stmt.Section != "model" { t.Errorf("Got Section %q", stmt.Section) } if stmt.Properties["DefaultLanguage"] != "en_US" { From aa91853d7d8ed4c74f7e9a9d9ea4ee7689ef26f5 Mon Sep 17 00:00:00 2001 From: Ako Date: Sun, 27 Sep 2026 19:48:48 +0000 Subject: [PATCH 06/11] docs: page action words, one spelling per keyword, lowercase keywords (R8) Skills, docs-site, mxcli syntax and the quick reference write the R8 spellings: page actions as words, `call microflow`, `error message`, `on delete`, ReferenceSet, `returns nothing`. The catalog's `show_page` reference kind is a data value and is left alone. Refs #752 Co-Authored-By: Claude Opus 5.5 --- .claude/skills/mendix/alter-page/SKILL.md | 28 +++---- .../atlas-design/reference/building-blocks.md | 2 +- .../skills/mendix/cheatsheet-errors/SKILL.md | 2 +- .../mendix/connect-rapidminer-graph/SKILL.md | 2 +- .claude/skills/mendix/create-page/SKILL.md | 6 +- .../mendix/create-page/reference/examples.md | 8 +- .../mendix/create-page/reference/widgets.md | 84 +++++++++---------- .claude/skills/mendix/custom-widgets/SKILL.md | 6 +- .claude/skills/mendix/fragments/SKILL.md | 16 ++-- .../reference/patterns.md | 28 +++---- .../generate-domain-model/reference/syntax.md | 38 ++++----- .../mendix/master-detail-pages/SKILL.md | 8 +- .claude/skills/mendix/mdl-entities/SKILL.md | 18 ++-- .../skills/mendix/migrate-k2-nintex/SKILL.md | 4 +- .../reference/walkthroughs.md | 4 +- .claude/skills/mendix/overview-pages/SKILL.md | 60 ++++++------- .../mendix/regular-expressions/SKILL.md | 14 ++-- .../resolve-forward-references/SKILL.md | 24 +++--- .claude/skills/mendix/system-module/SKILL.md | 4 +- .claude/skills/mendix/test-app/SKILL.md | 4 +- .../mendix/validation-microflows/SKILL.md | 2 +- .../skills/mendix/write-microflows/SKILL.md | 2 +- .../write-microflows/reference/integration.md | 2 +- CHANGELOG.md | 1 + cmd/mxcli/syntax/features_domain_model.go | 27 +++--- cmd/mxcli/syntax/features_integration.go | 4 +- cmd/mxcli/syntax/features_misc.go | 14 ++-- cmd/mxcli/syntax/features_page.go | 26 +++--- docs-site/src/appendixes/data-type-mapping.md | 4 +- docs-site/src/appendixes/quick-reference.md | 10 +-- docs-site/src/examples/alter-page.md | 6 +- docs-site/src/examples/crm-module.md | 12 +-- docs-site/src/examples/master-detail.md | 4 +- docs-site/src/examples/validation.md | 4 +- docs-site/src/internals/reference-tracking.md | 2 +- docs-site/src/language/alter-page.md | 2 +- docs-site/src/language/associations.md | 13 +-- docs-site/src/language/attributes.md | 10 +-- docs-site/src/language/basics.md | 16 ++++ docs-site/src/language/constraints.md | 14 ++-- docs-site/src/language/data-binding.md | 2 +- docs-site/src/language/domain-model.md | 6 +- docs-site/src/language/entities.md | 8 +- docs-site/src/language/page-patterns.md | 26 +++--- docs-site/src/language/page-structure.md | 4 +- docs-site/src/language/pages.md | 4 +- docs-site/src/language/snippets.md | 2 +- docs-site/src/language/widget-types.md | 36 ++++---- docs-site/src/migration/generation.md | 6 +- .../domain-model/create-association.md | 22 ++--- .../reference/domain-model/create-entity.md | 4 +- docs-site/src/reference/page/create-page.md | 26 +++--- .../src/reference/page/create-snippet.md | 2 +- .../reference/query/describe-association.md | 2 +- .../src/reference/query/describe-page.md | 4 +- docs-site/src/tools/theme.md | 4 +- docs-site/src/tutorial/create-entity.md | 2 +- docs-site/src/tutorial/create-page.md | 14 ++-- docs-site/src/tutorial/describe-search.md | 8 +- docs/01-project/MDL_QUICK_REFERENCE.md | 24 +++--- 60 files changed, 382 insertions(+), 359 deletions(-) diff --git a/.claude/skills/mendix/alter-page/SKILL.md b/.claude/skills/mendix/alter-page/SKILL.md index 751c31a3c..861a4fa7a 100644 --- a/.claude/skills/mendix/alter-page/SKILL.md +++ b/.claude/skills/mendix/alter-page/SKILL.md @@ -146,14 +146,14 @@ set (Documentation: 'What this page is for.') -- Retarget a button's on-click action. Any form `create page` accepts works -- here, including the combined ones. -set (Action: microflow Module.ACT_Other) on btnSave -set (Action: SAVE_CHANGES CLOSE_PAGE) on btnSave -set (Action: SHOW_PAGE Module.DetailPage) on btnEdit +set (Action: call microflow Module.ACT_Other) on btnSave +set (Action: SAVE CHANGES CLOSE PAGE) on btnSave +set (Action: SHOW PAGE Module.DetailPage) on btnEdit -- Retarget ONE named action slot of a pluggable widget, by the widget's own -- property key (the same key `create page` takes: `createFileAction: …`). -set ('createFileAction': microflow Module.ACT_CreateFile) on fileUploader1 -set ('onSelectionChange': show_page Module.Detail) on dgOrders +set ('createFileAction': call microflow Module.ACT_CreateFile) on fileUploader1 +set ('onSelectionChange': show page Module.Detail) on dgOrders -- Rebind a data-bound widget set (DataSource: $OrderParam) on dvOrder @@ -174,8 +174,8 @@ so a silent write would build cleanly and then fail to open. | Property | Widget Types | Value Type | Example | |----------|-------------|------------|---------| -| `Action` | Widgets with an on-click action (ACTIONBUTTON, LINKBUTTON, clickable containers) | Any `create page` action expression | `set (Action: microflow M.ACT_Go) on btnSave` | -| `''` | Pluggable widgets — any **action-typed** property (File Uploader `createFileAction`, DataGrid 2 `onSelectionChange`, …) | Any `create page` action expression | `set ('createFileAction': microflow M.ACT_Create) on fileUploader1` — refused, naming the widget's action slots, if the key is not action-typed | +| `Action` | Widgets with an on-click action (ACTIONBUTTON, LINKBUTTON, clickable containers) | Any `create page` action expression | `set (Action: call microflow M.ACT_Go) on btnSave` | +| `''` | Pluggable widgets — any **action-typed** property (File Uploader `createFileAction`, DataGrid 2 `onSelectionChange`, …) | Any `create page` action expression | `set ('createFileAction': call microflow M.ACT_Create) on fileUploader1` — refused, naming the widget's action slots, if the key is not action-typed | | `caption` | ACTIONBUTTON, LINKBUTTON | String | `set (caption: 'Submit') on btnSave` | | `content` | DYNAMICTEXT | String | `set (content: 'New Heading') on txtTitle` | | `label` | TEXTBOX, TEXTAREA, DATEPICKER, COMBOBOX, CHECKBOX, RADIOBUTTONS | String | `set (label: 'full Name') on txtName` | @@ -267,7 +267,7 @@ insert after txtName { -- Insert before a widget insert before btnSave { - actionbutton btnPreview (caption: 'Preview', action: microflow Module.ACT_Preview) + actionbutton btnPreview (caption: 'Preview', action: call microflow Module.ACT_Preview) } -- Insert INTO a container — append as its last child (works on an EMPTY container) @@ -313,8 +313,8 @@ Removes widgets and their entire subtree from the page. -- Replace a single widget with new content replace footer1 with { footer newFooter { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: primary) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } ``` @@ -422,9 +422,9 @@ alter page MyModule.Customer_Edit { alter page MyModule.Customer_Edit { replace footer1 with { footer newFooter { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: success) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: success) actionbutton btnDelete (caption: 'Delete', action: delete, buttonstyle: danger) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } }; @@ -436,7 +436,7 @@ alter page MyModule.Customer_Edit { alter snippet MyModule.NavigationMenu { set (caption: 'Dashboard') on btnHome; insert after btnHome { - actionbutton btnReports (caption: 'Reports', action: show_page MyModule.Reports_Overview) + actionbutton btnReports (caption: 'Reports', action: show page MyModule.Reports_Overview) } }; ``` @@ -555,7 +555,7 @@ page and bind the buttons at creation time instead of rewiring afterwards: 1. **`SET` cannot rewire a button's action.** `set` accepts a fixed property list (`caption`, `class`, `visible`, …) — `action` is not on it, so - `set Action = microflow … on btnSave` is a parse error. Set the button's action + `set Action = call microflow … on btnSave` is a parse error. Set the button's action when the button is created (or `REPLACE` the button subtree). 2. **`REPLACE` cannot reuse a widget name that lives inside the subtree being diff --git a/.claude/skills/mendix/atlas-design/reference/building-blocks.md b/.claude/skills/mendix/atlas-design/reference/building-blocks.md index f90311557..ef4f318f1 100644 --- a/.claude/skills/mendix/atlas-design/reference/building-blocks.md +++ b/.claude/skills/mendix/atlas-design/reference/building-blocks.md @@ -137,7 +137,7 @@ rebind overrides that rewrite the block's outermost datasource / first button: ```mdl use building block Atlas_Web_Content.List_Cards - (datasource: database Sales.Order, action: microflow Sales.Open) as orders_; + (datasource: database Sales.Order, action: call microflow Sales.Open) as orders_; ``` For a binding the override rule can't reach, copy the block in (`as prefix_`) and diff --git a/.claude/skills/mendix/cheatsheet-errors/SKILL.md b/.claude/skills/mendix/cheatsheet-errors/SKILL.md index 8565ba180..6b48fc51c 100644 --- a/.claude/skills/mendix/cheatsheet-errors/SKILL.md +++ b/.claude/skills/mendix/cheatsheet-errors/SKILL.md @@ -265,7 +265,7 @@ Run with `-p` for the fullest coverage. | CE0104 | Action activity is unreachable | Code after RETURN | | CE0105 | Must end with end event | Missing RETURN | | CE0117 | Error in expression | Unqualified association path | -| CE1571 | No argument selected for parameter | A microflow/nanoflow call with a parameter nothing fills — as a `datasource:` **or** an `action:`. Give it an argument (`action: nanoflow M.NF($P = $value)`), or nest the widget in a data container of the parameter's type. `check -p` reports both | +| CE1571 | No argument selected for parameter | A microflow/nanoflow call with a parameter nothing fills — as a `datasource:` **or** an `action:`. Give it an argument (`action: call nanoflow M.NF($P = $value)`), or nest the widget in a data container of the parameter's type. `check -p` reports both | | CE1571 | …in a control bar | A control bar is **not** row-scoped, so the grid's row does not fill it: pass the grid's selection (`$dgOrders`, with `Selection:` set) or move the widget into a column. `$currentObject` there is MDL-BUTTON01 | | CE1834 | The 'Page' property is required | Workflow user task without a `page` — `check` flags MDL-WF01 | | CE1876 | Single outcome must not contain activities | Single-outcome user task with a nested activity flow — `check` flags MDL-WF02 | diff --git a/.claude/skills/mendix/connect-rapidminer-graph/SKILL.md b/.claude/skills/mendix/connect-rapidminer-graph/SKILL.md index 8f947bc75..adff1ad41 100644 --- a/.claude/skills/mendix/connect-rapidminer-graph/SKILL.md +++ b/.claude/skills/mendix/connect-rapidminer-graph/SKILL.md @@ -213,7 +213,7 @@ create page MyModule.Customer_Overview ( layout: Atlas_Core.Atlas_Default ) { dynamictext heading (content: 'Customers', rendermode: H2) - actionbutton btnRefresh (caption: 'Refresh', action: microflow MyModule.ACT_RefreshCustomers, buttonstyle: primary) + actionbutton btnRefresh (caption: 'Refresh', action: call microflow MyModule.ACT_RefreshCustomers, buttonstyle: primary) datagrid gridCustomers (datasource: database MyModule.Customer sort by CustomerId asc) { column (attribute: CustomerId, caption: 'ID') column (attribute: CustomerName, caption: 'Name') diff --git a/.claude/skills/mendix/create-page/SKILL.md b/.claude/skills/mendix/create-page/SKILL.md index fb81af9fa..3f944f080 100644 --- a/.claude/skills/mendix/create-page/SKILL.md +++ b/.claude/skills/mendix/create-page/SKILL.md @@ -70,7 +70,7 @@ Both are optional and can be changed later with `alter page … { set Class = ' | Password field | `Password: true` | `textbox tbPw (attribute: Secret, Password: true)` | | Widget validation | `Validation: ''` + `ValidationMessage: ''` | `Validation: 'length(toString($value)) > 0'` — quoted, not `[bracketed]` | | Variable binding | `datasource: $Var` | `dataview dv (datasource: $Product) { ... }` | -| Action binding | `action: type` | `actionbutton btn (caption: 'Save', action: save_changes)` | +| Action binding | `action: type` | `actionbutton btn (caption: 'Save', action: save changes)` | | Database source | `datasource: database entity` | `datagrid dg (datasource: database Module.Entity)` | | Selection binding | `datasource: selection widget` | `dataview dv (datasource: selection galleryList)` | | CSS class | `class: 'classes'` | `container c (class: 'card mx-spacing-top-large')` | @@ -200,10 +200,10 @@ create page MyModule.CustomerPage } row { column (desktopwidth: 6) { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: primary) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary) } column (desktopwidth: 6) { - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } } diff --git a/.claude/skills/mendix/create-page/reference/examples.md b/.claude/skills/mendix/create-page/reference/examples.md index d700533d5..65c1b431c 100644 --- a/.claude/skills/mendix/create-page/reference/examples.md +++ b/.claude/skills/mendix/create-page/reference/examples.md @@ -28,8 +28,8 @@ create or replace page CRM.CustomerEdit checkbox cbActive (label: 'Active', attribute: IsActive) footer footer1 { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: primary) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } } @@ -97,8 +97,8 @@ create page CRM.Customer_MasterDetail textbox txtPhone (label: 'Phone', attribute: Phone) footer footer1 { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: primary) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } } diff --git a/.claude/skills/mendix/create-page/reference/widgets.md b/.claude/skills/mendix/create-page/reference/widgets.md index a2a305959..439651731 100644 --- a/.claude/skills/mendix/create-page/reference/widgets.md +++ b/.claude/skills/mendix/create-page/reference/widgets.md @@ -117,27 +117,27 @@ describe icon collection Atlas_Core.Atlas_Filled -- every icon + its reference **Action Bindings:** - `action: nothing` - Deliberately no action (a decorative button, a card that is not clickable) -- `action: save_changes` - Save changes to object -- `action: save_changes close_page` - Save and close page -- `action: cancel_changes` - Cancel changes -- `action: close_page` - Close the page +- `action: save changes` - Save changes to object +- `action: save changes close page` - Save and close page +- `action: cancel changes` - Cancel changes +- `action: close page` - Close the page - `action: delete` - Delete object -- `action: microflow Module.MicroflowName` - Call microflow -- `action: microflow Module.MicroflowName(Param: $value)` - Call microflow with parameters -- `action: nanoflow Module.NanoflowName` - Call nanoflow (client-side) -- `action: nanoflow Module.NanoflowName(Param: $value)` - Call nanoflow with parameters -- `action: nanoflow Module.NanoflowName($Param = $value)` - Also accepted (microflow-style) -- `action: open_link 'https://example.com'` - Open a fixed web address -- `action: open_link $currentObject/URL` - Open the address held in an attribute of the enclosing data container's object (inside a data container only; not over an association) +- `action: call microflow Module.MicroflowName` - Call microflow +- `action: call microflow Module.MicroflowName(Param: $value)` - Call microflow with parameters +- `action: call nanoflow Module.NanoflowName` - Call nanoflow (client-side) +- `action: call nanoflow Module.NanoflowName(Param: $value)` - Call nanoflow with parameters +- `action: call nanoflow Module.NanoflowName($Param = $value)` - Also accepted (microflow-style) +- `action: open link 'https://example.com'` - Open a fixed web address +- `action: open link $currentObject/URL` - Open the address held in an attribute of the enclosing data container's object (inside a data container only; not over an association) - **Every parameter needs an argument, or an enclosing data container of its type.** A flow called with a parameter nothing fills is **CE1571**; `mxcli check -p` reports it. This is the same on every widget that takes an action, a clickable `container` included. -- `action: show_page Module.PageName` - Navigate to page -- `action: show_page Module.PageName(Param: $value)` - Navigate with parameters -- `action: show_page Module.PageName($Param = $value)` - Also accepted (microflow-style) -- `action: create_object Module.Entity then show_page Module.PageName` - Create and navigate -- **A `show_page` argument must be the context object, and there has to BE one.** +- `action: show page Module.PageName` - Navigate to page +- `action: show page Module.PageName(Param: $value)` - Navigate with parameters +- `action: show page Module.PageName($Param = $value)` - Also accepted (microflow-style) +- `action: create object Module.Entity then show page Module.PageName` - Create and navigate +- **A `show page` argument must be the context object, and there has to BE one.** Mendix takes the page argument from the enclosing data widget, so the only spellings that mean anything are `$currentObject` or the name of the variable that widget is bound to (`datasource: $Customer` → `(Customer: $Customer)` is @@ -153,8 +153,8 @@ describe icon collection Atlas_Core.Atlas_Filled -- every icon + its reference button, call a microflow that does `show page Module.Page(Param: $value)` — that path wires the arguments properly. - **The list above is the whole vocabulary, and a keyword without its argument is - not in it.** `action: open_link` with no URL, `action: show_page` with no page, - `action: microflow` with no name — each is **MDL-WIDGET28**. Until + not in it.** `action: open link` with no URL, `action: show page` with no page, + `action: call microflow` with no name — each is **MDL-WIDGET28**. Until mendixlabs/mxcli#1062 these were written as a widget with *no action at all*: it rendered, carried its caption, and did nothing, while `mxcli check`, `exec` and mxbuild all reported success, because a no-action widget is legal Mendix. @@ -170,22 +170,22 @@ describe icon collection Atlas_Core.Atlas_Filled -- every icon + its reference **Examples:** ```sql -- Save with style -actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: primary) +actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary) -- Navigate with parameter (inside DATAVIEW) -actionbutton btnEdit (caption: 'Edit', action: show_page Module.EditPage(Product: $Product)) +actionbutton btnEdit (caption: 'Edit', action: show page Module.EditPage(Product: $Product)) -- Navigate with $currentObject (inside DATAGRID column) -actionbutton btnEdit (caption: 'Edit', action: show_page Module.EditPage(Product: $currentObject)) +actionbutton btnEdit (caption: 'Edit', action: show page Module.EditPage(Product: $currentObject)) -- Call microflow with page/dataview parameter -actionbutton btnProcess (caption: 'Process', action: microflow Module.ACT_Process(Order: $Order), buttonstyle: success) +actionbutton btnProcess (caption: 'Process', action: call microflow Module.ACT_Process(Order: $Order), buttonstyle: success) -- Call microflow with $currentObject (inside DATAGRID/LISTVIEW column) -actionbutton btnDelete (caption: 'Delete', action: microflow Module.ACT_Delete(Target: $currentObject), buttonstyle: danger) +actionbutton btnDelete (caption: 'Delete', action: call microflow Module.ACT_Delete(Target: $currentObject), buttonstyle: danger) -- Create object and show page -actionbutton btnNew (caption: 'New', action: create_object Module.Product then show_page Module.Product_Edit, buttonstyle: primary) +actionbutton btnNew (caption: 'New', action: create object Module.Product then show page Module.Product_Edit, buttonstyle: primary) ``` **Using `$currentObject`:** @@ -368,7 +368,7 @@ correctly on the default engine (mxbuild-verified, 0 errors) — an earlier CE04 ```sql column (caption: 'Actions') { - actionbutton btnView (caption: 'View', action: close_page) + actionbutton btnView (caption: 'View', action: close page) } ``` @@ -448,8 +448,8 @@ dataview dvName (datasource: $VariableName) { textarea txtDescription (label: 'Description', attribute: description) footer footer1 { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: primary) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } ``` @@ -508,8 +508,8 @@ dataview dataView1 (datasource: $Customer) { datepicker dpCreated (label: 'Created', attribute: CreateDate) footer footer1 { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: primary) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } ``` @@ -645,9 +645,9 @@ Create a menu with action items: ```sql navigationlist navName { - item itemEdit (caption: 'Edit', action: show_page Module.EditPage(entity: $EntityParameter)) + item itemEdit (caption: 'Edit', action: show page Module.EditPage(entity: $EntityParameter)) item itemDelete (caption: 'Delete', action: delete) - item itemBack (caption: 'Back', action: close_page) + item itemBack (caption: 'Back', action: close page) } ``` @@ -843,7 +843,7 @@ conditional visibility — or put the condition in the microflow it calls: ```sql actionbutton btnSubmit ( - caption: 'Submit', action: microflow Mod.ACT_Submit, + caption: 'Submit', action: call microflow Mod.ACT_Submit, visible: [$currentObject/Status = Mod.Status.Draft] ) ``` @@ -885,13 +885,13 @@ Two shapes, two remedies: ```sql -- WRONG: silently does nothing -dataview dvOrder (datasource: microflow Mod.DS_Order, onclick: show_page Mod.Detail) { +dataview dvOrder (datasource: microflow Mod.DS_Order, onclick: show page Mod.Detail) { dynamictext t (content: 'Open') } -- RIGHT: the container carries the click dataview dvOrder (datasource: microflow Mod.DS_Order) { - container clickable (onclick: show_page Mod.Detail) { + container clickable (onclick: show page Mod.Detail) { dynamictext t (content: 'Open') } } @@ -927,10 +927,10 @@ customcontainer outer1 (class: 'section') { **Clickable container (On click action).** A container can trigger an action when clicked — use `OnClick:` (or the equivalent `Action:` keyword) with any client -action (`microflow`, `nanoflow`, `show_page`, `save_changes`, …): +action (`microflow`, `nanoflow`, `show page`, `save changes`, …): ```sql -container card1 (OnClick: microflow MyModule.ACT_OpenDetails, class: 'clickable-card') { +container card1 (OnClick: call microflow MyModule.ACT_OpenDetails, class: 'clickable-card') { dynamictext title (content: 'Open details') } ``` @@ -947,7 +947,7 @@ as an `actionbutton`'s `action:`: ```sql -- Rich, parameterised trigger: a card that opens the object it represents -container tileCard (OnClick: microflow MyModule.ACT_Open(Item: $currentObject), class: 'tile') { +container tileCard (OnClick: call microflow MyModule.ACT_Open(Item: $currentObject), class: 'tile') { dynamictext tileValue (content: '4') dynamictext tileLabel (content: '4 LEFT', class: 'tile-label') } @@ -964,8 +964,8 @@ Container for form action buttons: ```sql footer footerName { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: primary) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } ``` @@ -985,7 +985,7 @@ Control bar for data widgets: ```sql controlbar { - actionbutton btnNew (caption: 'New', action: create_object Module.Entity then show_page Module.EditPage, buttonstyle: primary) + actionbutton btnNew (caption: 'New', action: create object Module.Entity then show page Module.EditPage, buttonstyle: primary) } ``` @@ -1004,7 +1004,7 @@ datagrid dgMaterials ( -- Row-scoped: the grid's row supplies the parameter, no argument needed. column (caption: 'Row') { - container cRowUnlink (action: nanoflow Module.ACT_UnLink, class: 'command') { + container cRowUnlink (action: call nanoflow Module.ACT_UnLink, class: 'command') { actionbutton btnRowUnlink (caption: 'Unlink') } } @@ -1013,7 +1013,7 @@ datagrid dgMaterials ( -- Not row-scoped: pass the selection explicitly. container cUnlink ( class: 'command', - action: nanoflow Module.ACT_UnLink ($Material = $dgMaterials) + action: call nanoflow Module.ACT_UnLink ($Material = $dgMaterials) ) { actionbutton btnUnlink (caption: 'Unlink') } diff --git a/.claude/skills/mendix/custom-widgets/SKILL.md b/.claude/skills/mendix/custom-widgets/SKILL.md index 192b7c046..bf0bd3371 100644 --- a/.claude/skills/mendix/custom-widgets/SKILL.md +++ b/.claude/skills/mendix/custom-widgets/SKILL.md @@ -368,8 +368,8 @@ object-list item mappings use: overlap with `dataSourceExprV3` and the datasource alternative has to win, or a chart series' `staticDataSource: microflow M.X` would become an action. The executor converts them, because the widget definition is the only layer that -knows the slot is action-typed. Every other action form (`show_page`, -`save_changes`, …) reaches the AST as an action directly. +knows the slot is action-typed. Every other action form (`show page`, +`save changes`, …) reaches the AST as an action directly. **A slot may be conditional, and writing into a pruned one is CE0463.** DataGrid 2's `onSelectionChange` is *hidden when `itemSelection` = None*, so it needs @@ -477,7 +477,7 @@ widgets take a different, simpler path than the MPR writer: selection, datasource, widgets (child slots), object lists, expression, texttemplate (including `{AttrName}` placeholders and `Params` / `contentparams` bindings -> template parameters), - and action (`microflow Module.Flow`, `show_page Module.Page`, or none). + and action (`microflow Module.Flow`, `show page Module.Page`, or none). - **Rejected loudly** (widget refused, nothing sent): actions *with argument mappings*, other action kinds (save/cancel/close/delete/create/open-link/ nanoflow), and any operation the MCP builder does not translate. The error diff --git a/.claude/skills/mendix/fragments/SKILL.md b/.claude/skills/mendix/fragments/SKILL.md index 2846dff2e..5e592cf50 100644 --- a/.claude/skills/mendix/fragments/SKILL.md +++ b/.claude/skills/mendix/fragments/SKILL.md @@ -29,8 +29,8 @@ Fragments are **script-scoped, transient** widget groups: ```mdl define fragment SaveCancelFooter as { footer footer1 { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: primary) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } }; ``` @@ -135,7 +135,7 @@ create page Module.Orders (title: 'Orders', layout: Atlas_Core.Atlas_Default) { ``` Rules: -- Param kinds are `datasource` (`$var` / `database E` / `$currentObject/Assoc` / `microflow M`) and `action` (a microflow / nanoflow / `save_changes` / `show_page` / …). +- Param kinds are `datasource` (`$var` / `database E` / `$currentObject/Assoc` / `microflow M`) and `action` (a microflow / nanoflow / `save changes` / `show page` / …). - Every declared parameter must be supplied; unknown args and type mismatches are errors. - Values substitute at expansion — `describe page` shows the concrete datasource/action, no `$param`. @@ -145,7 +145,7 @@ outermost datasource and/or its first button after the copy: ```mdl use building block Atlas_Web_Content.List_Cards - (datasource: database Module.Order, action: microflow Module.OpenOrder) as orders_; + (datasource: database Module.Order, action: call microflow Module.OpenOrder) as orders_; ``` Binding-point rule (prototype): datasource → the first widget carrying a @@ -173,8 +173,8 @@ describe fragment SaveCancelFooter; ```mdl define fragment CrudFooter as { footer footer1 { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: primary) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } }; @@ -218,8 +218,8 @@ create page Module.Customer_Edit (...) { ```mdl define fragment ActionButtons as { - actionbutton btnApprove (caption: 'Approve', action: save_changes, buttonstyle: success) - actionbutton btnReject (caption: 'Reject', action: cancel_changes, buttonstyle: danger) + actionbutton btnApprove (caption: 'Approve', action: save changes, buttonstyle: success) + actionbutton btnReject (caption: 'Reject', action: cancel changes, buttonstyle: danger) }; create page Module.DualPanel (...) { diff --git a/.claude/skills/mendix/generate-domain-model/reference/patterns.md b/.claude/skills/mendix/generate-domain-model/reference/patterns.md index fc2f87f6a..209e471e3 100644 --- a/.claude/skills/mendix/generate-domain-model/reference/patterns.md +++ b/.claude/skills/mendix/generate-domain-model/reference/patterns.md @@ -45,13 +45,13 @@ create enumeration ECommerce.OrderStatus ( @position(50, 50) create persistent entity ECommerce.Customer ( /** Unique customer identifier */ - CustomerId: long not null error 'Customer ID is required' unique error 'Customer ID must be unique', + CustomerId: long not null error message 'Customer ID is required' unique error message 'Customer ID must be unique', /** Customer full name */ - FullName: string(200) not null error 'Full name is required', + FullName: string(200) not null error message 'Full name is required', /** Email address */ - Email: string(200) not null error 'Email is required' unique error 'Email must be unique', + Email: string(200) not null error message 'Email is required' unique error message 'Email must be unique', /** Registration date */ - RegistrationDate: datetime not null error 'Registration date is required' + RegistrationDate: datetime not null error message 'Registration date is required' ); /** @@ -64,15 +64,15 @@ create persistent entity ECommerce.Customer ( @position(50, 250) create persistent entity ECommerce.Product ( /** Unique product identifier */ - ProductId: long not null error 'Product ID is required' unique error 'Product ID must be unique', + ProductId: long not null error message 'Product ID is required' unique error message 'Product ID must be unique', /** Product name */ - ProductName: string(200) not null error 'Product name is required', + ProductName: string(200) not null error message 'Product name is required', /** Product SKU */ - SKU: string(50) not null error 'SKU is required' unique error 'SKU must be unique', + SKU: string(50) not null error message 'SKU is required' unique error message 'SKU must be unique', /** Unit price */ - Price: decimal not null error 'Price is required', + Price: decimal not null error message 'Price is required', /** Stock quantity */ - StockQuantity: integer not null error 'Stock quantity is required' + StockQuantity: integer not null error message 'Stock quantity is required' ); /** @@ -85,15 +85,15 @@ create persistent entity ECommerce.Product ( @position(300, 150) create persistent entity ECommerce.SalesOrder ( /** Unique order identifier */ - OrderId: long not null error 'Order ID is required' unique error 'Order ID must be unique', + OrderId: long not null error message 'Order ID is required' unique error message 'Order ID must be unique', /** Order number */ - OrderNumber: string(50) not null error 'Order number is required' unique error 'Order number must be unique', + OrderNumber: string(50) not null error message 'Order number is required' unique error message 'Order number must be unique', /** Order date */ - OrderDate: datetime not null error 'Order date is required', + OrderDate: datetime not null error message 'Order date is required', /** Total amount */ - TotalAmount: decimal not null error 'Total amount is required', + TotalAmount: decimal not null error message 'Total amount is required', /** Order status */ - status: enumeration(ECommerce.OrderStatus) not null error 'Status is required' + status: enumeration(ECommerce.OrderStatus) not null error message 'Status is required' ); -- Associations diff --git a/.claude/skills/mendix/generate-domain-model/reference/syntax.md b/.claude/skills/mendix/generate-domain-model/reference/syntax.md index e3a444552..8c3247d7b 100644 --- a/.claude/skills/mendix/generate-domain-model/reference/syntax.md +++ b/.claude/skills/mendix/generate-domain-model/reference/syntax.md @@ -180,17 +180,17 @@ Cross-module associations have no anchors at all — Mendix stores none, and @position(100, 100) create persistent entity Module.EntityName ( /** Unique identifier */ - Id: long not null error 'ID is required' unique error 'ID must be unique', + Id: long not null error message 'ID is required' unique error message 'ID must be unique', /** Attribute description */ - attributename: string(200) not null error 'Attribute name is required', + attributename: string(200) not null error message 'Attribute name is required', /** Numeric value */ Amount: decimal, /** Date field (there is no date-only type) */ CreationDate: datetime, /** Boolean flag */ - IsActive: boolean not null error 'IsActive flag is required' default true, + IsActive: boolean not null error message 'IsActive flag is required' default true, /** Enumeration field */ - status: enumeration(Module.StatusEnum) not null error 'Status is required' + status: enumeration(Module.StatusEnum) not null error message 'Status is required' ); ``` @@ -474,7 +474,7 @@ create association Module.EntityWithFK_ReferencedEntity from Module.EntityWithFK to Module.ReferencedEntity type reference owner default -delete_behavior DELETE_BUT_KEEP_REFERENCES +on delete set null comment 'Additional documentation'; ``` @@ -520,12 +520,10 @@ both is refused as **MDL067**. > to-many collection and a list widget works. See master-detail-pages for the > widget patterns. -**Delete Behaviors**: -- `DELETE_AND_REFERENCES` - Delete object and all referencing objects -- `DELETE_BUT_KEEP_REFERENCES` - Delete object, keep references (nullify) -- `DELETE_IF_NO_REFERENCES` - Only delete if no objects reference it -- `cascade` - Cascade delete to associated objects -- `prevent` - Prevent deletion if references exist +**Delete Behaviors** (`on delete …`; the old `delete_behavior …` clause is deprecated, MDL-DEPR022): +- `on delete cascade` - Delete object and all referencing objects +- `on delete set null` - Delete object, keep references (nullify) — the default +- `on delete restrict [error message '…']` - Only delete if no objects reference it **Naming Convention**: `{FromEntity}_{ToEntity}` (e.g., `Order_Customer`, `Transaction_Account`) @@ -603,13 +601,13 @@ Each constraint can have a custom error message using `error 'message'` syntax: ```sql create persistent entity Module.Customer ( /** Customer name - required with custom error */ - Name: string(200) not null error 'Name is required', + Name: string(200) not null error message 'Name is required', /** Email - required and unique with separate error messages */ - Email: string(200) not null error 'Email is required' unique error 'Email must be unique', + Email: string(200) not null error message 'Email is required' unique error message 'Email must be unique', /** Age with default value */ Age: integer default 0, /** Active status flag */ - IsActive: boolean not null error 'IsActive flag is required' default true + IsActive: boolean not null error message 'IsActive flag is required' default true ); ``` @@ -617,23 +615,23 @@ create persistent entity Module.Customer ( - Place `error 'message'` immediately after the constraint - Multiple constraints can each have their own error message - Keep messages clear and user-friendly -- Follow the pattern: `not null error 'X is required'` for required fields -- For UNIQUE: `unique error 'X must be unique'` +- Follow the pattern: `not null error message 'X is required'` for required fields +- For UNIQUE: `unique error message 'X must be unique'` - Error messages are shown to end users during validation **Common patterns:** ```sql -- Required field -Name: string(200) not null error 'Name is required', +Name: string(200) not null error message 'Name is required', -- Required and unique -Email: string(200) not null error 'Email is required' unique error 'Email must be unique', +Email: string(200) not null error message 'Email is required' unique error message 'Email must be unique', -- Required with default -IsActive: boolean not null error 'IsActive flag is required' default true, +IsActive: boolean not null error message 'IsActive flag is required' default true, -- Enum with required error -status: enumeration(Module.Status) not null error 'Status is required', +status: enumeration(Module.Status) not null error message 'Status is required', -- Enum with default value (use fully qualified Module.Enum.Value) Priority: enumeration(Module.Priority) default Module.Priority.Normal diff --git a/.claude/skills/mendix/master-detail-pages/SKILL.md b/.claude/skills/mendix/master-detail-pages/SKILL.md index 330a55280..f60378fef 100644 --- a/.claude/skills/mendix/master-detail-pages/SKILL.md +++ b/.claude/skills/mendix/master-detail-pages/SKILL.md @@ -39,7 +39,7 @@ create page Module.Entity_MasterDetail textbox txtName (label: 'Name', attribute: Name) footer footer1 { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: success) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: success) } } } @@ -124,8 +124,8 @@ create page CRM.Customer_MasterDetail textbox txtPhone (label: 'Phone', attribute: Phone) footer footer1 { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: success) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: success) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } } @@ -172,7 +172,7 @@ template template1 { | Where filter | `datasource: database from entity where [IsActive = true]` | | Selection mode | `selection: single` | | Attribute binding | `attribute: attributename` | -| Action binding | `action: save_changes` | +| Action binding | `action: save changes` | | Button style | `buttonstyle: success` | | Text content | `content: 'text'` with `contentparams: [{1} = attr]` | | Render mode | `rendermode: H4` | diff --git a/.claude/skills/mendix/mdl-entities/SKILL.md b/.claude/skills/mendix/mdl-entities/SKILL.md index b8a8912ab..8491cb05d 100644 --- a/.claude/skills/mendix/mdl-entities/SKILL.md +++ b/.claude/skills/mendix/mdl-entities/SKILL.md @@ -164,7 +164,7 @@ the entity being referenced (the "one" / parent side). Name convention is `Child */ create association Module.Product_Category from Module.Product to Module.Category -type reference_set +type ReferenceSet owner both; / ``` @@ -178,14 +178,14 @@ owner both; create association Module.Order_Customer from Module.Order to Module.Customer type reference -delete_behavior DELETE_AND_REFERENCES; +on delete cascade; / ``` Delete behaviors (applied to the referenced `to` entity): -- `delete_behavior DELETE_AND_REFERENCES` - delete the referencing objects too (cascade) -- `delete_behavior DELETE_BUT_KEEP_REFERENCES` - delete, nullify the reference (default) -- `delete_behavior DELETE_IF_NO_REFERENCES` - only delete when nothing references it +- `on delete cascade` - delete the referencing objects too (cascade) +- `on delete set null` - delete, nullify the reference (default) +- `on delete restrict` - only delete when nothing references it ## Enumerations @@ -330,7 +330,7 @@ type reference; create association Shop.OrderLine_Order from Shop.OrderLine to Shop.Order type reference -delete_behavior DELETE_AND_REFERENCES; +on delete cascade; / create association Shop.OrderLine_Product @@ -347,7 +347,7 @@ not by re-running `describe` output: ```mdl alter entity Shop.Order add attribute Note: string(200); -alter association Shop.Order_Customer set delete_behavior DELETE_BUT_KEEP_REFERENCES; +alter association Shop.Order_Customer set on delete set null; alter enumeration Shop.OrderStatus add value Cancelled caption 'Cancelled'; ``` @@ -374,10 +374,10 @@ attributename: type [(length)] [not null] [unique] [default value] ```mdl create association Module.Child_Parent from Module.ChildEntity to Module.ParentEntity -[type reference | reference_set] +[type reference | ReferenceSet] [owner default | both] [storage column | table] -[delete_behavior DELETE_AND_REFERENCES | DELETE_BUT_KEEP_REFERENCES | DELETE_IF_NO_REFERENCES]; +[on delete cascade | restrict | set null [error message '...']]; ``` ### Enumeration Syntax diff --git a/.claude/skills/mendix/migrate-k2-nintex/SKILL.md b/.claude/skills/mendix/migrate-k2-nintex/SKILL.md index 502a4d903..62232722b 100644 --- a/.claude/skills/mendix/migrate-k2-nintex/SKILL.md +++ b/.claude/skills/mendix/migrate-k2-nintex/SKILL.md @@ -197,8 +197,8 @@ create page CRM.Customer_Edit -- Button bar (SmartForms action buttons) footer footer1 { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: primary) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } } diff --git a/.claude/skills/mendix/odata-data-sharing/reference/walkthroughs.md b/.claude/skills/mendix/odata-data-sharing/reference/walkthroughs.md index 424564714..fa17d592a 100644 --- a/.claude/skills/mendix/odata-data-sharing/reference/walkthroughs.md +++ b/.claude/skills/mendix/odata-data-sharing/reference/walkthroughs.md @@ -406,7 +406,7 @@ an invoice number, an ISIN, an employee number. Mendix requires a key to be unique, required and stable (the last is the point here), and the unique validation rule it makes you add is checking exactly that. -That is also why the key needs `unique error '…'` on the attribute — see the +That is also why the key needs `unique error message '…'` on the attribute — see the CE6624 note below. Both halves of the same idea: the value identifies one row, and keeps identifying it. @@ -526,7 +526,7 @@ Measured on Mendix 11.13, each row a separate build: | shape | result | |---|---| | single key attribute, persistable, no `unique` rule | **CE6624** — add one | -| single key attribute, persistable, `unique error '…'` | 0 errors | +| single key attribute, persistable, `unique error message '…'` | 0 errors | | **single key attribute, VIEW entity, no `unique` rule** | **0 errors** | | **composite key, `OData3`** | **CE7238** "You can only have more than one key attribute when the OData version is 4" | | composite key, `OData4`, persistable, no `unique` rules | 0 errors | diff --git a/.claude/skills/mendix/overview-pages/SKILL.md b/.claude/skills/mendix/overview-pages/SKILL.md index 2fdc531dd..c76b4bc2c 100644 --- a/.claude/skills/mendix/overview-pages/SKILL.md +++ b/.claude/skills/mendix/overview-pages/SKILL.md @@ -29,9 +29,9 @@ Create a reusable navigation snippet using NAVIGATIONLIST for vertical sidebar m create snippet Module.Entity_Menu { navigationlist navMenu { - item itemCustomers (caption: 'Customers', action: show_page Module.Customer_Overview) - item itemOrders (caption: 'Orders', action: show_page Module.Order_Overview) - item itemProducts (caption: 'Products', action: show_page Module.Product_Overview) + item itemCustomers (caption: 'Customers', action: show page Module.Customer_Overview) + item itemOrders (caption: 'Orders', action: show page Module.Order_Overview) + item itemProducts (caption: 'Products', action: show page Module.Product_Overview) } } ``` @@ -55,9 +55,9 @@ The NAVIGATIONLIST widget creates a vertical menu with navigation items: ```sql navigationlist widgetName { - item itemName (caption: 'Caption', action: show_page Module.PageName) - item itemName (caption: 'Caption', action: microflow Module.MicroflowName) - item itemName (caption: 'Caption', action: close_page) + item itemName (caption: 'Caption', action: show page Module.PageName) + item itemName (caption: 'Caption', action: call microflow Module.MicroflowName) + item itemName (caption: 'Caption', action: close page) } ``` @@ -105,7 +105,7 @@ create page Module.Entity_Overview textfilter textFilter2 } column (caption: 'Actions') { - actionbutton btnEdit (caption: 'Edit', action: show_page Module.Entity_NewEdit("entity": $currentObject)) + actionbutton btnEdit (caption: 'Edit', action: show page Module.Entity_NewEdit("entity": $currentObject)) actionbutton btnDelete (caption: 'Delete', action: delete, buttonstyle: danger) } } @@ -147,7 +147,7 @@ datagrid GridName ( textfilter textFilter1 } column (caption: 'Actions') { - actionbutton btnEdit (caption: 'Edit', action: show_page Module.Entity_NewEdit("entity": $currentObject)) + actionbutton btnEdit (caption: 'Edit', action: show page Module.Entity_NewEdit("entity": $currentObject)) } } ``` @@ -253,8 +253,8 @@ create page Module.Entity_NewEdit combobox cbStatus (label: 'Status', attribute: status) footer footer1 { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: success) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: success) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } } @@ -276,8 +276,8 @@ create page Module.PageName - Parameter name conventionally matches the entity name (e.g., `$store`, `$Customer`) - The DataView's binding references this parameter (`datasource: $ParamName`) -- When calling the page via SHOW_PAGE, pass the object by parameter name: `show_page Module.PageName(ParamName: $value)` -- **Reserved-word parameter names must be quoted in the SHOW_PAGE args.** The generic examples above name the parameter `entity`, which is a reserved keyword, so the call quotes it: `show_page Module.Entity_NewEdit("entity": $currentObject)`. A non-reserved name (`$store`, `$Customer`) needs no quotes: `show_page Module.Store_NewEdit(store: $currentObject)`. +- When calling the page via SHOW PAGE, pass the object by parameter name: `show page Module.PageName(ParamName: $value)` +- **Reserved-word parameter names must be quoted in the SHOW PAGE args.** The generic examples above name the parameter `entity`, which is a reserved keyword, so the call quotes it: `show page Module.Entity_NewEdit("entity": $currentObject)`. A non-reserved name (`$store`, `$Customer`) needs no quotes: `show page Module.Store_NewEdit(store: $currentObject)`. ### NewEdit Page Components @@ -299,8 +299,8 @@ create snippet MdlTemplates.NavigationMenu layoutgrid navGrid { row { column (desktopwidth: 12) { - actionbutton btnStores (caption: 'Stores', action: show_page MdlTemplates.Store_Overview) - actionbutton btnCars (caption: 'Cars', action: show_page MdlTemplates.Car_Overview) + actionbutton btnStores (caption: 'Stores', action: show page MdlTemplates.Store_Overview) + actionbutton btnCars (caption: 'Cars', action: show page MdlTemplates.Car_Overview) } } } @@ -368,8 +368,8 @@ create page MdlTemplates.Store_NewEdit textbox txtLocation (label: 'Location', attribute: Location) footer footer1 { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: success) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: success) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } } @@ -425,8 +425,8 @@ create page MdlTemplates.Car_NewEdit radiobuttons rbType (label: 'Car type', attribute: CarType) footer footer1 { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: success) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: success) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } } @@ -535,9 +535,9 @@ create snippet Module.Entity_Menu ) { navigationlist EntityMenuNav { - item itemEdit (caption: 'Edit', action: show_page Module.Entity_NewEdit("entity": $EntityParameter)) + item itemEdit (caption: 'Edit', action: show page Module.Entity_NewEdit("entity": $EntityParameter)) item itemDelete (caption: 'Delete', action: delete) - item itemBack (caption: 'Back', action: close_page) + item itemBack (caption: 'Back', action: close page) } } ``` @@ -551,15 +551,15 @@ navigationlist widgetName { ``` **Supported Actions:** -- `action: save_changes` - Save changes -- `action: cancel_changes` - Cancel changes -- `action: close_page` - Close current page +- `action: save changes` - Save changes +- `action: cancel changes` - Cancel changes +- `action: close page` - Close current page - `action: delete` - Delete object -- `action: microflow Module.MicroflowName` - Call microflow -- `action: microflow Module.MicroflowName(Param: $value)` - Call microflow with parameters -- `action: show_page Module.PageName` - Navigate to page -- `action: show_page Module.PageName(Param: $value)` - Navigate with parameters -- **A `show_page` argument must be the context object, and there has to BE one.** +- `action: call microflow Module.MicroflowName` - Call microflow +- `action: call microflow Module.MicroflowName(Param: $value)` - Call microflow with parameters +- `action: show page Module.PageName` - Navigate to page +- `action: show page Module.PageName(Param: $value)` - Navigate with parameters +- **A `show page` argument must be the context object, and there has to BE one.** Mendix takes the page argument from the enclosing data widget, so the only spellings that mean anything are `$currentObject` or the name of the variable that widget is bound to (`datasource: $Customer` → `(Customer: $Customer)` is @@ -577,7 +577,7 @@ navigationlist widgetName { ## Handling Circular Dependencies -When a navigation snippet references pages (via `show_page`) and those pages reference the snippet (via `snippetcall`), you have a circular dependency. Use the **placeholder pattern**: +When a navigation snippet references pages (via `show page`) and those pages reference the snippet (via `snippetcall`), you have a circular dependency. Use the **placeholder pattern**: ### Creation Order @@ -631,7 +631,7 @@ create or modify snippet Module.NavigationMenu layoutgrid navGrid { row { column (desktopwidth: 12) { - actionbutton btnCustomers (caption: 'Customers', action: show_page Module.Customer_Overview) + actionbutton btnCustomers (caption: 'Customers', action: show page Module.Customer_Overview) } } } diff --git a/.claude/skills/mendix/regular-expressions/SKILL.md b/.claude/skills/mendix/regular-expressions/SKILL.md index c115e28f4..0f70dd884 100644 --- a/.claude/skills/mendix/regular-expressions/SKILL.md +++ b/.claude/skills/mendix/regular-expressions/SKILL.md @@ -73,7 +73,7 @@ create regular expression Val.EmailAddress ( create validation rule for Val.Person.Email regex Val.EmailAddress - feedback 'Enter a valid email address'; + error message 'Enter a valid email address'; ``` Create the pattern first. The rule stores a **reference by qualified name**, so @@ -84,9 +84,9 @@ Ranges use the same statement. Bounds are inclusive and either may be omitted; Mendix has no strict `<` or `>`, so there is no exclusive form: ```sql -create validation rule for Val.Booking.Guests range from 1 to 100 feedback '…'; -create validation rule for Val.Product.Price range from 0 feedback '…'; -create validation rule for Val.Order.Discount range to 100 feedback '…'; +create validation rule for Val.Booking.Guests range from 1 to 100 error message '…'; +create validation rule for Val.Product.Price range from 0 error message '…'; +create validation rule for Val.Order.Discount range to 100 error message '…'; ``` Re-running a rule replaces the one of the **same type** on that attribute and @@ -98,12 +98,12 @@ constraints: ```sql create entity Val.Person ( - Email: String(200) not null error 'Email is required', - Code: String(20) unique error 'Code must be unique' + Email: String(200) not null error message 'Email is required', + Code: String(20) unique error message 'Code must be unique' ); alter entity Val.Person modify attribute Email String(200) - not null error 'Email is required'; + not null error message 'Email is required'; ``` ### What still cannot be authored diff --git a/.claude/skills/mendix/resolve-forward-references/SKILL.md b/.claude/skills/mendix/resolve-forward-references/SKILL.md index e72f1f00e..ee9320819 100644 --- a/.claude/skills/mendix/resolve-forward-references/SKILL.md +++ b/.claude/skills/mendix/resolve-forward-references/SKILL.md @@ -22,7 +22,7 @@ This applies to the following reference types: | Reference | In | Fails when | |---|---|---| | `snippetcall` | page / snippet | snippet created after the page | -| `show_page` in action | page / snippet | page created after the page that references it | +| `show page` in action | page / snippet | page created after the page that references it | | `SHOW PAGE` | microflow | page created after the microflow | | `call microflow` | microflow | callee microflow created after the caller (in the same script, `exec` resolves the call against the project/backend, not later same-script definitions — so order the callee first) | @@ -49,7 +49,7 @@ The standard workaround is a three-step sequence: ## Pattern 1 — Shared Navigation Snippet (most common) -A navigation snippet contains `show_page` buttons (references pages) and pages include +A navigation snippet contains `show page` buttons (references pages) and pages include the snippet via `snippetcall` (references the snippet). Both sides reference each other. ```sql @@ -100,7 +100,7 @@ create page MyModule.Order_Overview } / --- Step 3: fill in the snippet with real content (pages now exist → show_page resolves OK) +-- Step 3: fill in the snippet with real content (pages now exist → show page resolves OK) -- Use CREATE OR MODIFY (preserves ID) or ALTER SNIPPET (in-place) create or modify snippet MyModule.NavMenu ( @@ -110,11 +110,11 @@ create or modify snippet MyModule.NavMenu layoutgrid g { row r { column c (desktopwidth: 12) { actionbutton btnCustomers ( caption: 'Customers', - action: show_page MyModule.Customer_Overview + action: show page MyModule.Customer_Overview ) actionbutton btnOrders ( caption: 'Orders', - action: show_page MyModule.Order_Overview + action: show page MyModule.Order_Overview ) }}} } @@ -125,7 +125,7 @@ create or modify snippet MyModule.NavMenu ## Pattern 2 — Page References Another Page (new/edit from overview) -An overview page has a New button that opens a NewEdit page via `show_page`. The NewEdit +An overview page has a New button that opens a NewEdit page via `show page`. The NewEdit page must exist before the overview can reference it. ```sql @@ -142,8 +142,8 @@ create page MyModule.Customer_NewEdit dataview dv (datasource: $Customer) { textbox txtName (label: 'Name', attribute: Name) } - actionbutton btnSave (caption: 'Save', action: save_changes) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnSave (caption: 'Save', action: save changes) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) }}} } / @@ -158,7 +158,7 @@ create page MyModule.Customer_Overview layoutgrid g { row { column (desktopwidth: 12) { actionbutton btnNew ( caption: 'New', - action: microflow MyModule.ACT_Customer_New + action: call microflow MyModule.ACT_Customer_New ) datagrid dg (datasource: database MyModule.Customer) { column (caption: 'Name', attribute: Name) @@ -240,7 +240,7 @@ alter snippet MyModule.NavMenu replace activity loading with actionbutton btnCustomers ( caption: 'Customers', - action: show_page MyModule.Customer_Overview + action: show page MyModule.Customer_Overview ); ``` @@ -301,7 +301,7 @@ create or modify snippet MyModule.AppNav layoutgrid g { row r { column c (desktopwidth: 12) { actionbutton btnCustomers ( caption: 'Customers', - action: show_page MyModule.Customer_Overview + action: show page MyModule.Customer_Overview ) }}} } @@ -321,7 +321,7 @@ end; -- 6. Navigation (pages already exist) alter navigation Responsive insert menu item 'Customers' - action show_page MyModule.Customer_Overview; + action show page MyModule.Customer_Overview; ``` --- diff --git a/.claude/skills/mendix/system-module/SKILL.md b/.claude/skills/mendix/system-module/SKILL.md index eee046dae..206e524ee 100644 --- a/.claude/skills/mendix/system-module/SKILL.md +++ b/.claude/skills/mendix/system-module/SKILL.md @@ -220,7 +220,7 @@ create persistent entity MyModule.Attachment extends System.FileDocument ( create association MyModule.Order_Attachments from MyModule.Order to MyModule.Attachment -type reference_set; +type ReferenceSet; ``` ### System.Image @@ -475,7 +475,7 @@ create persistent entity MyModule.Attachment extends System.FileDocument ( create association MyModule.Order_Attachments from MyModule.Order to MyModule.Attachment -type reference_set; +type ReferenceSet; ``` ### Workflow Context Object diff --git a/.claude/skills/mendix/test-app/SKILL.md b/.claude/skills/mendix/test-app/SKILL.md index 149c3662a..6bc3a7791 100644 --- a/.claude/skills/mendix/test-app/SKILL.md +++ b/.claude/skills/mendix/test-app/SKILL.md @@ -105,7 +105,7 @@ Mendix renders each widget's `name` property as a CSS class on the DOM element: This maps directly to MDL widget names. When you generate a widget in MDL: ```sql -actionbutton submitButton (caption: 'Submit', action: save_changes) +actionbutton submitButton (caption: 'Submit', action: save changes) ``` The stable CSS selector is `.mx-name-submitButton`. Use this with `eval` for reliable assertions: @@ -379,7 +379,7 @@ playwright-cli show ```sql -- MDL: names you define become test hooks -actionbutton btnDrivers (caption: 'Drivers', action: show_page Module.Drivers_Overview) +actionbutton btnDrivers (caption: 'Drivers', action: show page Module.Drivers_Overview) datagrid dgOrders (datasource: database Module.Order) { ... } ``` diff --git a/.claude/skills/mendix/validation-microflows/SKILL.md b/.claude/skills/mendix/validation-microflows/SKILL.md index 1f7e0181e..e8f6c5046 100644 --- a/.claude/skills/mendix/validation-microflows/SKILL.md +++ b/.claude/skills/mendix/validation-microflows/SKILL.md @@ -292,4 +292,4 @@ This feature is implemented in: ## Validation rules (CREATE VALIDATION RULE) -binds a **regex** or a **range** to one attribute — `create validation rule for Mod.Entity.Attr regex Mod.Pattern feedback '…'`. The rule is anonymous and entity-scoped, so the statement names the attribute; re-running it replaces the rule of the same type and leaves the attribute's others alone. Unlocked by a `STORAGE-NAME OVERRIDE` in `modelsdk/gen`: it bound `RegularExpression` where Studio Pro stores `RegExIdentifier`, and the control (same script, key reverted) fails **CE0135 "No regular expression specified"** while the fixed one is 0 errors on mxbuild 11.13 with `RegExIdentifier` on disk. Range bounds are inclusive and map to Mendix's only three kinds (`from X to Y`/`from X`/`to Y` → Between/GreaterThanOrEqualTo/SmallerThanOrEqualTo); there is no strict `<`/`>`, and the old grammar's forms for it — plus an EXPRESSION rule type Mendix does not have and an inline regex literal — were removed, having never had a visitor or handler. Required/Unique stay attribute constraints (`not null error '…'` / `unique error '…'`), not a second spelling here. Rewriting an entity carrying **MaxLength or EqualsTo** is **refused** on both engines rather than silently downgraded to Required; that round trip was lossy and `mx check` stayed green, because a Required rule is valid. Both engines carry each rule's payload on READ (`ruleInfoFromGen` / `parseValidationRuleInfo`), which is what makes the refusal narrow instead of covering all of RegEx and Range — and what lets a **range bounded by another attribute** survive a rewrite even though MDL cannot author one (`describe entity` marks it with a comment rather than rendering it wrong). A rule whose payload did not survive the read is refused as firmly as an unknown type: a bare RuleInfo of the right `$Type` constrains nothing, which is the same silent downgrade wearing the right name +binds a **regex** or a **range** to one attribute — `create validation rule for Mod.Entity.Attr regex Mod.Pattern error message '…'`. The rule is anonymous and entity-scoped, so the statement names the attribute; re-running it replaces the rule of the same type and leaves the attribute's others alone. Unlocked by a `STORAGE-NAME OVERRIDE` in `modelsdk/gen`: it bound `RegularExpression` where Studio Pro stores `RegExIdentifier`, and the control (same script, key reverted) fails **CE0135 "No regular expression specified"** while the fixed one is 0 errors on mxbuild 11.13 with `RegExIdentifier` on disk. Range bounds are inclusive and map to Mendix's only three kinds (`from X to Y`/`from X`/`to Y` → Between/GreaterThanOrEqualTo/SmallerThanOrEqualTo); there is no strict `<`/`>`, and the old grammar's forms for it — plus an EXPRESSION rule type Mendix does not have and an inline regex literal — were removed, having never had a visitor or handler. Required/Unique stay attribute constraints (`not null error message '…'` / `unique error message '…'`), not a second spelling here. Rewriting an entity carrying **MaxLength or EqualsTo** is **refused** on both engines rather than silently downgraded to Required; that round trip was lossy and `mx check` stayed green, because a Required rule is valid. Both engines carry each rule's payload on READ (`ruleInfoFromGen` / `parseValidationRuleInfo`), which is what makes the refusal narrow instead of covering all of RegEx and Range — and what lets a **range bounded by another attribute** survive a rewrite even though MDL cannot author one (`describe entity` marks it with a comment rather than rendering it wrong). A rule whose payload did not survive the read is refused as firmly as an unknown type: a bare RuleInfo of the right `$Type` constrains nothing, which is the same silent downgrade wearing the right name diff --git a/.claude/skills/mendix/write-microflows/SKILL.md b/.claude/skills/mendix/write-microflows/SKILL.md index 2b31169d5..0eb490195 100644 --- a/.claude/skills/mendix/write-microflows/SKILL.md +++ b/.claude/skills/mendix/write-microflows/SKILL.md @@ -531,7 +531,7 @@ show page Module.EditPage($Product = $Product); show page Module.EditPage(Product: $Product); ``` -Both `($Param = $value)` and `(Param: $value)` syntaxes are accepted in microflow SHOW PAGE statements. Similarly, widget Action: properties accept both `show_page Module.Page(Param: $value)` and `show_page Module.Page($Param = $value)`. +Both `($Param = $value)` and `(Param: $value)` syntaxes are accepted in microflow SHOW PAGE statements. Similarly, widget Action: properties accept both `show page Module.Page(Param: $value)` and `show page Module.Page($Param = $value)`. ### CLOSE PAGE diff --git a/.claude/skills/mendix/write-microflows/reference/integration.md b/.claude/skills/mendix/write-microflows/reference/integration.md index b342c8523..28f389959 100644 --- a/.claude/skills/mendix/write-microflows/reference/integration.md +++ b/.claude/skills/mendix/write-microflows/reference/integration.md @@ -181,7 +181,7 @@ rest call delete 'https://api.example.com/items/{1}' with ( **REST CALL response types:** - `returns string` — response body as string variable -- `returns nothing` / `returns none` — ignore response +- `returns nothing` — ignore response (`returns none` is the deprecated second spelling, MDL-DEPR024) - `returns response` — returns `System.HttpResponse` object - `returns mapping Module.ImportMapping as Module.Entity` — single object result - `returns mapping Module.ImportMapping as list of Module.Entity` — list result diff --git a/CHANGELOG.md b/CHANGELOG.md index 40236a631..93cd4ad0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed +- **Page actions are words, each keyword has one spelling, and `fmt` writes lowercase keywords without touching names** (ako/mxcli#752, R8) — a button, container or menu action is written with the words a microflow uses: `Action: show page M.Edit`, `save changes close page`, `create object M.E then show page M.P`, `delete`, `call microflow M.F`, `call nanoflow M.F`, `open link '…'`, `sign out`, `complete task '…'`. A validation message is `error message '…'` everywhere (`not null error message '…'`, a validation rule's `error message '…'`, `on delete restrict error message '…'`), an association's delete behaviour is `on delete cascade|restrict|set null`, the many-to-many type is `ReferenceSet`, and a REST call that ignores its response `returns nothing`. `describe` writes these forms, and writes its keywords in lowercase (`folder '…'`, a workflow's `default ->`, a published REST operation's `get`). `mxcli fmt` used to upper-case every word on a keyword list, names included — `Issue64.User` became `Issue64.USER`, an attribute `Title` became `TITLE` — which changed what 374 of the example scripts build; it now lowercases only the words the parse tree shows are keywords, keeps property keys, names, CamelCase values, expressions, XPath and OQL as written, and builds exactly what the input built on every example script. `delete_object close_page`, which describe could print but the grammar did not accept, now parses. Measured on the Studio Pro-authored PedApp and TestApp fixtures: describe emits none of the old spellings and no upper-case keyword, and the round-trip harness stays green. **Migrating a script:** nothing breaks — every old spelling still parses with the same meaning and warns **MDL-DEPR020** (page actions, including a menu item's `sign_out`), **MDL-DEPR021** (`error '…'`, `feedback '…'`, `error_message`), **MDL-DEPR022** (`delete_behavior …`), **MDL-DEPR023** (`reference_set`) or **MDL-DEPR024** (`returns none`); `mxcli fmt --upgrade` rewrites each, keeping the script's letter case. - **`describe page` no longer invents names Mendix does not store; `alter page` addresses a DataGrid 2 column by what it shows** (ako/mxcli#749, R12) — a layout grid's rows and columns, a data grid's columns and control bar, a gallery's template and filter, a chart's series and other object-list items have no Name in the model, and describe used to print one it made up (`row row1`, `column col3`, `controlbar controlBar1`, and a data-grid column named after its attribute, so two columns over `FullName` were both `column FullName`). They now print without one — `row { column (DesktopWidth: 6) { … } }`, `column (Attribute: FullName, Caption: 'Full name')` — and the widget name is optional in the grammar for exactly those elements. A column is addressed as `grid column(Attr)`, written as describe writes the attribute (`Owner/Name` over an association), or `grid column('Caption')`; an address two columns share is refused, listing the matches, unless `@n` picks one (`drop dg column(FullName)@2`). A widget Mendix does store a name for still needs one: `check` reports a missing one as **MDL-WIDGET35** and the page builder refuses it. Measured on the Studio Pro-authored PedApp fixture: the round-trip harness stays green, and `mx check` 11.13 shows no new error after `alter page … column(…)` edits and a create with nameless rows, grid columns and gallery slots. **Migrating a script:** nothing breaks — a name written on one of those elements still parses, is ignored as before, and `check` / `exec` warn **MDL-DEPR005**; `mxcli fmt --upgrade` removes it. The older `grid.Column` address keeps working. The info-level MDL-WIDGET16 note about dropped column names is gone, replaced by that warning. - **`describe microflow` / `describe nanoflow` leave out layout the engine derives** (ako/mxcli#748, R12) — `@position`, `@merge`, `@anchor` and `@curve` now follow the rule `@start` has followed since #951: an annotation is printed only when re-executing the description without it would put the node, or route the flow, somewhere else. That is decided by building the flow again from the description exactly as `create or modify` would (nothing is written) and comparing it node by node with the stored flow, pinning the first misplaced node of each run and repeating until nothing moves — so a statement placed after a hand-placed one needs no annotation of its own. A flow authored with no annotations describes with none; a Studio Pro-drawn flow keeps its layout. Measured on the Studio Pro-authored PedApp fixture: re-executing the new description lands every node, anchor and curve where the old one did (all 16 microflows, and all 224 microflows of a second Studio Pro project that re-execute at all); there the output barely shrinks (433 layout lines to 430), as it should, while after `mxcli layout flows` the 24 flows it could lay out go from 163 layout lines to 3. **Migrating a script:** nothing to do — every annotation still parses and means the same, and committed describe output re-executes as before; only a fresh `describe` is shorter. diff --git a/cmd/mxcli/syntax/features_domain_model.go b/cmd/mxcli/syntax/features_domain_model.go index c16b2a2ed..8f094e842 100644 --- a/cmd/mxcli/syntax/features_domain_model.go +++ b/cmd/mxcli/syntax/features_domain_model.go @@ -69,7 +69,7 @@ func init() { "generalization", "extends", "event handler", "attribute", }, Syntax: "CREATE PERSISTENT ENTITY Module.Name (\n Attr: Type [constraints],\n ...\n) [INDEX (attr1)];\n\n-- Documentation is the /** … */ doc comment before the statement.\n-- (A `COMMENT 'text'` option existed, set nothing, and has been removed.)\n\nCREATE NON-PERSISTENT ENTITY Module.Name (...);\n\nCREATE PERSISTENT ENTITY Module.Name EXTENDS Module.Parent (...);", - Example: "/** Stores customer information. */\nCREATE PERSISTENT ENTITY MyModule.Customer (\n Name: String(100) NOT NULL ERROR 'Name is required',\n Email: String(200) UNIQUE,\n Balance: Decimal DEFAULT 0,\n IsActive: Boolean DEFAULT true,\n Status: Enumeration(MyModule.CustomerType)\n)\nINDEX (Email);", + Example: "/** Stores customer information. */\nCREATE PERSISTENT ENTITY MyModule.Customer (\n Name: String(100) NOT NULL ERROR MESSAGE 'Name is required',\n Email: String(200) UNIQUE,\n Balance: Decimal DEFAULT 0,\n IsActive: Boolean DEFAULT true,\n Status: Enumeration(MyModule.CustomerType)\n)\nINDEX (Email);", SeeAlso: []string{"domain-model.entity.create", "domain-model.entity.alter", "domain-model.entity.attributes"}, }) @@ -287,8 +287,8 @@ func init() { "many-to-one", "many-to-many", "foreign key", "owner", "delete behavior", }, - Syntax: "[@anchor(from: (x, y), to: (x, y))]\nCREATE [OR MODIFY] ASSOCIATION Module.Name\n FROM Module.FromEntity TO Module.ToEntity\n TYPE Reference|ReferenceSet\n [OWNER Default|Both]\n [DELETE_BEHAVIOR behavior]\n [COMMENT 'text'];\nALTER ASSOCIATION Module.Name SET ANCHOR FROM (x, y) TO (x, y);\nDROP ASSOCIATION Module.Name;\n\nOR MODIFY: updates type/owner/delete behavior in-place, preserves UUID.\n\n@anchor sets the LINE ANCHORS — where the connector attaches to each entity box\nin the domain model editor — as a PERCENTAGE of the box (0..100, whole numbers).\n`from` is the FROM entity's box, `to` the TO entity's: (0, 50) is the middle of\nthe left edge, (100, 50) the right, (50, 100) the bottom centre. Omitting an end\nPRESERVES what is stored, so a CREATE OR MODIFY about something else never\nflattens a hand-tuned line. Cross-module associations have no anchors.\n\nDROP reconciles the entity access rules that named the association, and is\nREFUSED while a message definition still exposes it (that one cannot be\nreconciled -- removing the member would change a published contract). The\nrefusal prints the `alter message definition ... drop member` statement for\neach definition, ready to run.", - Example: "-- Many-to-one\nCREATE ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer\n TYPE Reference\n OWNER Default\n DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES;\n\n-- Many-to-many\nCREATE ASSOCIATION Shop.Product_Tag\n FROM Shop.Product TO Shop.Tag\n TYPE ReferenceSet\n OWNER Both;\n\n-- Line leaving the bottom of Order and entering the top of Customer\n@anchor(from: (50, 100), to: (50, 0))\nCREATE ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer;\n\n-- Retune the line without restating the association\nALTER ASSOCIATION Shop.Order_Customer SET ANCHOR FROM (0, 54) TO (100, 54);", + Syntax: "[@anchor(from: (x, y), to: (x, y))]\nCREATE [OR MODIFY] ASSOCIATION Module.Name\n FROM Module.FromEntity TO Module.ToEntity\n TYPE Reference|ReferenceSet\n [OWNER Default|Both]\n [ON DELETE CASCADE|RESTRICT|SET NULL [ERROR MESSAGE 'text']]\n [COMMENT 'text'];\nALTER ASSOCIATION Module.Name SET ANCHOR FROM (x, y) TO (x, y);\nDROP ASSOCIATION Module.Name;\n\nOR MODIFY: updates type/owner/delete behavior in-place, preserves UUID.\n\n@anchor sets the LINE ANCHORS — where the connector attaches to each entity box\nin the domain model editor — as a PERCENTAGE of the box (0..100, whole numbers).\n`from` is the FROM entity's box, `to` the TO entity's: (0, 50) is the middle of\nthe left edge, (100, 50) the right, (50, 100) the bottom centre. Omitting an end\nPRESERVES what is stored, so a CREATE OR MODIFY about something else never\nflattens a hand-tuned line. Cross-module associations have no anchors.\n\nDROP reconciles the entity access rules that named the association, and is\nREFUSED while a message definition still exposes it (that one cannot be\nreconciled -- removing the member would change a published contract). The\nrefusal prints the `alter message definition ... drop member` statement for\neach definition, ready to run.", + Example: "-- Many-to-one\nCREATE ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer\n TYPE Reference\n OWNER Default\n ON DELETE SET NULL;\n\n-- Many-to-many\nCREATE ASSOCIATION Shop.Product_Tag\n FROM Shop.Product TO Shop.Tag\n TYPE ReferenceSet\n OWNER Both;\n\n-- Line leaving the bottom of Order and entering the top of Customer\n@anchor(from: (50, 100), to: (50, 0))\nCREATE ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer;\n\n-- Retune the line without restating the association\nALTER ASSOCIATION Shop.Order_Customer SET ANCHOR FROM (0, 54) TO (100, 54);", SeeAlso: []string{"domain-model.association.create", "domain-model.association.anchor", "domain-model.association.delete-behavior"}, }) @@ -300,8 +300,8 @@ func init() { "reference set", "junction table", "foreign key", "owner default", "owner both", "storage column", "storage table", }, - Syntax: "CREATE [OR MODIFY] ASSOCIATION Module.AssociationName\n FROM Module.FromEntity TO Module.ToEntity\n TYPE Reference|ReferenceSet\n [OWNER Default|Both]\n [STORAGE COLUMN|TABLE]\n [DELETE_BEHAVIOR behavior]\n [COMMENT 'text'];\n\nDirection:\n FROM = entity holding the FK (the \"many\" side)\n TO = entity being referenced (the \"one\" side)\n\nTypes:\n Reference = Many-to-one (FK column on FROM table)\n ReferenceSet = Many-to-many (junction table)\n\nOR MODIFY: updates in-place, preserves UUID. Safe to re-run.", - Example: "-- Many-to-one with delete behavior\nCREATE ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer\n TYPE Reference\n OWNER Default\n DELETE_BEHAVIOR PREVENT;\n\n-- Many-to-many\nCREATE ASSOCIATION Shop.Product_Tag\n FROM Shop.Product TO Shop.Tag\n TYPE ReferenceSet\n OWNER Both;\n\n-- Idempotent update\nCREATE OR MODIFY ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer\n TYPE Reference\n OWNER Default\n DELETE_BEHAVIOR CASCADE;", + Syntax: "CREATE [OR MODIFY] ASSOCIATION Module.AssociationName\n FROM Module.FromEntity TO Module.ToEntity\n TYPE Reference|ReferenceSet\n [OWNER Default|Both]\n [STORAGE COLUMN|TABLE]\n [ON DELETE CASCADE|RESTRICT|SET NULL [ERROR MESSAGE 'text']]\n [COMMENT 'text'];\n\nDirection:\n FROM = entity holding the FK (the \"many\" side)\n TO = entity being referenced (the \"one\" side)\n\nTypes:\n Reference = Many-to-one (FK column on FROM table)\n ReferenceSet = Many-to-many (junction table)\n\nOR MODIFY: updates in-place, preserves UUID. Safe to re-run.", + Example: "-- Many-to-one with delete behavior\nCREATE ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer\n TYPE Reference\n OWNER Default\n ON DELETE RESTRICT;\n\n-- Many-to-many\nCREATE ASSOCIATION Shop.Product_Tag\n FROM Shop.Product TO Shop.Tag\n TYPE ReferenceSet\n OWNER Both;\n\n-- Idempotent update\nCREATE OR MODIFY ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer\n TYPE Reference\n OWNER Default\n ON DELETE CASCADE;", SeeAlso: []string{"domain-model.association.delete-behavior", "domain-model.entity.create"}, }) @@ -327,7 +327,7 @@ func init() { "A fractional coordinate is refused: Mendix stores two integers and its\n" + "loader will not OPEN a project whose anchor is fractional.\n" + "Cross-module associations have no anchors — Mendix stores none for them.", - Example: "-- Line leaving the bottom of Order, entering the top of Customer\n@anchor(from: (50, 100), to: (50, 0))\nCREATE ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer\n TYPE Reference;\n\n-- Retune the line without restating the association\nALTER ASSOCIATION Shop.Order_Customer SET ANCHOR FROM (0, 54) TO (100, 54);\n\n-- Says nothing about anchors: whatever the line was dragged to survives\nCREATE OR MODIFY ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer\n DELETE_BEHAVIOR CASCADE;", + Example: "-- Line leaving the bottom of Order, entering the top of Customer\n@anchor(from: (50, 100), to: (50, 0))\nCREATE ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer\n TYPE Reference;\n\n-- Retune the line without restating the association\nALTER ASSOCIATION Shop.Order_Customer SET ANCHOR FROM (0, 54) TO (100, 54);\n\n-- Says nothing about anchors: whatever the line was dragged to survives\nCREATE OR MODIFY ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer\n ON DELETE CASCADE;", SeeAlso: []string{"domain-model.association.create", "domain-model.entity"}, }) @@ -340,7 +340,7 @@ func init() { "delete and references", "delete but keep references", "delete if no references", "referential integrity", }, - Syntax: "ON DELETE [ERROR_MESSAGE '']\n" + + Syntax: "ON DELETE [ERROR MESSAGE '']\n" + " ON DELETE SET NULL Keep the referencing objects, clear the reference (default)\n" + " ON DELETE CASCADE Delete the referencing objects too\n" + " ON DELETE RESTRICT Refuse the delete while references exist\n\n" + @@ -349,19 +349,18 @@ func init() { "key, TO is referenced -- so `FROM Order TO Customer ON DELETE RESTRICT` means\n" + "deleting a CUSTOMER is refused while Orders reference it, the same way it would\n" + "in CREATE TABLE.\n\n" + - "ERROR_MESSAGE is what the user sees when a RESTRICT delete is refused (Studio\n" + + "ERROR MESSAGE is what the user sees when a RESTRICT delete is refused (Studio\n" + "Pro's \"Error message if 'X' object cannot be deleted\"). SQL has no equivalent;\n" + "this is a Mendix extension. Omitting it stores an empty message.\n\n" + - "The older spelling still works and means the same thing:\n" + - " DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES | DELETE_AND_REFERENCES\n" + - " | DELETE_IF_NO_REFERENCES | CASCADE | PREVENT\n" + - " [ERROR_MESSAGE '']\n" + - "DESCRIBE emits the ON DELETE form, because it says which side is governed.", + "The older DELETE_BEHAVIOR clause (DELETE_AND_REFERENCES | CASCADE,\n" + + "DELETE_IF_NO_REFERENCES | PREVENT, DELETE_BUT_KEEP_REFERENCES) still parses and\n" + + "means the same thing, but is deprecated (MDL-DEPR022); `mxcli fmt --upgrade`\n" + + "rewrites it. DESCRIBE emits the ON DELETE form, because it says which side is governed.", Example: "CREATE ASSOCIATION Shop.Order_Customer\n" + " FROM Shop.Order TO Shop.Customer\n" + " TYPE Reference\n" + " ON DELETE RESTRICT\n" + - " ERROR_MESSAGE 'A customer with orders cannot be deleted';\n\n" + + " ERROR MESSAGE 'A customer with orders cannot be deleted';\n\n" + "CREATE ASSOCIATION Shop.Order_Lines\n" + " FROM Shop.OrderLine TO Shop.Order\n" + " TYPE Reference\n" + diff --git a/cmd/mxcli/syntax/features_integration.go b/cmd/mxcli/syntax/features_integration.go index cb98e9b71..48e1093fb 100644 --- a/cmd/mxcli/syntax/features_integration.go +++ b/cmd/mxcli/syntax/features_integration.go @@ -148,7 +148,7 @@ func init() { "-- Take a $Request: System.HttpRequest parameter to see the query string.\n" + "-- MDL-ODATA02 and MDL-ODATA03 flag a read microflow that takes none.", Example: "create persistent entity Shop.Customer (\n" + - " Email: string(200) unique error 'unique' required error 'required',\n" + + " Email: string(200) unique error message 'unique' required error message 'required',\n" + " Name: string(200)\n" + ");\n" + "\n" + @@ -218,7 +218,7 @@ func init() { "RETURNS Module.MyFile -- store the body in a file document\n" + "RETURNS MAPPING Module.IMM AS Module.E -- apply an import mapping (single object)\n" + "RETURNS MAPPING Module.IMM AS LIST OF Module.E\n" + - "RETURNS NONE | NOTHING -- ignore the response\n\n" + + "RETURNS NOTHING -- ignore the response\n\n" + "-- The file document form takes a SPECIALIZATION of System.FileDocument.\n" + "-- Mendix rejects the base type as a return type (CE0362), and MDL064\n" + "-- reports that before the write. There is no matching form for an\n" + diff --git a/cmd/mxcli/syntax/features_misc.go b/cmd/mxcli/syntax/features_misc.go index 2334c55fa..dcf2255ad 100644 --- a/cmd/mxcli/syntax/features_misc.go +++ b/cmd/mxcli/syntax/features_misc.go @@ -695,11 +695,11 @@ SHOW REFERENCES TO Val.EmailAddress;`, }, Syntax: `CREATE VALIDATION RULE FOR Module.Entity.Attribute REGEX Module.PatternName - FEEDBACK ''; + ERROR MESSAGE ''; CREATE VALIDATION RULE FOR Module.Entity.Attribute RANGE FROM TO - FEEDBACK ''; + ERROR MESSAGE ''; The bounds are inclusive and either may be omitted: RANGE FROM 1 TO 100 between 1 and 100 @@ -720,24 +720,24 @@ CE0135 "No regular expression specified" at build time. REQUIRED and UNIQUE rules are written as attribute constraints instead, on CREATE ENTITY or ALTER ENTITY: ALTER ENTITY Shop.Product MODIFY ATTRIBUTE Email string(200) - NOT NULL ERROR 'Email is required'; + NOT NULL ERROR MESSAGE 'Email is required'; ALTER ENTITY Shop.Product MODIFY ATTRIBUTE Code string(20) - UNIQUE ERROR 'Code must be unique';`, + UNIQUE ERROR MESSAGE 'Code must be unique';`, Example: `CREATE REGULAR EXPRESSION Shop.EmailPattern ( Expression: '^[^@\s]+@[^@\s]+\.[^@\s]+$' ); CREATE VALIDATION RULE FOR Shop.Customer.Email REGEX Shop.EmailPattern - FEEDBACK 'Enter a valid email address'; + ERROR MESSAGE 'Enter a valid email address'; CREATE VALIDATION RULE FOR Shop.Booking.Guests RANGE FROM 1 TO 100 - FEEDBACK 'Between 1 and 100 guests are allowed'; + ERROR MESSAGE 'Between 1 and 100 guests are allowed'; CREATE VALIDATION RULE FOR Shop.Product.Price RANGE FROM 0 - FEEDBACK 'Price cannot be negative';`, + ERROR MESSAGE 'Price cannot be negative';`, }) // ── Scheduled events ──────────────────────────────────────────────── diff --git a/cmd/mxcli/syntax/features_page.go b/cmd/mxcli/syntax/features_page.go index c8952bf9a..4d861d9ea 100644 --- a/cmd/mxcli/syntax/features_page.go +++ b/cmd/mxcli/syntax/features_page.go @@ -13,7 +13,7 @@ func init() { "widget", "layout", "screen", }, Syntax: "CREATE PAGE Module.Name\n (\n Title: 'Page Title',\n Layout: Module.LayoutName\n [, Params: { $Param: Module.Entity }]\n [, Url: 'page-url']\n [, Folder: 'FolderPath']\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 }", + 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"}, }) @@ -122,7 +122,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "statictext", "staticimage", "dynamicimage", "dropdown", "referenceselector", "legacydatagrid", }, - Syntax: "-- Containers\nLAYOUTGRID name { ROW r { COLUMN c (DesktopWidth: 6) { ... } } }\nCONTAINER name (Class: 'cls') { ... }\nCONTAINER name (OnClick: MICROFLOW Module.MF) { ... } -- clickable container\nCUSTOMCONTAINER name (Class: 'cls') { ... }\nGROUPBOX name (Caption: 'C') { ... }\nTABCONTAINER name { TABPAGE tp (Caption: 'One') { ... } TABPAGE tp2 (Caption: 'Two') { ... } }\n\n" + + Syntax: "-- Containers\nLAYOUTGRID name { ROW r { COLUMN c (DesktopWidth: 6) { ... } } }\nCONTAINER name (Class: 'cls') { ... }\nCONTAINER name (OnClick: CALL MICROFLOW Module.MF) { ... } -- clickable container\nCUSTOMCONTAINER name (Class: 'cls') { ... }\nGROUPBOX name (Caption: 'C') { ... }\nTABCONTAINER name { TABPAGE tp (Caption: 'One') { ... } TABPAGE tp2 (Caption: 'Two') { ... } }\n\n" + "-- Data widgets\nDATAVIEW name (DataSource: $Param) { ... FOOTER f { ... } }\nDATAGRID name (DataSource: DATABASE Module.Entity) { COLUMN c (Attribute: A) }\nGALLERY name (DataSource: DATABASE Module.Entity, DesktopColumns: 3) { ... }\nLISTVIEW name (DataSource: DATABASE Module.Entity) { ... }\nLISTVIEW name (...) { ... TEMPLATE FOR Module.Specialization { ... } }\n\n" + "-- Data grid 2 column filters go INSIDE the column's own braces\nDATAGRID dg (...) { COLUMN c (Attribute: A) { TEXTFILTER tf (Attribute: A) } }\nTEXTFILTER | NUMBERFILTER | DATEFILTER | DROPDOWNFILTER | DROPDOWNSORT\n" + "-- Match the filter to the column's type, or MxBuild refuses it: String ->\n" + @@ -144,7 +144,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "-- container the grid declares — it used to be dropped on write with no\n" + "-- diagnostic, and is now refused (MDL-WIDGET30).\n\n" + "-- Inputs\nTEXTBOX name (Label: 'L', Attribute: Attr)\nTEXTAREA | DATEPICKER | COMBOBOX | CHECKBOX | RADIOBUTTONS\n\n" + - "-- Actions\nACTIONBUTTON name (Caption: 'C', Action: SAVE_CHANGES, ButtonStyle: Primary)\nLINKBUTTON name (Caption: 'C', Action: ...)\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" + "-- IMAGE needs a source. Its default, `ImageType: image`, shows an entry from an\n" + "-- image collection, named as three parts: Module.Collection.ImageName.\n" + @@ -187,7 +187,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "-- Use DYNAMICTEXT with a literal Content.)\n" + "-- REFERENCESELECTOR (unsupported widget type)\n" + "-- LEGACYDATAGRID (use DATAGRID for the pluggable equivalent on Mendix 11+)", - Example: "DATAVIEW dvCustomer (DataSource: $Customer) {\n TEXTBOX txtName (Label: 'Name', Attribute: Name)\n COMBOBOX cbStatus (Label: 'Status', Attribute: Status)\n FOOTER footer1 {\n ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)\n ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)\n }\n}", + Example: "DATAVIEW dvCustomer (DataSource: $Customer) {\n TEXTBOX txtName (Label: 'Name', Attribute: Name)\n COMBOBOX cbStatus (Label: 'Status', Attribute: Status)\n FOOTER footer1 {\n ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary)\n ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES)\n }\n}", SeeAlso: []string{"page.create", "page.datasource"}, }) @@ -259,8 +259,8 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "icon", "linkbutton", "link button", "nothing", "no action", "inert", "dead button", }, - Syntax: "Action: NOTHING -- deliberately no action (Forms$NoAction)\nAction: SAVE_CHANGES\nAction: SAVE_CHANGES CLOSE_PAGE -- save, then close the pop-up\nAction: CANCEL_CHANGES\nAction: CANCEL_CHANGES CLOSE_PAGE\nAction: CLOSE_PAGE\nAction: DELETE\nAction: DELETE CLOSE_PAGE\nAction: DELETE_OBJECT\nAction: NANOFLOW Module.NF\nAction: NANOFLOW Module.NF(Param: $val)\nAction: OPEN_LINK 'https://example.com'\nAction: OPEN_LINK $currentObject/URL -- address read from an attribute\nAction: SIGN_OUT\nAction: COMPLETE_TASK 'OutcomeName'\nAction: SHOW_PAGE Module.Page\nAction: SHOW_PAGE Module.Page(Param: $currentObject)\nAction: MICROFLOW Module.MF\nAction: MICROFLOW Module.MF(Param: $val)\nAction: CREATE_OBJECT Module.Entity THEN SHOW_PAGE Module.Page\n\nThe list above is exhaustive. Anything else in an action slot is an\nERROR (MDL-WIDGET28), including a real keyword short its argument --\n`Action: OPEN_LINK` without a URL, `Action: SHOW_PAGE` without a page.\nSuch a widget used to be written with NO action at all and rendered as a\ndead control, with check, exec and mxbuild all clean, because a\nno-action widget is legal Mendix (mendixlabs/mxcli#1062). Write NOTHING\nwhen a control really is meant to be inert.\n\nThe same forms serve `OnClick:` (an alias of `Action:`) and `OnChange:`.\n\nA microflow or nanoflow action is a CALL: it needs an argument for every\nparameter the flow declares, or Mendix rejects the page with CE1571. The\nargument list is the same on every widget that takes an action -- a\nCONTAINER (which is clickable) as much as an ACTIONBUTTON. An enclosing\ndata container of the right type supplies it without an argument; a data\ngrid's CONTROL BAR does not, because it is not row-scoped -- pass the\ngrid's selection there (`$dgOrders`).\n\nA SHOW_PAGE argument must be the enclosing widget's context object --\neither $currentObject or the name of the variable the enclosing data\nwidget is bound to. Mendix infers it from that widget, so naming any\nother variable is refused (MDL-PAGEARG01); call a microflow instead.\nOutside any data widget there is no context object to infer, so such a\nbutton takes NO argument at all -- not a page parameter, not\n$currentObject, not a literal. mxcli used to drop it in silence and\nmxbuild then reported CE1571 per parameter of the target page\n(mendixlabs/mxcli#1029). Route that navigation through a microflow.\n\nOPEN_LINK takes a web address, stored as a Forms$StaticOrDynamicString:\neither a literal, or $currentObject/Attr to read it from an attribute of\nthe enclosing data container's object at runtime (Studio Pro's \"Address:\nattribute\"). An address over an association path is not supported yet.\n\nButton styles: Default, Primary, Success, Info, Warning, Danger\n\nMendix has THREE icon elements and the keyword picks which one:\n\nIcon: 'Atlas_Core.Atlas_Filled.pencil' -- an icon collection\nIcon: image MyModule.Images.logo -- an IMAGE collection\nIcon: glyph 57377 -- a font code point\n\nThe bare form is the icon-collection icon and any collection in the\nproject works, third-party ones included. The image form points into a\ndifferent document, and is spelled the same way apart from the keyword\n-- write it without `image` and mxcli stores a custom-icon reference,\nwhich fails the build with CE1613 (mendixlabs/mxcli#1059).\n`mxcli check -p … --references` resolves each kind against its own\ncollection and names the remedy when the kind is wrong.\n\nA glyph carries a code and no name. Codes are sparse, and an undefined\none fails only at `mxbuild --target=deploy`, naming the PAGE rather\nthan the icon -- so MDL078 checks it against the font's own table.\nList them with `show glyphs`.\n\nA name may be quoted or bare; a hyphenated segment is double-quoted on\nits own: Atlas_Core.Atlas.\"align-center\".\n\nUse `linkbutton` instead of `actionbutton` for link render mode (same properties).", - Example: "ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)\nACTIONBUTTON btnEdit (Caption: 'Edit',\n Action: SHOW_PAGE Module.EditPage(Item: $currentObject))\nLINKBUTTON btnDelete (Caption: 'Delete', Action: DELETE,\n Icon: 'Atlas_Core.Atlas_Filled.pencil')\n\n-- A clickable CONTAINER in a data grid's control bar, calling a nanoflow\n-- with the grid's selection as its argument.\nDATAGRID dgOrders (DataSource: DATABASE FROM Sales.Order, Selection: Single) {\n COLUMN colNr (Attribute: Number, Caption: 'Order #')\n CONTROLBAR cb {\n CONTAINER cShip (Class: 'command',\n Action: NANOFLOW Sales.ACT_Ship($Order = $dgOrders)) {\n ACTIONBUTTON btnShip (Caption: 'Ship')\n }\n }\n}", + Syntax: "Action: NOTHING -- deliberately no action (Forms$NoAction)\nAction: SAVE CHANGES\nAction: SAVE CHANGES CLOSE PAGE -- save, then close the pop-up\nAction: CANCEL CHANGES\nAction: CANCEL CHANGES CLOSE PAGE\nAction: CLOSE PAGE\nAction: DELETE\nAction: DELETE CLOSE PAGE\nAction: CALL NANOFLOW Module.NF\nAction: CALL NANOFLOW Module.NF(Param: $val)\nAction: OPEN LINK 'https://example.com'\nAction: OPEN LINK $currentObject/URL -- address read from an attribute\nAction: SIGN OUT\nAction: COMPLETE TASK 'OutcomeName'\nAction: SHOW PAGE Module.Page\nAction: SHOW PAGE Module.Page(Param: $currentObject)\nAction: CALL MICROFLOW Module.MF\nAction: CALL MICROFLOW Module.MF(Param: $val)\nAction: CREATE OBJECT Module.Entity THEN SHOW PAGE Module.Page\n\nThe list above is exhaustive. Anything else in an action slot is an\nERROR (MDL-WIDGET28), including a real keyword short its argument --\n`Action: OPEN LINK` without a URL, `Action: SHOW PAGE` without a page.\nSuch a widget used to be written with NO action at all and rendered as a\ndead control, with check, exec and mxbuild all clean, because a\nno-action widget is legal Mendix (mendixlabs/mxcli#1062). Write NOTHING\nwhen a control really is meant to be inert.\n\nThe actions are the words a microflow uses (R8). The snake-case spellings\n(SAVE_CHANGES, SHOW_PAGE, CLOSE_PAGE, DELETE_OBJECT, OPEN_LINK, SIGN_OUT, ...)\nand a flow call without CALL still parse, but are deprecated (MDL-DEPR020);\n`mxcli fmt --upgrade` rewrites them.\n\nThe same forms serve `OnClick:` (an alias of `Action:`) and `OnChange:`.\n\nA microflow or nanoflow action is a CALL: it needs an argument for every\nparameter the flow declares, or Mendix rejects the page with CE1571. The\nargument list is the same on every widget that takes an action -- a\nCONTAINER (which is clickable) as much as an ACTIONBUTTON. An enclosing\ndata container of the right type supplies it without an argument; a data\ngrid's CONTROL BAR does not, because it is not row-scoped -- pass the\ngrid's selection there (`$dgOrders`).\n\nA SHOW PAGE argument must be the enclosing widget's context object --\neither $currentObject or the name of the variable the enclosing data\nwidget is bound to. Mendix infers it from that widget, so naming any\nother variable is refused (MDL-PAGEARG01); call a microflow instead.\nOutside any data widget there is no context object to infer, so such a\nbutton takes NO argument at all -- not a page parameter, not\n$currentObject, not a literal. mxcli used to drop it in silence and\nmxbuild then reported CE1571 per parameter of the target page\n(mendixlabs/mxcli#1029). Route that navigation through a microflow.\n\nOPEN LINK takes a web address, stored as a Forms$StaticOrDynamicString:\neither a literal, or $currentObject/Attr to read it from an attribute of\nthe enclosing data container's object at runtime (Studio Pro's \"Address:\nattribute\"). An address over an association path is not supported yet.\n\nButton styles: Default, Primary, Success, Info, Warning, Danger\n\nMendix has THREE icon elements and the keyword picks which one:\n\nIcon: 'Atlas_Core.Atlas_Filled.pencil' -- an icon collection\nIcon: image MyModule.Images.logo -- an IMAGE collection\nIcon: glyph 57377 -- a font code point\n\nThe bare form is the icon-collection icon and any collection in the\nproject works, third-party ones included. The image form points into a\ndifferent document, and is spelled the same way apart from the keyword\n-- write it without `image` and mxcli stores a custom-icon reference,\nwhich fails the build with CE1613 (mendixlabs/mxcli#1059).\n`mxcli check -p … --references` resolves each kind against its own\ncollection and names the remedy when the kind is wrong.\n\nA glyph carries a code and no name. Codes are sparse, and an undefined\none fails only at `mxbuild --target=deploy`, naming the PAGE rather\nthan the icon -- so MDL078 checks it against the font's own table.\nList them with `show glyphs`.\n\nA name may be quoted or bare; a hyphenated segment is double-quoted on\nits own: Atlas_Core.Atlas.\"align-center\".\n\nUse `linkbutton` instead of `actionbutton` for link render mode (same properties).", + Example: "ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary)\nACTIONBUTTON btnEdit (Caption: 'Edit',\n Action: SHOW PAGE Module.EditPage(Item: $currentObject))\nLINKBUTTON btnDelete (Caption: 'Delete', Action: DELETE,\n Icon: 'Atlas_Core.Atlas_Filled.pencil')\n\n-- A clickable CONTAINER in a data grid's control bar, calling a nanoflow\n-- with the grid's selection as its argument.\nDATAGRID dgOrders (DataSource: DATABASE FROM Sales.Order, Selection: Single) {\n COLUMN colNr (Attribute: Number, Caption: 'Order #')\n CONTROLBAR cb {\n CONTAINER cShip (Class: 'command',\n Action: CALL NANOFLOW Sales.ACT_Ship($Order = $dgOrders)) {\n ACTIONBUTTON btnShip (Caption: 'Ship')\n }\n }\n}", SeeAlso: []string{"page.widgets"}, }) @@ -284,7 +284,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: MICROFLOW Module.MF) ON btnSave; -- any CREATE PAGE action form\n SET ('createFileAction': MICROFLOW Module.MF) ON fileUploader1;\n -- a pluggable widget's NAMED action slot,\n -- by the widget's own key; refused on a\n -- key that is not action-typed\n SET (DataSource: $Param) ON dvOrder; -- parameter/microflow/nanoflow/selection;\n -- DATABASE and association are REPLACE-only,\n -- and a data view takes no database source\n SET (prop1: val1, prop2: val2) ON widgetName;\n SET (Title: 'New Title'); -- page-level (case-sensitive): no ON\n SET (Documentation: 'What this page is for.');\n SET (Class: 'css-class'); -- page-level CSS class / style\n SET (Style: 'css: rule');\n SET (PopupWidth: 800, PopupHeight: 480, PopupResizable: true); -- page-level pop-up\n INSERT AFTER widgetName { };\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"}, }) @@ -413,7 +413,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { // 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\n [( Params: { $P: Module.Entity } )] -- entities only; a primitive is CE0046\n [( Variables: { $isEditable: Boolean = 'true' } )]\n [( Folder: 'Snippets/Common' )]\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}", + 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"}, }) @@ -424,7 +424,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "alter snippet", "modify snippet", "update snippet", }, Syntax: "ALTER SNIPPET Module.Name {\n SET (property: value) ON widgetName;\n INSERT AFTER widgetName { };\n INSERT BEFORE widgetName { };\n INSERT INTO containerName { };\n DROP name1, name2;\n REPLACE widgetName WITH { };\n};", - Example: "ALTER SNIPPET Module.NavSnippet {\n REPLACE navItem1 WITH {\n ACTIONBUTTON btnHome (Caption: 'Home', Action: SHOW_PAGE Module.HomePage)\n };\n DROP txtOldField;\n INSERT AFTER txtName {\n TEXTBOX txtNewField (Label: 'New Field', Attribute: NewAttr)\n };\n};", + Example: "ALTER SNIPPET Module.NavSnippet {\n REPLACE navItem1 WITH {\n ACTIONBUTTON btnHome (Caption: 'Home', Action: SHOW PAGE Module.HomePage)\n };\n DROP txtOldField;\n INSERT AFTER txtName {\n TEXTBOX txtNewField (Label: 'New Field', Attribute: NewAttr)\n };\n};", SeeAlso: []string{"snippet", "page.alter"}, }) @@ -619,7 +619,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "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 ITEM '' [PAGE Module.Page | MICROFLOW Module.Flow | SIGN OUT] [ICON Module.Collection.name];\n" + " MENU '' [ICON Module.Collection.name] ( );\n" + ");\n" + "DESCRIBE MENU Module.Name;\n" + @@ -638,7 +638,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "-- SHOW NAVIGATION MENU and ALTER NAVIGATION. Both use these same items.\n" + "-- * OR MODIFY replaces the item list wholesale; an omitted item is removed.\n" + "-- The document's identity and export level are preserved.\n" + - "-- * SIGN_OUT is the log-out menu item. It needs no target and stores the\n" + + "-- * 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" + @@ -658,7 +658,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "use fragment", "template", "script scope", }, Syntax: "DEFINE FRAGMENT Name AS { };\nDEFINE FRAGMENT Name AS { SLOT [name] };\nDEFINE 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: "DEFINE 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: "DEFINE 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"}, }) @@ -693,7 +693,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "binding", "rebind", "building block override", "reusable component", }, Syntax: "-- Declare typed params, reference with $name in a datasource/action slot:\nDEFINE FRAGMENT Name ($data: datasource, $onEdit: action) AS { … };\n-- Supply values at the use site:\nUSE FRAGMENT Name ($data: , $onEdit: ) [{ payload }]\n-- Building blocks: rebind the outermost datasource / first button:\nUSE BUILDING BLOCK Module.Block (datasource: , action: ) [AS prefix_]", - Example: "DEFINE FRAGMENT DataPanel ($data: datasource, $onEdit: action) AS {\n CONTAINER panel (Class: 'card') {\n LISTVIEW lv (DataSource: $data) {\n SLOT content\n ACTIONBUTTON edit (Caption: 'Edit', Action: $onEdit, ButtonStyle: Primary)\n }\n }\n};\n\nCREATE PAGE Module.Orders (Title: 'Orders', Layout: Atlas_Core.Atlas_Default) {\n USE FRAGMENT DataPanel ($data: DATABASE Sales.Order, $onEdit: MICROFLOW Sales.Edit) {\n DYNAMICTEXT heading (Content: 'Orders', RenderMode: H4)\n }\n};\n\n-- Rebind a building block's datasource and primary button:\nUSE BUILDING BLOCK Atlas_Web_Content.List_Cards (datasource: DATABASE Sales.Order, action: MICROFLOW Sales.Open) AS orders_\n\n-- Notes:\n-- * Param kinds: datasource | action. Every declared param must be supplied.\n-- * A microflow value parses as a datasource and is reinterpreted for an action param.\n-- * BB binding-point rule: datasource → first datasource widget; action → first button.", + Example: "DEFINE FRAGMENT DataPanel ($data: datasource, $onEdit: action) AS {\n CONTAINER panel (Class: 'card') {\n LISTVIEW lv (DataSource: $data) {\n SLOT content\n ACTIONBUTTON edit (Caption: 'Edit', Action: $onEdit, ButtonStyle: Primary)\n }\n }\n};\n\nCREATE PAGE Module.Orders (Title: 'Orders', Layout: Atlas_Core.Atlas_Default) {\n USE FRAGMENT DataPanel ($data: DATABASE Sales.Order, $onEdit: MICROFLOW Sales.Edit) {\n DYNAMICTEXT heading (Content: 'Orders', RenderMode: H4)\n }\n};\n\n-- Rebind a building block's datasource and primary button:\nUSE BUILDING BLOCK Atlas_Web_Content.List_Cards (datasource: DATABASE Sales.Order, action: CALL MICROFLOW Sales.Open) AS orders_\n\n-- Notes:\n-- * Param kinds: datasource | action. Every declared param must be supplied.\n-- * A microflow value parses as a datasource and is reinterpreted for an action param.\n-- * BB binding-point rule: datasource → first datasource widget; action → first button.", SeeAlso: []string{"fragment", "fragment.slot", "fragment.use"}, }) diff --git a/docs-site/src/appendixes/data-type-mapping.md b/docs-site/src/appendixes/data-type-mapping.md index 5129b59b2..9ccb3bacc 100644 --- a/docs-site/src/appendixes/data-type-mapping.md +++ b/docs-site/src/appendixes/data-type-mapping.md @@ -67,9 +67,9 @@ Comprehensive mapping between MDL data types, Mendix internal types, and backend | Constraint | Syntax | Description | |------------|--------|-------------| | Not Null | `NOT NULL` | Value is required | -| Not Null with Error | `NOT NULL ERROR 'message'` | Required with custom error | +| Not Null with Error | `NOT NULL ERROR MESSAGE 'message'` | Required with custom error | | Unique | `UNIQUE` | Value must be unique | -| Unique with Error | `UNIQUE ERROR 'message'` | Unique with custom error | +| Unique with Error | `UNIQUE ERROR MESSAGE 'message'` | Unique with custom error | Constraints must appear in this order: 1. `NOT NULL [ERROR '...']` diff --git a/docs-site/src/appendixes/quick-reference.md b/docs-site/src/appendixes/quick-reference.md index 5747f21e2..25119ec56 100644 --- a/docs-site/src/appendixes/quick-reference.md +++ b/docs-site/src/appendixes/quick-reference.md @@ -32,7 +32,7 @@ CREATE PERSISTENT ENTITY Module.Photo ( | List entities | `LIST ENTITIES [IN Module];` | List all or filter by module | | Create enumeration | `CREATE [OR MODIFY] ENUMERATION Module.Name (Value1 'Caption', ...);` | | | Drop enumeration | `DROP ENUMERATION Module.Name;` | | -| Create association | `CREATE ASSOCIATION Module.Name FROM Parent TO Child TYPE Reference\|ReferenceSet [OWNER Default\|Both] [DELETE_BEHAVIOR ...];` | | +| Create association | `CREATE ASSOCIATION Module.Name FROM Parent TO Child TYPE Reference\|ReferenceSet [OWNER Default\|Both] [ON DELETE CASCADE\|RESTRICT\|SET NULL];` | | | Drop association | `DROP ASSOCIATION Module.Name;` | | ## ALTER ENTITY @@ -353,8 +353,8 @@ MDL uses explicit property declarations for pages: | 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) { ... }` | -| Action binding | `Action: TYPE` | `ACTIONBUTTON btn (Caption: 'Save', Action: SAVE_CHANGES)` | -| Microflow action | `Action: MICROFLOW Name(Param: val)` | `Action: MICROFLOW Mod.ACT_Process(Order: $Order)` | +| Action binding | `Action: TYPE` | `ACTIONBUTTON btn (Caption: 'Save', Action: SAVE CHANGES)` | +| Microflow action | `Action: CALL MICROFLOW Name(Param: val)` | `Action: CALL MICROFLOW Mod.ACT_Process(Order: $Order)` | | Database source | `DataSource: DATABASE Entity` | `DATAGRID dg (DataSource: DATABASE Module.Entity)` | | Selection binding | `DataSource: SELECTION widget` | `DATAVIEW dv (DataSource: SELECTION galleryList)` | | CSS class | `Class: 'classes'` | `CONTAINER c (Class: 'card mx-spacing-top-large')` | @@ -401,8 +401,8 @@ CREATE PAGE MyModule.Customer_Edit COMBOBOX cbStatus (Label: 'Status', Attribute: Status) FOOTER footer1 { - ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } } } diff --git a/docs-site/src/examples/alter-page.md b/docs-site/src/examples/alter-page.md index af4461984..2afd6c931 100644 --- a/docs-site/src/examples/alter-page.md +++ b/docs-site/src/examples/alter-page.md @@ -46,9 +46,9 @@ ALTER PAGE CRM.Customer_Edit { ALTER PAGE CRM.Customer_Edit { REPLACE footer1 WITH { FOOTER newFooter { - ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Success) + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Success) ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } } }; @@ -127,7 +127,7 @@ ALTER SNIPPET CRM.NavigationMenu { INSERT AFTER btnHome { ACTIONBUTTON btnReports ( Caption: 'Reports', - Action: SHOW_PAGE CRM.Reports_Overview + Action: SHOW PAGE CRM.Reports_Overview ) } }; diff --git a/docs-site/src/examples/crm-module.md b/docs-site/src/examples/crm-module.md index ca5980c00..2fdf3cfcf 100644 --- a/docs-site/src/examples/crm-module.md +++ b/docs-site/src/examples/crm-module.md @@ -25,9 +25,9 @@ CREATE PERSISTENT ENTITY CRM.Customer ( /** Auto-generated unique identifier */ CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1, /** Full legal name */ - Name: String(200) NOT NULL ERROR 'Customer name is required', + Name: String(200) NOT NULL ERROR MESSAGE 'Customer name is required', /** Primary contact email */ - Email: String(200) UNIQUE ERROR 'Email already exists', + Email: String(200) UNIQUE ERROR MESSAGE 'Email already exists', /** Phone number in international format */ Phone: String(50), /** Current account balance */ @@ -51,7 +51,7 @@ CREATE PERSISTENT ENTITY CRM.ContactLog ( /** Type of interaction */ Type: Enumeration(CRM.ContactType) DEFAULT 'Email', /** Summary of what was discussed */ - Summary: String(2000) NOT NULL ERROR 'Summary is required', + Summary: String(2000) NOT NULL ERROR MESSAGE 'Summary is required', /** Follow-up needed? */ FollowUpRequired: Boolean DEFAULT FALSE ); @@ -123,7 +123,7 @@ CREATE PAGE CRM.Customer_Overview ( COLUMN colStatus (Attribute: Status, Caption: 'Status') COLUMN colActive (Attribute: IsActive, Caption: 'Active') CONTROLBAR cb1 { - ACTIONBUTTON btnNew (Caption: 'New', Action: SHOW_PAGE CRM.Customer_NewEdit, ButtonStyle: Primary) + ACTIONBUTTON btnNew (Caption: 'New', Action: SHOW PAGE CRM.Customer_NewEdit, ButtonStyle: Primary) } } }; @@ -146,10 +146,10 @@ CREATE PAGE CRM.Customer_NewEdit ( FOOTER footer1 { ACTIONBUTTON btnSave ( Caption: 'Save', - Action: MICROFLOW CRM.ACT_Customer_Save, + Action: CALL MICROFLOW CRM.ACT_Customer_Save, ButtonStyle: Success ) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } } } diff --git a/docs-site/src/examples/master-detail.md b/docs-site/src/examples/master-detail.md index cb52a25c7..f6e7cda13 100644 --- a/docs-site/src/examples/master-detail.md +++ b/docs-site/src/examples/master-detail.md @@ -38,10 +38,10 @@ CREATE PAGE CRM.Customer_MasterDetail ( FOOTER footer1 { ACTIONBUTTON btnSave ( Caption: 'Save', - Action: SAVE_CHANGES, + Action: SAVE CHANGES, ButtonStyle: Success ) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } } } diff --git a/docs-site/src/examples/validation.md b/docs-site/src/examples/validation.md index 91f952aa0..cfdab13f2 100644 --- a/docs-site/src/examples/validation.md +++ b/docs-site/src/examples/validation.md @@ -87,10 +87,10 @@ CREATE PAGE Sales.Order_Edit ( FOOTER footer1 { ACTIONBUTTON btnSave ( Caption: 'Save', - Action: MICROFLOW Sales.ACT_Order_Save, + Action: CALL MICROFLOW Sales.ACT_Order_Save, ButtonStyle: Success ) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } } } diff --git a/docs-site/src/internals/reference-tracking.md b/docs-site/src/internals/reference-tracking.md index bd8a6c706..f1088fba2 100644 --- a/docs-site/src/internals/reference-tracking.md +++ b/docs-site/src/internals/reference-tracking.md @@ -10,7 +10,7 @@ During a full catalog refresh, every document is analyzed for outgoing reference |--------------|-------------------| | **Microflows** | CALL MICROFLOW actions, RETRIEVE data sources, entity parameters, SHOW PAGE actions, association traversals | | **Nanoflows** | Same as microflows (client-side) | -| **Pages** | Data source entities, microflow data sources, SHOW_PAGE actions, association paths, snippet calls | +| **Pages** | Data source entities, microflow data sources, SHOW PAGE actions, association paths, snippet calls | | **Snippets** | Same as pages | | **Domain Models** | Generalization references, association endpoints | diff --git a/docs-site/src/language/alter-page.md b/docs-site/src/language/alter-page.md index af598a9b9..187547306 100644 --- a/docs-site/src/language/alter-page.md +++ b/docs-site/src/language/alter-page.md @@ -113,7 +113,7 @@ ALTER PAGE Module.EditPage { -- Insert before a widget ALTER PAGE Module.EditPage { INSERT BEFORE btnSave { - ACTIONBUTTON btnPreview (Caption: 'Preview', Action: MICROFLOW Module.ACT_Preview) + ACTIONBUTTON btnPreview (Caption: 'Preview', Action: CALL MICROFLOW Module.ACT_Preview) } }; diff --git a/docs-site/src/language/associations.md b/docs-site/src/language/associations.md index a39486b72..c1f108033 100644 --- a/docs-site/src/language/associations.md +++ b/docs-site/src/language/associations.md @@ -18,7 +18,7 @@ CREATE ASSOCIATION . TO TYPE [OWNER ] - [DELETE_BEHAVIOR ] + [ON DELETE [ERROR MESSAGE '']] ``` ### Reference (Many-to-One) @@ -32,7 +32,7 @@ CREATE ASSOCIATION Sales.Order_Customer TO Sales.Order TYPE Reference OWNER Default - DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES; + ON DELETE SET NULL; ``` ### ReferenceSet (Many-to-Many) @@ -65,8 +65,11 @@ Controls what happens when an associated object is deleted. | Behavior | MDL Keyword | Description | |----------|-------------|-------------| -| Keep references | `DELETE_BUT_KEEP_REFERENCES` | Delete the object, set references to null | -| Cascade | `DELETE_AND_REFERENCES` | Delete associated objects as well | +| Keep references | `ON DELETE SET NULL` | Delete the object, set references to null (the default) | +| Cascade | `ON DELETE CASCADE` | Delete associated objects as well | +| Restrict | `ON DELETE RESTRICT [ERROR MESSAGE '…']` | Refuse the delete while references exist | + +The older `DELETE_BEHAVIOR ` clause still parses with the same meaning, but is deprecated (MDL-DEPR022). ```sql /** Invoice must be deleted with Order */ @@ -74,7 +77,7 @@ CREATE ASSOCIATION Sales.Order_Invoice FROM Sales.Order TO Sales.Invoice TYPE Reference - DELETE_BEHAVIOR DELETE_AND_REFERENCES; + ON DELETE CASCADE; ``` ## Naming Convention diff --git a/docs-site/src/language/attributes.md b/docs-site/src/language/attributes.md index 398e2ae5f..a83ebc5b5 100644 --- a/docs-site/src/language/attributes.md +++ b/docs-site/src/language/attributes.md @@ -37,9 +37,9 @@ Validation rules are expressed as attribute constraints. When validation fails, | Validation | MDL Syntax | Description | |------------|------------|-------------| | Required | `NOT NULL` | Attribute must have a value | -| Required with message | `NOT NULL ERROR 'message'` | Custom error message on empty | +| Required with message | `NOT NULL ERROR MESSAGE 'message'` | Custom error message on empty | | Unique | `UNIQUE` | Value must be unique across all objects | -| Unique with message | `UNIQUE ERROR 'message'` | Custom error message on duplicate | +| Unique with message | `UNIQUE ERROR MESSAGE 'message'` | Custom error message on duplicate | ### Example with Validation @@ -49,14 +49,14 @@ CREATE PERSISTENT ENTITY Sales.Product ( Name: String(200) NOT NULL, -- Required with custom error - SKU: String(50) NOT NULL ERROR 'SKU is required for all products', + SKU: String(50) NOT NULL ERROR MESSAGE 'SKU is required for all products', -- Unique only Barcode: String(50) UNIQUE, -- Required and unique with custom errors - ProductCode: String(20) NOT NULL ERROR 'Product code required' - UNIQUE ERROR 'Product code must be unique', + ProductCode: String(20) NOT NULL ERROR MESSAGE 'Product code required' + UNIQUE ERROR MESSAGE 'Product code must be unique', -- Optional field (no validation) Description: String(unlimited) diff --git a/docs-site/src/language/basics.md b/docs-site/src/language/basics.md index b33d43e2e..1d7923729 100644 --- a/docs-site/src/language/basics.md +++ b/docs-site/src/language/basics.md @@ -36,6 +36,22 @@ Under `mdl 1;` a missing `;` is an error, and so is the Oracle SQL*Plus-style `/ A trailing comma is allowed in every bracketed list — attributes, enumeration values, parameters, property lists, `{ … }` blocks — under every language version. `()` is the only way to write an empty list: `(,)` and `(a,,)` are errors. +## Keyword Case + +Keywords are case-insensitive, and lowercase is canonical: `describe` writes them in lowercase and `mxcli fmt` normalizes them to it. Names are not keywords, even when they are spelled like one — a module member `User`, an attribute `Title` or a property key `Folder:` keeps its case, and so does a CamelCase value such as `ButtonStyle: Success` or a type name such as `String(200)`. Expressions, XPath, OQL and SQL are stored as written, so `fmt` leaves their text alone. + +## One Spelling per Keyword + +Each keyword has one spelling, and a page action uses the words a microflow uses. The older spellings still parse with the same meaning, warn with the code shown, and are rewritten by `mxcli fmt --upgrade`: + +| Canonical | Deprecated | Code | +|---|---|---| +| `show page`, `save changes`, `cancel changes`, `close page`, `create object`, `delete`, `open link`, `sign out`, `complete task`, `call microflow M.F`, `call nanoflow M.F` | `show_page`, `save_changes`, `cancel_changes`, `close_page`, `create_object`, `delete_object`, `open_link`, `sign_out`, `complete_task`, `microflow M.F`, `nanoflow M.F` | `MDL-DEPR020` | +| `not null error message '…'` (also after `unique`, `required`), validation rule `error message '…'`, `on delete restrict error message '…'` | `not null error '…'`, `feedback '…'`, `error_message '…'`, `errormessage '…'` | `MDL-DEPR021` | +| `on delete cascade` / `restrict` / `set null` | `delete_behavior cascade` / `prevent` / `delete_and_references` / `delete_if_no_references` / `delete_but_keep_references` | `MDL-DEPR022` | +| `type ReferenceSet` | `type reference_set` | `MDL-DEPR023` | +| `returns nothing` (REST call) | `returns none` | `MDL-DEPR024` | + ## Language Version Header A script may start with a header that names the MDL language version it is written in: diff --git a/docs-site/src/language/constraints.md b/docs-site/src/language/constraints.md index 287fa3351..d61e91ad2 100644 --- a/docs-site/src/language/constraints.md +++ b/docs-site/src/language/constraints.md @@ -25,7 +25,7 @@ Name: String(200) NOT NULL With a custom error message displayed to the user: ```sql -Name: String(200) NOT NULL ERROR 'Name is required' +Name: String(200) NOT NULL ERROR MESSAGE 'Name is required' ``` ## UNIQUE @@ -39,7 +39,7 @@ Email: String(200) UNIQUE With a custom error message: ```sql -Email: String(200) UNIQUE ERROR 'Email already exists' +Email: String(200) UNIQUE ERROR MESSAGE 'Email already exists' ``` ## DEFAULT @@ -76,8 +76,8 @@ OptionalField: String(200) All three constraints can be used together: ```sql -Email: String(200) NOT NULL ERROR 'Email is required' - UNIQUE ERROR 'Email already exists' +Email: String(200) NOT NULL ERROR MESSAGE 'Email is required' + UNIQUE ERROR MESSAGE 'Email already exists' DEFAULT '' ``` @@ -89,14 +89,14 @@ CREATE PERSISTENT ENTITY Sales.Product ( Name: String(200) NOT NULL, -- Required with custom error - SKU: String(50) NOT NULL ERROR 'SKU is required for all products', + SKU: String(50) NOT NULL ERROR MESSAGE 'SKU is required for all products', -- Unique only Barcode: String(50) UNIQUE, -- Required and unique with custom errors - ProductCode: String(20) NOT NULL ERROR 'Product code required' - UNIQUE ERROR 'Product code must be unique', + ProductCode: String(20) NOT NULL ERROR MESSAGE 'Product code required' + UNIQUE ERROR MESSAGE 'Product code must be unique', -- Default only Quantity: Integer DEFAULT 0, diff --git a/docs-site/src/language/data-binding.md b/docs-site/src/language/data-binding.md index cb2409563..e2ca2fa93 100644 --- a/docs-site/src/language/data-binding.md +++ b/docs-site/src/language/data-binding.md @@ -144,7 +144,7 @@ Action buttons can pass the current data context to microflows and pages: DATAVIEW dvOrder (DataSource: $Order) { ACTIONBUTTON btnProcess ( Caption: 'Process Order', - Action: MICROFLOW Sales.ACT_ProcessOrder(Order: $Order), + Action: CALL MICROFLOW Sales.ACT_ProcessOrder(Order: $Order), ButtonStyle: Primary ) diff --git a/docs-site/src/language/domain-model.md b/docs-site/src/language/domain-model.md index 38b70fa63..2649b3861 100644 --- a/docs-site/src/language/domain-model.md +++ b/docs-site/src/language/domain-model.md @@ -43,8 +43,8 @@ CREATE ENUMERATION Sales.OrderStatus ( @Position(100, 100) CREATE PERSISTENT ENTITY Sales.Customer ( CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1, - Name: String(200) NOT NULL ERROR 'Customer name is required', - Email: String(200) UNIQUE ERROR 'Email already registered', + Name: String(200) NOT NULL ERROR MESSAGE 'Customer name is required', + Email: String(200) UNIQUE ERROR MESSAGE 'Email already registered', Phone: String(50), IsActive: Boolean DEFAULT TRUE, CreatedAt: DateTime @@ -72,7 +72,7 @@ CREATE ASSOCIATION Sales.Order_Customer TO Sales.Order TYPE Reference OWNER Default - DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES; + ON DELETE SET NULL; ``` ## Further Reading diff --git a/docs-site/src/language/entities.md b/docs-site/src/language/entities.md index e5886bde1..f945111ce 100644 --- a/docs-site/src/language/entities.md +++ b/docs-site/src/language/entities.md @@ -35,9 +35,9 @@ CREATE PERSISTENT ENTITY Sales.Customer ( /** Auto-incrementing unique identifier */ CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1, /** Full legal name of the customer */ - Name: String(200) NOT NULL ERROR 'Name is required', + Name: String(200) NOT NULL ERROR MESSAGE 'Name is required', /** Primary contact email address */ - Email: String(200) UNIQUE ERROR 'Email must be unique', + Email: String(200) UNIQUE ERROR MESSAGE 'Email must be unique', /** Current account balance */ Balance: Decimal DEFAULT 0, /** Whether the account is active */ @@ -170,10 +170,10 @@ CREATE PERSISTENT ENTITY Sales.Customer ( CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1, /** Full legal name of the customer */ - Name: String(200) NOT NULL ERROR 'Name is required', + Name: String(200) NOT NULL ERROR MESSAGE 'Name is required', /** Primary contact email address */ - Email: String(200) UNIQUE ERROR 'Email must be unique', + Email: String(200) UNIQUE ERROR MESSAGE 'Email must be unique', /** Current account balance in the base currency */ Balance: Decimal DEFAULT 0, diff --git a/docs-site/src/language/page-patterns.md b/docs-site/src/language/page-patterns.md index 89be95155..66f465b73 100644 --- a/docs-site/src/language/page-patterns.md +++ b/docs-site/src/language/page-patterns.md @@ -22,10 +22,10 @@ CREATE PAGE MyModule.Customer_Overview CONTROLBAR bar1 { ACTIONBUTTON btnNew ( Caption: 'New Customer', - Action: MICROFLOW MyModule.ACT_Customer_New, + Action: CALL MICROFLOW MyModule.ACT_Customer_New, ButtonStyle: Primary ) - ACTIONBUTTON btnEdit (Caption: 'Edit', Action: SHOW_PAGE MyModule.Customer_Edit) + ACTIONBUTTON btnEdit (Caption: 'Edit', Action: SHOW PAGE MyModule.Customer_Edit) ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger) } } @@ -66,8 +66,8 @@ CREATE PAGE MyModule.Customer_Edit COMBOBOX cbStatus (Label: 'Status', Attribute: Status) CHECKBOX cbActive (Label: 'Active', Attribute: IsActive) FOOTER footer1 { - ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } } } @@ -107,10 +107,10 @@ CREATE PAGE MyModule.Customer_Detail FOOTER footer1 { ACTIONBUTTON btnEdit ( Caption: 'Edit', - Action: SHOW_PAGE MyModule.Customer_Edit, + Action: SHOW PAGE MyModule.Customer_Edit, ButtonStyle: Primary ) - ACTIONBUTTON btnBack (Caption: 'Back', Action: CLOSE_PAGE) + ACTIONBUTTON btnBack (Caption: 'Back', Action: CLOSE PAGE) } } } @@ -140,7 +140,7 @@ CREATE PAGE MyModule.Product_MasterDetail CONTROLBAR bar1 { ACTIONBUTTON btnNew ( Caption: 'New', - Action: MICROFLOW MyModule.ACT_Product_New, + Action: CALL MICROFLOW MyModule.ACT_Product_New, ButtonStyle: Primary ) } @@ -153,7 +153,7 @@ CREATE PAGE MyModule.Product_MasterDetail TEXTBOX txtPrice (Label: 'Price', Attribute: Price) COMBOBOX cbCategory (Label: 'Category', Attribute: Category) FOOTER footer1 { - ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) } } } @@ -199,7 +199,7 @@ CREATE PAGE MyModule.Order_MasterDetail FOOTER footer1 { ACTIONBUTTON btnEdit ( Caption: 'Edit Order', - Action: SHOW_PAGE MyModule.Order_Edit, + Action: SHOW PAGE MyModule.Order_Edit, ButtonStyle: Primary ) } @@ -236,10 +236,10 @@ CREATE PAGE HR.Employee_Overview CONTROLBAR bar1 { ACTIONBUTTON btnNew ( Caption: 'New Employee', - Action: MICROFLOW HR.ACT_Employee_New, + Action: CALL MICROFLOW HR.ACT_Employee_New, ButtonStyle: Primary ) - ACTIONBUTTON btnEdit (Caption: 'Edit', Action: SHOW_PAGE HR.Employee_Edit) + ACTIONBUTTON btnEdit (Caption: 'Edit', Action: SHOW PAGE HR.Employee_Edit) ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger) } } @@ -260,8 +260,8 @@ CREATE PAGE HR.Employee_Edit DATEPICKER dpHireDate (Label: 'Hire Date', Attribute: HireDate) TEXTBOX txtEmail (Label: 'Email', Attribute: Email) FOOTER footer1 { - ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } } } diff --git a/docs-site/src/language/page-structure.md b/docs-site/src/language/page-structure.md index 615dea2f4..c19c54f8a 100644 --- a/docs-site/src/language/page-structure.md +++ b/docs-site/src/language/page-structure.md @@ -50,8 +50,8 @@ CREATE PAGE MyModule.Customer_Edit DATAVIEW dvCustomer (DataSource: $Customer) { TEXTBOX txtName (Label: 'Name', Attribute: Name) FOOTER footer1 { - ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } } } diff --git a/docs-site/src/language/pages.md b/docs-site/src/language/pages.md index 83f15e816..05b8715dd 100644 --- a/docs-site/src/language/pages.md +++ b/docs-site/src/language/pages.md @@ -60,8 +60,8 @@ CREATE PAGE MyModule.Customer_Edit TEXTBOX txtName (Label: 'Name', Attribute: Name) TEXTBOX txtEmail (Label: 'Email', Attribute: Email) FOOTER footer1 { - ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } } } diff --git a/docs-site/src/language/snippets.md b/docs-site/src/language/snippets.md index 04fb1c533..846936226 100644 --- a/docs-site/src/language/snippets.md +++ b/docs-site/src/language/snippets.md @@ -47,7 +47,7 @@ CREATE SNIPPET MyModule.CustomerCard DYNAMICTEXT txtEmail (Content: '{1}', Attribute: Email) ACTIONBUTTON btnEdit ( Caption: 'Edit', - Action: SHOW_PAGE MyModule.Customer_Edit, + Action: SHOW PAGE MyModule.Customer_Edit, ButtonStyle: Primary ) } diff --git a/docs-site/src/language/widget-types.md b/docs-site/src/language/widget-types.md index 8e13a2248..006f08727 100644 --- a/docs-site/src/language/widget-types.md +++ b/docs-site/src/language/widget-types.md @@ -88,8 +88,8 @@ DATAVIEW dvCustomer (DataSource: $Customer) { TEXTBOX txtEmail (Label: 'Email', Attribute: Email) COMBOBOX cbStatus (Label: 'Status', Attribute: Status) FOOTER footer1 { - ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } } ``` @@ -112,7 +112,7 @@ DATAGRID dgOrders (DataSource: DATABASE Sales.Order, PageSize: 20) { COLUMN colAmount (Attribute: Amount, Caption: 'Amount', Alignment: right) COLUMN colStatus (Attribute: Status, Caption: 'Status') CONTROLBAR bar1 { - ACTIONBUTTON btnNew (Caption: 'New', Action: MICROFLOW Sales.ACT_CreateOrder, ButtonStyle: Primary) + ACTIONBUTTON btnNew (Caption: 'New', Action: CALL MICROFLOW Sales.ACT_CreateOrder, ButtonStyle: Primary) } } ``` @@ -331,8 +331,8 @@ IMAGE imgBanner (Width: 800, Height: 200) A button that triggers an action. The primary interactive element: ```sql -ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) -ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) +ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) +ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger) ``` @@ -340,13 +340,19 @@ ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger) | Action | Description | |--------|-------------| -| `SAVE_CHANGES` | Commit and close the page | -| `CANCEL_CHANGES` | Roll back and close the page | +| `SAVE CHANGES` | Commit and close the page | +| `CANCEL CHANGES` | Roll back and close the page | | `DELETE` | Delete the current object | -| `CLOSE_PAGE` | Close the page without saving | -| `MICROFLOW Module.MF_Name` | Call a microflow | -| `NANOFLOW Module.NF_Name` | Call a nanoflow | -| `PAGE Module.PageName` | Open a page | +| `CLOSE PAGE` | Close the page without saving | +| `CALL MICROFLOW Module.MF_Name` | Call a microflow | +| `CALL NANOFLOW Module.NF_Name` | Call a nanoflow | +| `SHOW PAGE Module.PageName` | Open a page | +| `CREATE OBJECT Module.Entity THEN SHOW PAGE Module.PageName` | Create an object and open a page for it | +| `OPEN LINK 'https://…'` | Open a web address | +| `SIGN OUT` | Sign the user out | +| `COMPLETE TASK 'Outcome'` | Complete a workflow user task | + +The snake-case spellings (`SAVE_CHANGES`, `SHOW_PAGE`, …) and `MICROFLOW …` without `CALL` are deprecated aliases (MDL-DEPR020). **Button styles:** @@ -364,7 +370,7 @@ ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger) ```sql ACTIONBUTTON btnProcess ( Caption: 'Process', - Action: MICROFLOW Sales.ACT_ProcessOrder(Order: $Order), + Action: CALL MICROFLOW Sales.ACT_ProcessOrder(Order: $Order), ButtonStyle: Primary ) ``` @@ -410,8 +416,8 @@ Footer section of a DataView. Typically contains save/cancel buttons: ```sql FOOTER footer1 { - ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } ``` @@ -421,7 +427,7 @@ Control bar for DataGrid widgets. Contains action buttons for the grid: ```sql CONTROLBAR bar1 { - ACTIONBUTTON btnNew (Caption: 'New', Action: MICROFLOW Module.ACT_Create, ButtonStyle: Primary) + ACTIONBUTTON btnNew (Caption: 'New', Action: CALL MICROFLOW Module.ACT_Create, ButtonStyle: Primary) ACTIONBUTTON btnEdit (Caption: 'Edit', Action: PAGE Module.Entity_Edit) ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger) } diff --git a/docs-site/src/migration/generation.md b/docs-site/src/migration/generation.md index 7d52f148f..1cf086030 100644 --- a/docs-site/src/migration/generation.md +++ b/docs-site/src/migration/generation.md @@ -46,8 +46,8 @@ CREATE ENUMERATION Sales.OrderStatus ( /** Customer master data */ @Position(100, 100) CREATE PERSISTENT ENTITY CRM.Customer ( - Name: String(200) NOT NULL ERROR 'Customer name is required', - Email: String(200) UNIQUE ERROR 'Email already exists', + Name: String(200) NOT NULL ERROR MESSAGE 'Customer name is required', + Email: String(200) UNIQUE ERROR MESSAGE 'Email already exists', Phone: String(50), IsActive: Boolean DEFAULT TRUE ) @@ -106,7 +106,7 @@ CREATE PAGE CRM.Customer_Overview ( COLUMN colPhone (Attribute: Phone, Caption: 'Phone') COLUMN colActive (Attribute: IsActive, Caption: 'Active') CONTROLBAR cb1 { - ACTIONBUTTON btnNew (Caption: 'New', Action: SHOW_PAGE CRM.Customer_NewEdit, ButtonStyle: Primary) + ACTIONBUTTON btnNew (Caption: 'New', Action: SHOW PAGE CRM.Customer_NewEdit, ButtonStyle: Primary) } } }; diff --git a/docs-site/src/reference/domain-model/create-association.md b/docs-site/src/reference/domain-model/create-association.md index 0835677cc..73f203f85 100644 --- a/docs-site/src/reference/domain-model/create-association.md +++ b/docs-site/src/reference/domain-model/create-association.md @@ -7,7 +7,7 @@ TO to_module.to_entity TYPE { Reference | ReferenceSet } [ OWNER { Default | Both | Parent | Child } ] - [ DELETE_BEHAVIOR { DELETE_BUT_KEEP_REFERENCES | DELETE_AND_REFERENCES | DELETE_IF_NO_REFERENCES | CASCADE | PREVENT } ] + [ ON DELETE { SET NULL | CASCADE | RESTRICT } [ ERROR MESSAGE 'text' ] ] ## Description @@ -29,15 +29,15 @@ The `OWNER` clause controls which side of the association can modify the relatio | `Parent` | Only the FROM side can modify the association | | `Child` | Only the TO side can modify the association | -The `DELETE_BEHAVIOR` clause controls what happens when an object on the FROM side is deleted: +The `ON DELETE` clause controls what happens when an object on the FROM side is deleted: -| Behavior | Description | +| Action | Description | |----------|-------------| -| `DELETE_BUT_KEEP_REFERENCES` | Delete the object and set references to null (the default) | -| `DELETE_AND_REFERENCES` | Delete the object and all associated objects on the TO side | -| `CASCADE` | Alias for `DELETE_AND_REFERENCES` | -| `DELETE_IF_NO_REFERENCES` | Refuse the delete while anything still references the object | -| `PREVENT` | Alias for `DELETE_IF_NO_REFERENCES` | +| `SET NULL` | Delete the object and set references to null (the default) | +| `CASCADE` | Delete the object and all associated objects on the TO side | +| `RESTRICT` | Refuse the delete while anything still references the object; `ERROR MESSAGE '…'` is the text the user sees | + +The older `DELETE_BEHAVIOR` clause (`DELETE_BUT_KEEP_REFERENCES`, `DELETE_AND_REFERENCES` / `CASCADE`, `DELETE_IF_NO_REFERENCES` / `PREVENT`) still parses with the same meaning, but is deprecated (MDL-DEPR022); `mxcli fmt --upgrade` rewrites it. If `OR MODIFY` is specified, the statement is idempotent: if the association already exists, it is updated to match the new definition. @@ -63,7 +63,7 @@ A documentation comment (`/** ... */`) placed before the statement is preserved **OWNER** : Which side can modify the association. Defaults to `Default` if omitted. -**DELETE_BEHAVIOR** +**ON DELETE** : What happens to associated objects when a FROM-side object is deleted. If omitted, references are kept (the default Mendix behavior). ## Examples @@ -76,7 +76,7 @@ CREATE ASSOCIATION Sales.Order_Customer TO Sales.Order TYPE Reference OWNER Default - DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES; + ON DELETE SET NULL; ``` ### Many-to-many: Order has Products @@ -97,7 +97,7 @@ CREATE ASSOCIATION Sales.Order_Invoice FROM Sales.Order TO Sales.Invoice TYPE Reference - DELETE_BEHAVIOR DELETE_AND_REFERENCES; + ON DELETE CASCADE; ``` ### Idempotent with OR MODIFY diff --git a/docs-site/src/reference/domain-model/create-entity.md b/docs-site/src/reference/domain-model/create-entity.md index 0207c6d92..ca613bdd2 100644 --- a/docs-site/src/reference/domain-model/create-entity.md +++ b/docs-site/src/reference/domain-model/create-entity.md @@ -116,9 +116,9 @@ CREATE PERSISTENT ENTITY Sales.Customer ( CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1, /** Customer full name */ - Name: String(200) NOT NULL ERROR 'Name is required', + Name: String(200) NOT NULL ERROR MESSAGE 'Name is required', - Email: String(200) UNIQUE ERROR 'Email must be unique', + Email: String(200) UNIQUE ERROR MESSAGE 'Email must be unique', Balance: Decimal DEFAULT 0, diff --git a/docs-site/src/reference/page/create-page.md b/docs-site/src/reference/page/create-page.md index 6c2750094..636684e08 100644 --- a/docs-site/src/reference/page/create-page.md +++ b/docs-site/src/reference/page/create-page.md @@ -119,18 +119,18 @@ values, including the argument list. | Action | Syntax | Description | |--------|--------|-------------| -| Save | `Action: SAVE_CHANGES` | Commits and closes | -| Cancel | `Action: CANCEL_CHANGES` | Rolls back and closes | -| Microflow | `Action: MICROFLOW Module.Name(Param: val)` | Calls a microflow | -| Nanoflow | `Action: NANOFLOW Module.Name(Param: val)` | Calls a nanoflow | +| Save | `Action: SAVE CHANGES` | Commits and closes | +| Cancel | `Action: CANCEL CHANGES` | Rolls back and closes | +| Microflow | `Action: CALL MICROFLOW Module.Name(Param: val)` | Calls a microflow | +| Nanoflow | `Action: CALL NANOFLOW Module.Name(Param: val)` | Calls a nanoflow | | Page | `Action: PAGE Module.PageName` | Opens a page | -| Close | `Action: CLOSE_PAGE` | Closes the current page | +| Close | `Action: CLOSE PAGE` | Closes the current page | | Delete | `Action: DELETE` | Deletes the context object | | Nothing | `Action: NOTHING` | Deliberately no action — a decorative button, a card that is not clickable | The set is closed. Anything else in an action slot is an error (**MDL-WIDGET28**), and that includes a real action keyword **missing its -argument** — `Action: OPEN_LINK` with no URL, `Action: SHOW_PAGE` with no page. +argument** — `Action: OPEN LINK` with no URL, `Action: SHOW PAGE` with no page. Such a widget used to be written with no action at all: it rendered, carried its caption, and did nothing, while `mxcli check`, `exec` and mxbuild all reported success, because a no-action widget is perfectly legal Mendix. Write `NOTHING` @@ -150,7 +150,7 @@ DATAGRID dgOrders (DataSource: DATABASE FROM Sales.Order, Selection: Single) { COLUMN colNr (Attribute: Number, Caption: 'Order #') CONTROLBAR cb { CONTAINER cShip (Class: 'command', - Action: NANOFLOW Sales.ACT_Ship($Order = $dgOrders)) { + Action: CALL NANOFLOW Sales.ACT_Ship($Order = $dgOrders)) { ACTIONBUTTON btnShip (Caption: 'Ship') } } @@ -233,8 +233,8 @@ CREATE PAGE MyModule.Customer_Edit COMBOBOX cbStatus (Label: 'Status', Attribute: Status) FOOTER footer1 { - ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } } }; @@ -256,7 +256,7 @@ CREATE PAGE Sales.Order_Overview COLUMN colStatus (Attribute: Status, Caption: 'Status') COLUMN colAmount (Attribute: TotalAmount, Caption: 'Amount', Alignment: right) CONTROLBAR cb1 { - ACTIONBUTTON btnNew (Caption: 'New Order', Action: SHOW_PAGE Sales.Order_Edit, ButtonStyle: Primary) + ACTIONBUTTON btnNew (Caption: 'New Order', Action: SHOW PAGE Sales.Order_Edit, ButtonStyle: Primary) } } }; @@ -349,13 +349,13 @@ CREATE PAGE MyModule.AdvancedForm { DATAVIEW dvItem (DataSource: $Item) { TEXTBOX txtName (Label: 'Name', Attribute: Name) - ACTIONBUTTON btnToggle (Caption: 'Show Advanced', Action: NANOFLOW MyModule.NAV_Toggle) + ACTIONBUTTON btnToggle (Caption: 'Show Advanced', Action: CALL NANOFLOW MyModule.NAV_Toggle) CONTAINER cntAdvanced (Visible: '$showAdvanced') { TEXTAREA taNotes (Label: 'Notes', Attribute: Notes) } FOOTER footer1 { - ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } } }; diff --git a/docs-site/src/reference/page/create-snippet.md b/docs-site/src/reference/page/create-snippet.md index 72dbe5229..2dce807b1 100644 --- a/docs-site/src/reference/page/create-snippet.md +++ b/docs-site/src/reference/page/create-snippet.md @@ -104,7 +104,7 @@ CREATE PAGE MyModule.Customer_Edit DATAVIEW dvCustomer (DataSource: $Customer) { SNIPPETCALL snpAddress (Snippet: MyModule.AddressFields) FOOTER footer1 { - ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) } } }; diff --git a/docs-site/src/reference/query/describe-association.md b/docs-site/src/reference/query/describe-association.md index 62a53145f..bdaf5415c 100644 --- a/docs-site/src/reference/query/describe-association.md +++ b/docs-site/src/reference/query/describe-association.md @@ -32,7 +32,7 @@ CREATE ASSOCIATION Sales.Order_Customer TO Sales.Order TYPE Reference OWNER Default - DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES; + ON DELETE SET NULL; ``` Describe a many-to-many association: diff --git a/docs-site/src/reference/query/describe-page.md b/docs-site/src/reference/query/describe-page.md index 8fc665777..183914a47 100644 --- a/docs-site/src/reference/query/describe-page.md +++ b/docs-site/src/reference/query/describe-page.md @@ -39,8 +39,8 @@ CREATE PAGE Sales.Customer_Edit TEXTBOX txtName (Label: 'Name', Attribute: Name) TEXTBOX txtEmail (Label: 'Email', Attribute: Email) FOOTER footer1 { - ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } } } diff --git a/docs-site/src/tools/theme.md b/docs-site/src/tools/theme.md index 6636339a2..eee26b5ed 100644 --- a/docs-site/src/tools/theme.md +++ b/docs-site/src/tools/theme.md @@ -87,7 +87,7 @@ the set that is actually installed — so a cycle button can never offer a theme whose CSS is not in the page. ```sql -actionbutton btnSkin (caption: 'Theme', action: nanoflow MyFirstModule.ACT_CycleSkin) +actionbutton btnSkin (caption: 'Theme', action: call nanoflow MyFirstModule.ACT_CycleSkin) ``` The same reload caveat as the light/dark toggle applies: the choice is remembered @@ -149,7 +149,7 @@ to run script before first paint. It creates three JavaScript actions wire a button: ```sql -actionbutton btnTheme (caption: 'Theme', action: nanoflow MyFirstModule.ACT_ToggleTheme) +actionbutton btnTheme (caption: 'Theme', action: call nanoflow MyFirstModule.ACT_ToggleTheme) ``` A click flips the palette and remembers the choice in `localStorage`. The class diff --git a/docs-site/src/tutorial/create-entity.md b/docs-site/src/tutorial/create-entity.md index 65eed3e1c..f87002249 100644 --- a/docs-site/src/tutorial/create-entity.md +++ b/docs-site/src/tutorial/create-entity.md @@ -76,7 +76,7 @@ You can specify what happens when the "to" entity is deleted: CREATE ASSOCIATION MyModule.Order_Product FROM MyModule.Order TO MyModule.Product TYPE Reference - DELETE_BEHAVIOR PREVENT; + ON DELETE RESTRICT; ``` Options: `PREVENT` (block deletion if referenced), `CASCADE` (delete the associated objects too), or leave it out for the default behavior — delete the object and null out the references. diff --git a/docs-site/src/tutorial/create-page.md b/docs-site/src/tutorial/create-page.md index ced0ca607..70554d35a 100644 --- a/docs-site/src/tutorial/create-page.md +++ b/docs-site/src/tutorial/create-page.md @@ -85,8 +85,8 @@ CREATE PAGE MyModule.Product_Edit CHECKBOX cbActive (Label: 'Active', Attribute: IsActive) FOOTER footer1 { - ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } } }; @@ -101,8 +101,8 @@ Key differences from the overview page: | `DATAVIEW dvProduct (DataSource: $Product)` | Binds to the page parameter | | `TEXTBOX`, `CHECKBOX` | Input widgets bound to entity attributes | | `FOOTER` | A section at the bottom of the DataView for action buttons | -| `Action: SAVE_CHANGES` | Built-in action that commits the object and closes the page | -| `Action: CANCEL_CHANGES` | Built-in action that rolls back changes and closes the page | +| `Action: SAVE CHANGES` | Built-in action that commits the object and closes the page | +| `Action: CANCEL CHANGES` | Built-in action that rolls back changes and closes the page | Notice the two different page syntaxes: the overview page uses the **compact syntax** (`LAYOUT` and `TITLE` as keywords before parentheses), while the edit page uses the **property syntax** (properties inside a `(Key: value)` block followed by a `{ widget tree }` block). Both are valid -- use whichever fits better. @@ -130,10 +130,10 @@ DYNAMICTEXT dynName (Attribute: Name) ### Action buttons ```sql -ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) -ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) +ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) +ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger) -ACTIONBUTTON btnProcess (Caption: 'Process', Action: MICROFLOW MyModule.ACT_ProcessProduct(Product: $Product)) +ACTIONBUTTON btnProcess (Caption: 'Process', Action: CALL MICROFLOW MyModule.ACT_ProcessProduct(Product: $Product)) ``` ### Layout widgets diff --git a/docs-site/src/tutorial/describe-search.md b/docs-site/src/tutorial/describe-search.md index 8856cdf73..76ad32622 100644 --- a/docs-site/src/tutorial/describe-search.md +++ b/docs-site/src/tutorial/describe-search.md @@ -42,7 +42,7 @@ CREATE ASSOCIATION MyFirstModule.Order_Customer FROM MyFirstModule.Order TO MyFirstModule.Customer TYPE Reference OWNER Default - DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES; + ON DELETE SET NULL; ``` ## DESCRIBE MICROFLOW @@ -96,8 +96,8 @@ CREATE PAGE MyFirstModule.Customer_Edit TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone) FOOTER footer1 { - ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary) - ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES) + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE CHANGES, ButtonStyle: Primary) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL CHANGES) } } }; @@ -132,7 +132,7 @@ CREATE ASSOCIATION MyFirstModule.Order_Customer FROM MyFirstModule.Order TO MyFirstModule.Customer TYPE Reference OWNER Default - DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES; + ON DELETE SET NULL; ``` ## DESCRIBE MODULE diff --git a/docs/01-project/MDL_QUICK_REFERENCE.md b/docs/01-project/MDL_QUICK_REFERENCE.md index f9add0af0..9203a4d1d 100644 --- a/docs/01-project/MDL_QUICK_REFERENCE.md +++ b/docs/01-project/MDL_QUICK_REFERENCE.md @@ -89,7 +89,7 @@ create persistent entity Module.Photo ( | Create enumeration | `create [or modify] enumeration Module.Name (Value1 'caption', ...);` | | | Alter enumeration values | `alter enumeration Module.Name add value [if not exists] X [caption '..'] \| rename value X to Y \| modify value X caption '..' \| drop value [if exists] X;` | `modify value … caption` re-captions in place (works while referenced). `if not exists` / `if exists` make the script re-runnable — the bare forms error and stop the run | | Drop enumeration | `drop enumeration [if exists] Module.Name;` | Refused for `System.*` (read-only platform module) | -| Create association | `create [or modify] association Module.Name from Parent to Child type reference\|ReferenceSet [owner default\|both] [delete_behavior ...];` | OR MODIFY updates existing association in-place. **The FROM entity must live in `Module`** — Mendix stores an association in its FROM entity's module, so a remote FROM writes a dangling pointer and the project stops OPENING (**MDL070**). The TO entity may be remote; that direction is stored BY NAME | +| Create association | `create [or modify] association Module.Name from Parent to Child type reference\|ReferenceSet [owner default\|both] [on delete cascade\|restrict\|set null [error message '...']];` | OR MODIFY updates existing association in-place. **The FROM entity must live in `Module`** — Mendix stores an association in its FROM entity's module, so a remote FROM writes a dangling pointer and the project stops OPENING (**MDL070**). The TO entity may be remote; that direction is stored BY NAME | | Drop association | `drop association [if exists] Module.Name;` | | | Association line anchors | `@anchor(from: (0, 54), to: (100, 54))` above `create association …` | Where the connector attaches to each entity box, as a **percentage** of the box (0..100, whole numbers). `from` = the FROM entity's box, `to` = the TO entity's. Omitting an end preserves what is stored, so a `create or modify` about something else never flattens a hand-tuned line. Cross-module associations have no anchors — Mendix stores none | | Retune anchors in place | `alter association Module.Name set anchor from (50, 100) to (50, 0);` | `(0, 50)` left-middle, `(100, 50)` right-middle, `(50, 100)` bottom-centre. `describe association` re-emits a non-default pair as the same `@anchor(...)`, so describe → edit → exec round-trips | @@ -236,8 +236,8 @@ the statement names the **attribute**, not the rule. | Statement | Syntax | Notes | |-----------|--------|-------| -| Create regex rule | `create validation rule for Module.Entity.Attribute regex Module.Pattern feedback '';` | Pattern must already exist | -| Create range rule | `create validation rule for Module.Entity.Attribute range from to feedback '';` | Bounds inclusive | +| Create regex rule | `create validation rule for Module.Entity.Attribute regex Module.Pattern error message '';` | Pattern must already exist | +| Create range rule | `create validation rule for Module.Entity.Attribute range from to error message '';` | Bounds inclusive | | Lower bound only | `... range from ...` | Mendix `GreaterThanOrEqualTo` | | Upper bound only | `... range to ...` | Mendix `SmallerThanOrEqualTo` | @@ -255,8 +255,8 @@ expression specified". **Required and Unique are attribute constraints, not this statement:** ```sql -create entity Shop.Product ( Email: String(200) not null error 'Required' ); -alter entity Shop.Product modify attribute Code String(20) unique error 'Unique'; +create entity Shop.Product ( Email: String(200) not null error message 'Required' ); +alter entity Shop.Product modify attribute Code String(20) unique error message 'Unique'; ``` A range bounded by another *attribute* cannot be authored in MDL, but survives a @@ -1453,13 +1453,13 @@ MDL uses explicit property declarations for pages: | Password field | `Password: true` on a textbox | `textbox tbPw (attribute: Secret, Password: true)` — omitted when false. Without it a describe → exec round trip turns a password field into a plaintext one | | Widget validation | `Validation: ''`, `ValidationMessage: ''` | `Validation: 'length(toString($value)) > 0'` — a Mendix expression over `$value`, QUOTED not bracketed (`[...]` is the XPath spelling and parses as an array) | | Variable binding | `datasource: $Var` | `dataview dv (datasource: $Product) { ... }` | -| Action binding | `action: type` | `actionbutton btn (caption: 'Save', action: save_changes)` — the forms are a closed set (`mxcli syntax page.action`); anything else is **MDL-WIDGET28** | -| No action | `action: nothing` | `actionbutton btn (caption: 'Decorative', action: nothing)` — an explicitly inert control. Write it deliberately: an action keyword **short its argument** (`action: open_link` with no URL) is now an error rather than a widget silently written with no action at all | -| Microflow action | `action: microflow Name(Param: val)` | `action: microflow Mod.ACT_Process(Order: $Order)` | +| Action binding | `action: type` | `actionbutton btn (caption: 'Save', action: save changes)` — the forms are a closed set (`mxcli syntax page.action`); anything else is **MDL-WIDGET28** | +| No action | `action: nothing` | `actionbutton btn (caption: 'Decorative', action: nothing)` — an explicitly inert control. Write it deliberately: an action keyword **short its argument** (`action: open link` with no URL) is now an error rather than a widget silently written with no action at all | +| Microflow action | `action: call microflow Name(Param: val)` | `action: call microflow Mod.ACT_Process(Order: $Order)` | | Button icon | `icon: 'Module.IconCollection.IconName'` | `linkbutton btn (caption: 'Edit', action: nothing, icon: 'Atlas_Core.Atlas_Filled.pencil')` — the **icon-collection** icon; MxBuild rejects an unknown name (CE1613) | | Image icon | `icon: image Module.ImageCollection.Name` | `actionbutton btn (caption: 'Logo', action: nothing, icon: image MyMod.Images.logo)` — an **image** collection is a different document from an icon collection, and the names are spelled the same, so the keyword is what separates them. Written without `image` it is stored as a custom-icon reference and the build fails **CE1613** | | Glyph icon | `icon: glyph ` | `actionbutton btn (caption: 'Home', action: nothing, icon: glyph 57377)` — a font code point with no name. Codes are sparse; an undefined one fails only at `mxbuild --target=deploy`, naming the **page**, so **MDL078** checks it. Browse with `show glyphs` | -| Clickable container | `onclick: action` (alias of `action:`) | `container card (onclick: microflow Mod.ACT_Open) { ... }` — takes an argument list like a button: `action: nanoflow Mod.ACT_Ship($Order = $dgOrders)` | +| Clickable container | `onclick: action` (alias of `action:`) | `container card (onclick: call microflow Mod.ACT_Open) { ... }` — takes an argument list like a button: `action: call nanoflow Mod.ACT_Ship($Order = $dgOrders)` | | Action arguments | every parameter needs one | A flow action with an unfilled parameter is **CE1571**. An enclosing data container of its type supplies it; a data grid's **control bar** does not (not row-scoped) — pass the grid's selection, `$dgOrders` | | Database source | `datasource: database entity` | `datagrid dg (datasource: database Module.Entity)` | | Database source, constrained and sorted | `datasource: database entity where [...] sort by Attr asc` | `listview lv (datasource: database from Mod.Vehicle where [Brand != ''] sort by Brand asc)` | @@ -1552,8 +1552,8 @@ create page MyModule.Customer_Edit combobox cbStatus (label: 'Status', attribute: status) footer footer1 { - actionbutton btnSave (caption: 'Save', action: save_changes, buttonstyle: primary) - actionbutton btnCancel (caption: 'Cancel', action: cancel_changes) + actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary) + actionbutton btnCancel (caption: 'Cancel', action: cancel changes) } } } @@ -1667,7 +1667,7 @@ This is the generic ALTER — `alter Module.Name { set (Key: value) on Date: Sun, 27 Sep 2026 20:02:20 +0000 Subject: [PATCH 07/11] =?UTF-8?q?mdl:=20error=20handlers=20are=20`on=20err?= =?UTF-8?q?or=20begin=20=E2=80=A6=20end=20error`;=20while=20needs=20begin/?= =?UTF-8?q?end=20while=20under=20mdl=201=20(#754)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R2 (ADR-0010): braces hold declarative children, imperative flow is `begin … end `. The custom error handler was the only brace block inside a microflow: - grammar: `on error [without rollback] begin … end error` is canonical; the brace form stays as an alias marked MDL-DEPR540, which builds the identical handler and has a structural fmt --upgrade rewrite (`{` -> begin, `}` -> end error), nested handlers included. - describe microflow/nanoflow, the commented-out handler fallback, handle targets and the generated `mxcli test` flows emit the canonical form. - `while` without `begin` or ending in a bare `end` is a new rejection gated on `mdl 1` (MDL-V1-WHILE); under mdl 0 it keeps parsing, warns, and `fmt --upgrade --header` inserts the missing words. - The PedApp describe check now also refuses any deprecated spelling in describe output (control: with the old emitter it reports MDL-DEPR540 on the Studio Pro-authored handlers). - mdl-examples, skills, docs-site, quick reference and `mxcli syntax` migrated to the canonical spellings. Co-Authored-By: Claude Opus 5.5 --- .claude/skills/mendix/check-syntax/SKILL.md | 2 +- .../mendix/migrate-oracle-forms/SKILL.md | 8 +- .../skills/mendix/write-microflows/SKILL.md | 20 +- .../reference/control-flow.md | 40 ++-- .../write-microflows/reference/pitfalls.md | 4 +- .../skills/mendix/write-nanoflows/SKILL.md | 4 +- CHANGELOG.md | 1 + cmd/mxcli/syntax/features_microflow.go | 29 +-- cmd/mxcli/testrunner/generator.go | 20 +- cmd/mxcli/testrunner/generator_endpoint.go | 10 +- .../testrunner/generator_endpoint_test.go | 8 +- cmd/mxcli/testrunner/generator_test.go | 24 +++ docs-site/src/appendixes/common-mistakes.md | 2 +- docs-site/src/appendixes/quick-reference.md | 4 +- docs-site/src/examples/rest-integration.md | 4 +- docs-site/src/language/basics.md | 4 +- docs-site/src/language/control-flow.md | 24 ++- docs-site/src/language/microflow-patterns.md | 12 +- .../reference/microflow/create-microflow.md | 10 +- docs-site/src/tutorial/create-microflow.md | 4 +- docs/01-project/MDL_QUICK_REFERENCE.md | 8 +- .../bug-tests/1100-loop-variable-typing.mdl | 4 +- .../349-custom-error-handler-routing.mdl | 4 +- .../863-execute-database-query-describe.mdl | 8 +- ...63-synchronize-and-unsupported-actions.mdl | 4 +- .../bug-tests/error-handling-continue.mdl | 8 +- .../microflow-1030-raise-error-main-flow.mdl | 8 +- .../microflow-1077-annotation-sharing.mdl | 4 +- ...microflow-1078-error-handler-roundtrip.mdl | 44 ++--- ...icroflow-empty-error-handler-end-event.mdl | 6 +- .../doctype-tests/02-microflow-examples.mdl | 16 +- .../02c-complex-layout-examples.mdl | 20 +- .../38-import-mapping-usage-examples.mdl | 4 +- .../41-microflow-error-handling-examples.mdl | 22 +-- mdl/deprecation/deprecation.go | 15 ++ mdl/executor/cmd_microflows_builder_flows.go | 6 +- mdl/executor/cmd_microflows_builder_merge.go | 2 +- .../cmd_microflows_describe_fold_test.go | 2 +- mdl/executor/cmd_microflows_handles.go | 16 +- mdl/executor/cmd_microflows_handles_test.go | 31 +-- mdl/executor/cmd_microflows_show_helpers.go | 20 +- mdl/executor/cmd_microflows_show_merge.go | 4 +- mdl/executor/nanoflow_validation.go | 2 +- .../validate_microflow_error_handling.go | 2 +- mdl/executor/validate_microflow_merge_join.go | 2 +- .../validate_microflow_raise_error.go | 4 +- mdl/grammar/domains/MDLMicroflow.g4 | 14 +- mdl/roundtrip/describe_terminators_test.go | 8 +- mdl/upgrade/gated.go | 1 + mdl/upgrade/gated_test.go | 6 + mdl/upgrade/upgrade_test.go | 25 +++ mdl/visitor/flow_blocks_test.go | 183 ++++++++++++++++++ mdl/visitor/visitor_flow_blocks.go | 83 ++++++++ mdl/visitor/visitor_microflow_statements.go | 7 +- mdl/visitor/visitor_silent_drops.go | 2 +- mdl/visitor/visitor_upgrade_fixes.go | 2 +- 56 files changed, 604 insertions(+), 227 deletions(-) create mode 100644 mdl/visitor/flow_blocks_test.go create mode 100644 mdl/visitor/visitor_flow_blocks.go diff --git a/.claude/skills/mendix/check-syntax/SKILL.md b/.claude/skills/mendix/check-syntax/SKILL.md index dbed9603e..faf48ef59 100644 --- a/.claude/skills/mendix/check-syntax/SKILL.md +++ b/.claude/skills/mendix/check-syntax/SKILL.md @@ -256,7 +256,7 @@ Before writing any MDL, verify these requirements: - `if condition then ... [else ...] end if;` - `loop $item in $list begin ... end loop;` - `return $value;` -- `on error continue|rollback|{ handler };` +- `on error continue|rollback|[without rollback] begin handler end error;` **Now Supported (previously not):** - `rollback $entity [refresh];` - Reverts uncommitted changes diff --git a/.claude/skills/mendix/migrate-oracle-forms/SKILL.md b/.claude/skills/mendix/migrate-oracle-forms/SKILL.md index 41ea558e9..b2a6a6fba 100644 --- a/.claude/skills/mendix/migrate-oracle-forms/SKILL.md +++ b/.claude/skills/mendix/migrate-oracle-forms/SKILL.md @@ -225,7 +225,7 @@ end; | `for ... loop ... end loop` | `loop $item in $list begin ... end loop` | | `while ... loop ... end loop` | Not directly supported; use recursive microflow | | `CURSOR` | `retrieve $list from ...` then `loop` | -| `EXCEPTION when ... then` | `on error { ... }` | +| `EXCEPTION when ... then` | `on error begin ... end error` | ### Example: PL/SQL to MDL @@ -272,9 +272,9 @@ begin set $Total = $Total + $Order/Amount; change $Order (status = 'PROCESSED'); - commit $Order on error { + commit $Order on error begin log error 'Failed to process order: ' + $Order/OrderNumber; - }; + end error; end loop; log info 'Processed ' + toString($count) + ' orders, total: ' + toString($Total); @@ -358,7 +358,7 @@ widgets ( | `PRE-insert` / `PRE-update` | Before commit event handler | | `post-insert` / `post-update` | After commit event handler | | `key-commit` | Save button action microflow | -| `on-error` | `on error { ... }` blocks | +| `on-error` | `on error begin ... end error` blocks | ## Migration Checklist diff --git a/.claude/skills/mendix/write-microflows/SKILL.md b/.claude/skills/mendix/write-microflows/SKILL.md index 2b31169d5..29ef1fed6 100644 --- a/.claude/skills/mendix/write-microflows/SKILL.md +++ b/.claude/skills/mendix/write-microflows/SKILL.md @@ -656,24 +656,24 @@ show home page; ```mdl call microflow ... on error continue; -- Ignore error call microflow ... on error rollback; -- Rollback on error -call microflow ... on error { log ...; return ...; }; -- Custom handler -call microflow ... on error without rollback { ... }; -- No rollback +call microflow ... on error begin log ...; return ...; end error; -- Custom handler +call microflow ... on error without rollback begin ... end error; -- No rollback ``` The clause goes on whichever activity may fail, not only on calls: ```mdl -declare $Name String = 'default' on error { return 'could not initialise'; }; -$Name = $Other/Name on error { return 'lookup failed'; }; -change $Order (Status = Shipped) on error { log error 'could not ship'; return; }; -log info node 'App' 'starting' on error { return; }; -show message 'saved' on error { return; }; +declare $Name String = 'default' on error begin return 'could not initialise'; end error; +$Name = $Other/Name on error begin return 'lookup failed'; end error; +change $Order (Status = Shipped) on error begin log error 'could not ship'; return; end error; +log info node 'App' 'starting' on error begin return; end error; +show message 'saved' on error begin return; end error; -- BLOCKING halts the client until dismissed; after `objects`, before `on error`. show message 'Hello {1}' type Warning objects [$Name] blocking; -validation feedback $Order/Total message 'must be positive' on error { return; }; -show page Module.Page on error { return; }; -close page on error { return; }; +validation feedback $Order/Total message 'must be positive' on error begin return; end error; +show page Module.Page on error begin return; end error; +close page on error begin return; end error; ``` **Two limits, both reported rather than silently ignored:** diff --git a/.claude/skills/mendix/write-microflows/reference/control-flow.md b/.claude/skills/mendix/write-microflows/reference/control-flow.md index 3ebf37138..563de7048 100644 --- a/.claude/skills/mendix/write-microflows/reference/control-flow.md +++ b/.claude/skills/mendix/write-microflows/reference/control-flow.md @@ -333,17 +333,17 @@ call microflow Module.RiskyOperation() on error continue; -- ON ERROR ROLLBACK: Rollback transaction and propagate error commit $Order on error rollback; --- ON ERROR { ... }: Custom error handler with rollback -$Result = call microflow Module.ExternalService(data = $data) on error { +-- ON ERROR BEGIN ... END ERROR: Custom error handler with rollback +$Result = call microflow Module.ExternalService(data = $data) on error begin log error node 'ServiceError' 'External service failed'; return $DefaultResult; -}; +end error; --- ON ERROR WITHOUT ROLLBACK { ... }: Custom handler, keep changes -commit $Order on error without rollback { +-- ON ERROR WITHOUT ROLLBACK BEGIN ... END ERROR: Custom handler, keep changes +commit $Order on error without rollback begin log warning node 'CommitError' 'Commit failed, using fallback'; change $Order (status = 'PENDING'); -}; +end error; ``` ### Error Handling Semantics @@ -352,23 +352,23 @@ commit $Order on error without rollback { |--------|----------| | `on error continue` | Catch error silently, continue normal flow | | `on error rollback` | Rollback database changes, propagate error | -| `on error { ... }` | Execute handler block, then continue (with rollback) | -| `on error without rollback { ... }` | Execute handler block, keep database changes | +| `on error begin ... end error` | Execute handler block, then continue (with rollback) | +| `on error without rollback begin ... end error` | Execute handler block, keep database changes | ### RAISE ERROR is handler-only `raise error;` builds Mendix's **error event**, which *re-raises the error currently being handled*. Mendix therefore allows one only where an error is in -scope — that is, inside an `on error { ... }` block. Studio Pro will not even +scope — that is, inside an `on error begin ... end error` block. Studio Pro will not even let you draw the connection from the normal flow to an error event. ```mdl -- ✅ inside a handler: an error IS in scope call microflow Module.RiskyOperation() -on error { +on error begin log error node 'Module' 'failed, re-raising'; raise error; -}; +end error; -- ❌ on the main flow: MDL084, and mxbuild rejects it with -- CE0710 "The main flow cannot join an error flow or end in an error event." @@ -409,13 +409,13 @@ returns Module.Response as $response begin -- The call output establishes $response — objects are never declared $response = call microflow Module.CallExternalAPI(data = $RequestData) - on error without rollback { + on error without rollback begin log error node 'ExternalAPI' 'API call failed for: ' + $RequestData; -- Create error response $response = create Module.Response ( success = false, message = 'External service unavailable'); - }; + end error; return $response; end; @@ -431,9 +431,9 @@ knowing which one you are writing. | Form | Error path | |------|-----------| | `on error continue` | No error path at all | -| `on error [without rollback] { … return/throw }` | Its own path, its own terminator | -| `on error [without rollback] { }` | **Not a no-op** — falls through to whatever the *enclosing branch* does next | -| `on error [without rollback] { … join L; }` | Rejoins the normal path at the merge labelled `L` | +| `on error [without rollback] begin … return/throw end error` | Its own path, its own terminator | +| `on error [without rollback] begin end error` | **Not a no-op** — falls through to whatever the *enclosing branch* does next | +| `on error [without rollback] begin … join L; end error` | Rejoins the normal path at the merge labelled `L` | The empty form is the one that surprises people. It means "on error, do whatever the enclosing branch's continuation does" — which in a branch that returns @@ -443,11 +443,11 @@ something else is a value nowhere in the text. Prefer `join` when you mean it. create microflow Module.Post (Payload: String) returns String begin declare $Status String = 'sent'; - $r = call microflow Module.Send(Payload = $Payload) on error without rollback { + $r = call microflow Module.Send(Payload = $Payload) on error without rollback begin log warning node 'Module' 'send failed, degrading'; set $Status = 'degraded'; join recovered; - }; + end error; join recovered; merge recovered; @@ -465,10 +465,10 @@ activity: ```mdl merge attempt; -$r = call microflow Module.Send(Payload = $Payload) on error without rollback { +$r = call microflow Module.Send(Payload = $Payload) on error without rollback begin log warning node 'Module' 'retrying'; join attempt; -}; +end error; return $r; ``` diff --git a/.claude/skills/mendix/write-microflows/reference/pitfalls.md b/.claude/skills/mendix/write-microflows/reference/pitfalls.md index 01e0dd4ab..615dfe0a0 100644 --- a/.claude/skills/mendix/write-microflows/reference/pitfalls.md +++ b/.claude/skills/mendix/write-microflows/reference/pitfalls.md @@ -443,9 +443,9 @@ CATCH end TRY; -- CORRECT: Use ON ERROR on specific activities -commit $Order on error { +commit $Order on error begin log error 'Commit failed'; -}; +end error; ``` ### BREAK/CONTINUE in Loops diff --git a/.claude/skills/mendix/write-nanoflows/SKILL.md b/.claude/skills/mendix/write-nanoflows/SKILL.md index afcae59da..9acbf6791 100644 --- a/.claude/skills/mendix/write-nanoflows/SKILL.md +++ b/.claude/skills/mendix/write-nanoflows/SKILL.md @@ -528,9 +528,9 @@ synchronize unsynchronized; -- only objects with uncommitted offline change synchronize $Order, $Lines; -- named objects/lists ("Specific" mode) synchronize all on error continue; -synchronize all on error without rollback { +synchronize all on error without rollback begin log error 'sync failed'; -}; +end error; ``` The mode is always written out, including `all` — the statement says what it does diff --git a/CHANGELOG.md b/CHANGELOG.md index 40236a631..fa895bd64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed +- **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. - **`describe page` no longer invents names Mendix does not store; `alter page` addresses a DataGrid 2 column by what it shows** (ako/mxcli#749, R12) — a layout grid's rows and columns, a data grid's columns and control bar, a gallery's template and filter, a chart's series and other object-list items have no Name in the model, and describe used to print one it made up (`row row1`, `column col3`, `controlbar controlBar1`, and a data-grid column named after its attribute, so two columns over `FullName` were both `column FullName`). They now print without one — `row { column (DesktopWidth: 6) { … } }`, `column (Attribute: FullName, Caption: 'Full name')` — and the widget name is optional in the grammar for exactly those elements. A column is addressed as `grid column(Attr)`, written as describe writes the attribute (`Owner/Name` over an association), or `grid column('Caption')`; an address two columns share is refused, listing the matches, unless `@n` picks one (`drop dg column(FullName)@2`). A widget Mendix does store a name for still needs one: `check` reports a missing one as **MDL-WIDGET35** and the page builder refuses it. Measured on the Studio Pro-authored PedApp fixture: the round-trip harness stays green, and `mx check` 11.13 shows no new error after `alter page … column(…)` edits and a create with nameless rows, grid columns and gallery slots. **Migrating a script:** nothing breaks — a name written on one of those elements still parses, is ignored as before, and `check` / `exec` warn **MDL-DEPR005**; `mxcli fmt --upgrade` removes it. The older `grid.Column` address keeps working. The info-level MDL-WIDGET16 note about dropped column names is gone, replaced by that warning. - **`describe microflow` / `describe nanoflow` leave out layout the engine derives** (ako/mxcli#748, R12) — `@position`, `@merge`, `@anchor` and `@curve` now follow the rule `@start` has followed since #951: an annotation is printed only when re-executing the description without it would put the node, or route the flow, somewhere else. That is decided by building the flow again from the description exactly as `create or modify` would (nothing is written) and comparing it node by node with the stored flow, pinning the first misplaced node of each run and repeating until nothing moves — so a statement placed after a hand-placed one needs no annotation of its own. A flow authored with no annotations describes with none; a Studio Pro-drawn flow keeps its layout. Measured on the Studio Pro-authored PedApp fixture: re-executing the new description lands every node, anchor and curve where the old one did (all 16 microflows, and all 224 microflows of a second Studio Pro project that re-execute at all); there the output barely shrinks (433 layout lines to 430), as it should, while after `mxcli layout flows` the 24 flows it could lay out go from 163 layout lines to 3. **Migrating a script:** nothing to do — every annotation still parses and means the same, and committed describe output re-executes as before; only a fresh `describe` is shorter. diff --git a/cmd/mxcli/syntax/features_microflow.go b/cmd/mxcli/syntax/features_microflow.go index 74d1d3af0..bb8a5fe90 100644 --- a/cmd/mxcli/syntax/features_microflow.go +++ b/cmd/mxcli/syntax/features_microflow.go @@ -23,7 +23,7 @@ func init() { "offline first", "nanoflow", }, Syntax: "SYNCHRONIZE ALL [ON ERROR ...];\nSYNCHRONIZE UNSYNCHRONIZED [ON ERROR ...]; -- Mendix 9.4+\nSYNCHRONIZE $Var[, $Var...] [ON ERROR ...];\n\nNanoflow only: in a microflow this is MDL057 / CE0009.", - Example: "CREATE NANOFLOW MyModule.NF_Sync ($Order: MyModule.Order)\nBEGIN\n SYNCHRONIZE ALL;\n SYNCHRONIZE UNSYNCHRONIZED;\n SYNCHRONIZE $Order;\n SYNCHRONIZE ALL ON ERROR WITHOUT ROLLBACK {\n LOG ERROR 'sync failed';\n };\nEND;", + Example: "CREATE NANOFLOW MyModule.NF_Sync ($Order: MyModule.Order)\nBEGIN\n SYNCHRONIZE ALL;\n SYNCHRONIZE UNSYNCHRONIZED;\n SYNCHRONIZE $Order;\n SYNCHRONIZE ALL ON ERROR WITHOUT ROLLBACK BEGIN\n LOG ERROR 'sync failed';\n END ERROR;\nEND;", SeeAlso: []string{"microflow.nanoflow", "microflow.error-handling"}, }) @@ -117,7 +117,10 @@ func init() { "loop", "while", "break", "continue", "return", "conditional", "branch", "iterate", }, - Syntax: "IF condition THEN\n ...\nELSIF condition THEN\n ...\nELSE\n ...\nEND IF;\n\nLOOP $Item IN $List BEGIN ... END LOOP;\nWHILE condition BEGIN ... END WHILE;\nRETURN $Value;\nRETURN empty;", + Syntax: "IF condition THEN\n ...\nELSIF condition THEN\n ...\nELSE\n ...\nEND IF;\n\nLOOP $Item IN $List BEGIN ... END LOOP;\nWHILE condition BEGIN ... END WHILE;\nRETURN $Value;\nRETURN empty;\n\n" + + "-- WHILE takes BEGIN and END WHILE like LOOP. Without the `mdl 1;` header both\n" + + "-- may still be left out, with a warning (MDL-V1-WHILE); under `mdl 1;` that\n" + + "-- is an error. `mxcli fmt --upgrade --header` inserts them.", Example: "IF $Customer = empty THEN\n LOG ERROR NODE 'Svc' 'Not found';\n RETURN empty;\nELSIF $Customer/Active = false THEN\n LOG WARNING 'Inactive customer';\nELSE\n CHANGE $Customer (LastAccess = [%CurrentDateTime%]);\nEND IF;\n\nLOOP $Item IN $OrderLines BEGIN\n COMMIT $Item;\nEND LOOP;", SeeAlso: []string{"microflow.variables", "microflow.error-handling", "microflow.splits"}, }) @@ -160,7 +163,11 @@ func init() { "rollback", "throw", "exception", "try", "catch", }, Syntax: "COMMIT $Obj ON ERROR CONTINUE;\nCOMMIT $Obj ON ERROR ROLLBACK;\n" + - "COMMIT $Obj ON ERROR { };\nCOMMIT $Obj ON ERROR WITHOUT ROLLBACK { };\n\n" + + "COMMIT $Obj ON ERROR BEGIN END ERROR;\nCOMMIT $Obj ON ERROR WITHOUT ROLLBACK BEGIN END ERROR;\n\n" + + "-- The handler is flow, so it is BEGIN ... END ERROR like IF, LOOP and WHILE.\n" + + "-- The brace form `ON ERROR { ... }` still parses and warns MDL-DEPR540;\n" + + "-- `mxcli fmt --upgrade` rewrites it.\n" + + "--\n" + "-- The clause goes on the ACTIVITY that may fail. Most statements take it:\n" + "-- DECLARE, SET, CREATE, CHANGE, COMMIT, DELETE, RETRIEVE, every CALL,\n" + "-- LOG, SHOW PAGE, CLOSE PAGE, SHOW MESSAGE, VALIDATION FEEDBACK,\n" + @@ -189,7 +196,7 @@ func init() { "-- flow, so a variable created after the merge is out of scope on the error\n" + "-- path (CE0108). End the handler, or expect that.\n" + "--\n" + - "-- An EMPTY handler `{ }` is not a no-op: it means \"on error, do whatever\n" + + "-- An EMPTY handler `BEGIN END ERROR` is not a no-op: it means \"on error, do whatever\n" + "-- the enclosing branch does next\". Say where the path goes with JOIN.\n" + "--\n" + "-- RAISE ERROR re-raises the error being handled, so it belongs INSIDE an\n" + @@ -198,9 +205,9 @@ func init() { "-- the shape, and mxbuild rejects it with CE0710 \"The main flow cannot\n" + "-- join an error flow or end in an error event.\". To fail deliberately\n" + "-- from the main flow, call a Java action that throws.", - Example: "COMMIT $Order ON ERROR {\n LOG ERROR 'Failed to save order';\n RAISE ERROR;\n};\n\n" + - "COMMIT $Batch ON ERROR WITHOUT ROLLBACK {\n LOG WARNING 'Batch save failed, continuing';\n};\n\n" + - "DECLARE $Name String = 'default' ON ERROR {\n RETURN 'could not initialise';\n};", + Example: "COMMIT $Order ON ERROR BEGIN\n LOG ERROR 'Failed to save order';\n RAISE ERROR;\nEND ERROR;\n\n" + + "COMMIT $Batch ON ERROR WITHOUT ROLLBACK BEGIN\n LOG WARNING 'Batch save failed, continuing';\nEND ERROR;\n\n" + + "DECLARE $Name String = 'default' ON ERROR BEGIN\n RETURN 'could not initialise';\nEND ERROR;", SeeAlso: []string{"microflow.control-flow"}, }) @@ -295,14 +302,14 @@ func init() { "-- Forward and backward references both resolve, so declaration order is\n" + "-- free. A backward one is how a retry loop is written:\n" + "-- MERGE attempt;\n" + - "-- $r = CALL MICROFLOW M.Post() ON ERROR WITHOUT ROLLBACK { JOIN attempt; };\n" + + "-- $r = CALL MICROFLOW M.Post() ON ERROR WITHOUT ROLLBACK BEGIN JOIN attempt; END ERROR;\n" + "--\n" + "-- What this is FOR. Nested IF can only describe a graph whose branches\n" + "-- pair up. Two cases do not:\n" + "-- 1. An ERROR path that rejoins the normal one somewhere other than the\n" + "-- enclosing branch's own continuation. Without JOIN the only\n" + "-- spellings are \"terminate\" and \"fall through\", and DESCRIBE used to\n" + - "-- emit an empty `{ }` for anything else — MDL that re-executes to a\n" + + "-- emit an empty handler for anything else — MDL that re-executes to a\n" + "-- DIFFERENT graph, with no warning.\n" + "-- 2. Crossed branches: an inner split's branch landing where an outer\n" + "-- split's branch lands. No nesting of IF reproduces that.\n" + @@ -326,11 +333,11 @@ func init() { Example: "CREATE MICROFLOW M.Post (Payload: String) RETURNS String\n" + "BEGIN\n" + " DECLARE $Status String = 'sent';\n" + - " $r = CALL MICROFLOW M.Send(Payload = $Payload) ON ERROR WITHOUT ROLLBACK {\n" + + " $r = CALL MICROFLOW M.Send(Payload = $Payload) ON ERROR WITHOUT ROLLBACK BEGIN\n" + " LOG WARNING NODE 'M' 'send failed, degrading';\n" + " SET $Status = 'degraded';\n" + " JOIN recovered;\n" + - " };\n" + + " END ERROR;\n" + " JOIN recovered;\n" + " MERGE recovered;\n" + " RETURN $Status;\n" + diff --git a/cmd/mxcli/testrunner/generator.go b/cmd/mxcli/testrunner/generator.go index 53770c897..7bf25c2bf 100644 --- a/cmd/mxcli/testrunner/generator.go +++ b/cmd/mxcli/testrunner/generator.go @@ -103,13 +103,13 @@ func writeTestBlock(b *strings.Builder, tc TestCase, index int) { // the reasons the endpoint runner exists. func writeSetupBlock(b *strings.Builder, tc TestCase) { for _, flow := range tc.Setups { - fmt.Fprintf(b, " CALL MICROFLOW %s() ON ERROR {\n", flow) + fmt.Fprintf(b, " CALL MICROFLOW %s() ON ERROR BEGIN\n", flow) fmt.Fprintf(b, " LOG ERROR NODE 'MXTEST' 'MXTEST:ERROR:%s:Setup failed: %s';\n", escapeMDLString(tc.ID), escapeMDLString(flow)) b.WriteString(" SET $TestFailed = true;\n") b.WriteString(" SET $AllPassed = false;\n") b.WriteString(" RETURN $AllPassed;\n") - b.WriteString(" };\n") + b.WriteString(" END ERROR;\n") } } @@ -262,14 +262,14 @@ func rewriteWithErrorHandling(lines []string, testID string) []string { if containsCallMicroflow(trimmed) && strings.HasSuffix(trimmed, ";") { withoutSemicolon := strings.TrimSuffix(trimmed, ";") - result = append(result, withoutSemicolon+" ON ERROR {") + result = append(result, withoutSemicolon+" ON ERROR BEGIN") result = append(result, fmt.Sprintf( " LOG ERROR NODE 'MXTEST' 'MXTEST:FAIL:%s:Exception during execution';", escapeMDLString(testID))) result = append(result, " SET $TestFailed = true;") result = append(result, " SET $AllPassed = false;") result = append(result, " RETURN $AllPassed;") - result = append(result, "};") + result = append(result, "END ERROR;") } else if containsCallMicroflow(trimmed) && !strings.HasSuffix(trimmed, ";") { var callLines []string callLines = append(callLines, line) @@ -284,14 +284,14 @@ func rewriteWithErrorHandling(lines []string, testID string) []string { joined := strings.Join(callLines, "\n") joined = strings.TrimSpace(joined) joined = strings.TrimSuffix(joined, ";") - result = append(result, joined+" ON ERROR {") + result = append(result, joined+" ON ERROR BEGIN") result = append(result, fmt.Sprintf( " LOG ERROR NODE 'MXTEST' 'MXTEST:FAIL:%s:Exception during execution';", escapeMDLString(testID))) result = append(result, " SET $TestFailed = true;") result = append(result, " SET $AllPassed = false;") result = append(result, " RETURN $AllPassed;") - result = append(result, "};") + result = append(result, "END ERROR;") } else { result = append(result, line) } @@ -315,9 +315,9 @@ func rewriteForThrowsTest(lines []string, didThrowVar string, want string) []str if containsCallMicroflow(trimmed) && strings.HasSuffix(trimmed, ";") { withoutSemicolon := strings.TrimSuffix(trimmed, ";") - result = append(result, withoutSemicolon+" ON ERROR {") + result = append(result, withoutSemicolon+" ON ERROR BEGIN") result = append(result, throwsFlagHandler(didThrowVar, want)...) - result = append(result, "};") + result = append(result, "END ERROR;") } else if containsCallMicroflow(trimmed) && !strings.HasSuffix(trimmed, ";") { var callLines []string callLines = append(callLines, line) @@ -332,9 +332,9 @@ func rewriteForThrowsTest(lines []string, didThrowVar string, want string) []str joined := strings.Join(callLines, "\n") joined = strings.TrimSpace(joined) joined = strings.TrimSuffix(joined, ";") - result = append(result, joined+" ON ERROR {") + result = append(result, joined+" ON ERROR BEGIN") result = append(result, throwsFlagHandler(didThrowVar, want)...) - result = append(result, "};") + result = append(result, "END ERROR;") } else { result = append(result, line) } diff --git a/cmd/mxcli/testrunner/generator_endpoint.go b/cmd/mxcli/testrunner/generator_endpoint.go index 9f4633239..a49357ffa 100644 --- a/cmd/mxcli/testrunner/generator_endpoint.go +++ b/cmd/mxcli/testrunner/generator_endpoint.go @@ -82,11 +82,11 @@ func writeTestFlow(b *strings.Builder, tc TestCase) { // produces an assertion failure that says nothing about the code under test. func writeSetupCalls(b *strings.Builder, tc TestCase) { for _, flow := range tc.Setups { - fmt.Fprintf(b, " CALL MICROFLOW %s() ON ERROR {\n", flow) + fmt.Fprintf(b, " CALL MICROFLOW %s() ON ERROR BEGIN\n", flow) fmt.Fprintf(b, " SET $Verdict = '%s';\n", escapeMDLString(verdictSetupPrefix+flow)) b.WriteString(" RETURN $Verdict;\n") - b.WriteString(" };\n") + b.WriteString(" END ERROR;\n") } } @@ -228,7 +228,7 @@ func quoteForMessage(s string) string { return "'" + s + "'" } -// attachOnError appends `ON ERROR { ... }` to each CALL statement in the body, +// attachOnError appends `ON ERROR BEGIN ... END ERROR` to each CALL statement in the body, // joining a statement that spans several lines first. func attachOnError(lines, handler []string) []string { var out []string @@ -246,9 +246,9 @@ func attachOnError(lines, handler []string) []string { } stmt = strings.TrimSuffix(strings.TrimSpace(stmt), ";") - out = append(out, stmt+" ON ERROR {") + out = append(out, stmt+" ON ERROR BEGIN") out = append(out, handler...) - out = append(out, "};") + out = append(out, "END ERROR;") } return out } diff --git a/cmd/mxcli/testrunner/generator_endpoint_test.go b/cmd/mxcli/testrunner/generator_endpoint_test.go index 3bbbbbc74..cfaa930bd 100644 --- a/cmd/mxcli/testrunner/generator_endpoint_test.go +++ b/cmd/mxcli/testrunner/generator_endpoint_test.go @@ -94,7 +94,7 @@ func TestGenerateTestFlowsWrapsCallsWithErrorHandling(t *testing.T) { }}} mdl := GenerateTestFlows(suite) - if !strings.Contains(mdl, "ON ERROR {") { + if !strings.Contains(mdl, "ON ERROR BEGIN") { t.Errorf("the CALL was not wrapped in ON ERROR:\n%s", mdl) } if !strings.Contains(mdl, verdictFailPrefix+"exception during execution") { @@ -113,7 +113,7 @@ func TestGenerateTestFlowsThrowsTestStartsFailed(t *testing.T) { mdl := GenerateTestFlows(suite) failIdx := strings.Index(mdl, verdictFailPrefix+"expected an exception") - handlerIdx := strings.Index(mdl, "ON ERROR {") + handlerIdx := strings.Index(mdl, "ON ERROR BEGIN") if failIdx < 0 { t.Fatalf("no pre-set failure verdict:\n%s", mdl) } @@ -135,10 +135,10 @@ func TestGenerateTestFlowsMultiLineCall(t *testing.T) { }}} mdl := GenerateTestFlows(suite) - if !strings.Contains(mdl, ") ON ERROR {") { + if !strings.Contains(mdl, ") ON ERROR BEGIN") { t.Errorf("a statement spanning lines was not joined before ON ERROR was attached:\n%s", mdl) } - if strings.Count(mdl, "ON ERROR {") != 1 { + if strings.Count(mdl, "ON ERROR BEGIN") != 1 { t.Errorf("expected exactly one handler for one call:\n%s", mdl) } } diff --git a/cmd/mxcli/testrunner/generator_test.go b/cmd/mxcli/testrunner/generator_test.go index 4622a0676..2b0a434ec 100644 --- a/cmd/mxcli/testrunner/generator_test.go +++ b/cmd/mxcli/testrunner/generator_test.go @@ -101,3 +101,27 @@ func TestGenerateTestRunner_RenamesAllAssignmentsInTestBlock(t *testing.T) { } } } + +// The generated test flows are MDL mxcli executes itself, so their error +// handlers use the canonical `on error begin … end error` and record no +// deprecated spelling (MDL-DEPR540, ako/mxcli#754). +func TestGeneratedErrorHandlersAreCanonical(t *testing.T) { + suite := &TestSuite{Tests: []TestCase{ + {ID: "t1", Name: "call", MDL: "$r = CALL MICROFLOW Mod.A();", Setups: []string{"Mod.Setup"}}, + {ID: "t2", Name: "multi", MDL: "$r = CALL MICROFLOW Mod.A(\n X = 1\n);"}, + }} + for name, src := range map[string]string{ + "endpoint": GenerateTestFlows(suite), + "legacy": GenerateTestRunner(suite), + } { + prog, errs := visitor.Build(src) + if len(errs) > 0 { + t.Fatalf("%s: generated MDL does not parse: %v\n%s", name, errs[0], src) + } + for _, d := range prog.Deprecations { + if d.Code == "MDL-DEPR540" { + t.Errorf("%s: line %d uses the brace error handler:\n%s", name, d.Line, src) + } + } + } +} diff --git a/docs-site/src/appendixes/common-mistakes.md b/docs-site/src/appendixes/common-mistakes.md index 945330d48..7d70bd926 100644 --- a/docs-site/src/appendixes/common-mistakes.md +++ b/docs-site/src/appendixes/common-mistakes.md @@ -126,7 +126,7 @@ These constructs will cause parse errors: | Unsupported | Use Instead | |-------------|-------------| | `CASE ... WHEN 'String' ... ELSE ...` | Bare enum values, one branch per value including `(empty)` — `CASE` itself IS supported for enum splits | -| `TRY ... CATCH ... END TRY` | `ON ERROR { ... }` blocks on specific activities | +| `TRY ... CATCH ... END TRY` | `ON ERROR BEGIN ... END ERROR` blocks on specific activities | ## Boolean Attributes Must Have Defaults diff --git a/docs-site/src/appendixes/quick-reference.md b/docs-site/src/appendixes/quick-reference.md index 5747f21e2..73366dd89 100644 --- a/docs-site/src/appendixes/quick-reference.md +++ b/docs-site/src/appendixes/quick-reference.md @@ -170,14 +170,14 @@ AUTHENTICATION Basic, Session | WHILE | `WHILE condition BEGIN ... END WHILE;` | Condition-based loop | | Return | `RETURN $value;` | Required at end of every flow path | | Execute DB query | `$Result = EXECUTE DATABASE QUERY Module.Conn.Query;` | 3-part name; supports DYNAMIC, params, CONNECTION override | -| Error handling | `... ON ERROR CONTINUE\|ROLLBACK\|{ handler };` | Not supported on EXECUTE DATABASE QUERY | +| Error handling | `... ON ERROR CONTINUE\|ROLLBACK\|[WITHOUT ROLLBACK] BEGIN handler END ERROR;` | Not supported on EXECUTE DATABASE QUERY | ## Microflows - NOT Supported (Will Cause Parse Errors) | Unsupported | Use Instead | Notes | |-------------|-------------|-------| | `CASE ... WHEN 'String' ... ELSE ...` | Bare enum values, one branch per value | `CASE` itself IS supported for **enum splits**; what fails is quoted/qualified values, an `ELSE` branch (MDL008), and an `AS` alias | -| `TRY ... CATCH ... END TRY` | `ON ERROR { ... }` blocks | Use error handlers on specific activities | +| `TRY ... CATCH ... END TRY` | `ON ERROR BEGIN ... END ERROR` blocks | Use error handlers on specific activities | **Notes:** - `RETRIEVE ... FIRST` returns a single entity; `RETRIEVE ... LIMIT n [OFFSET n]` returns a list. A bare `LIMIT 1` is a list of one under `mdl 1;`, and without the header the single entity, with warning `MDL-V1-LIMIT1`. diff --git a/docs-site/src/examples/rest-integration.md b/docs-site/src/examples/rest-integration.md index 49afe9219..de482eac7 100644 --- a/docs-site/src/examples/rest-integration.md +++ b/docs-site/src/examples/rest-integration.md @@ -130,10 +130,10 @@ BEGIN HEADER Accept = 'application/json' TIMEOUT 30 RETURNS String - ON ERROR WITHOUT ROLLBACK { + ON ERROR WITHOUT ROLLBACK BEGIN LOG ERROR NODE 'Integration' 'API call failed: ' + $Url; RETURN false; - }; + END ERROR; SET $Success = true; RETURN $Success; diff --git a/docs-site/src/language/basics.md b/docs-site/src/language/basics.md index b33d43e2e..c17dada3c 100644 --- a/docs-site/src/language/basics.md +++ b/docs-site/src/language/basics.md @@ -61,10 +61,11 @@ What `mdl 1` makes strict (each is a warning without the header, with the code s | A `/` terminator line is an error. | Accepted; `MDL-V1-SLASH`. | | `''` is the only string escape; a backslash is an ordinary character, so `'C:\temp'` is that path. | `\n`, `\r`, `\t`, `\\` and `\'` are escapes; `MDL-V1-ESCAPE` for each literal whose value would change. | | In a REST client, published REST service, business event service, model, knowledge base, consumed MCP service or agent, an unknown property key is an error that names the key it most likely meant, and so is a value its key does not take (`Response: json from $X`). | The property is ignored, or read by its shape as before; `MDL-V1-PROP` / `MDL-V1-PROPVALUE`. | +| A `while` loop is `while begin … end while;`; leaving out `begin`, or the `while` after `end`, is an error. | Accepted; `MDL-V1-WHILE`. | ### Upgrading a script: `mxcli fmt --upgrade` -`mxcli fmt --upgrade` rewrites every deprecated spelling (the `MDL-DEPRnnn` warnings) to its canonical form — `create or replace` becomes `create or modify`, `show entities` becomes `list entities` — and changes nothing else: comments, layout and keyword case are kept. A deprecated use with no mechanical rewrite is reported and left in place. +`mxcli fmt --upgrade` rewrites every deprecated spelling (the `MDL-DEPRnnn` warnings) to its canonical form — `create or replace` becomes `create or modify`, `show entities` becomes `list entities`, `on error { … }` becomes `on error begin … end error` — and changes nothing else: comments, layout and keyword case are kept. A deprecated use with no mechanical rewrite is reported and left in place. ```bash mxcli fmt --upgrade script.mdl # print the upgraded script @@ -83,6 +84,7 @@ mxcli fmt --upgrade --header -w script.mdl # also add `mdl 1;` | `MDL-V1-SET` | `$x = …` becomes `set $x = …` | | `MDL-V1-LIST`, `MDL-DEPR003`, `MDL-DEPR004` | a list operation or aggregate call becomes its statement form (`$x = filter($L, …)` → `$x = filter $L where …`); `find`/`contains` on a declared String keeps the call and gains `set` | | `MDL-V1-REPLACE02` | `create or replace user role` / `demo user` becomes a plain `create` | +| `MDL-V1-WHILE` | inserts the missing `begin` after a `while` condition and `while` after its `end` | A construct with no mechanical rewrite is reported with the reason, and `fmt` refuses to add the header rather than change the script's meaning: an unknown or mis-shaped property (`MDL-V1-PROP`, `MDL-V1-PROPVALUE`), `create or replace view entity` (`MDL-V1-REPLACE01`), a nested list operation such as `count(filter(…))`, `find`/`contains` on a variable whose type the script does not state, and an escaped line break (`\n`) inside an expression. While `mdl 1` is a preview, the header is added only when asked. Running `fmt --upgrade` on its own output changes nothing. diff --git a/docs-site/src/language/control-flow.md b/docs-site/src/language/control-flow.md index 06201caa5..f87ac5f4c 100644 --- a/docs-site/src/language/control-flow.md +++ b/docs-site/src/language/control-flow.md @@ -170,6 +170,8 @@ END WHILE; > **Caution:** Ensure the condition will eventually become false to avoid infinite loops. +`BEGIN` and `END WHILE` are required under the `mdl 1;` header, as they are for `LOOP`. A script without the header may still leave either out, and `check` warns `MDL-V1-WHILE`. + ## Error Handling ### ON ERROR Suffix @@ -197,14 +199,16 @@ DELETE $Order ON ERROR ROLLBACK; Executes a custom error handling block when the activity fails: ```sql -COMMIT $Order ON ERROR { +COMMIT $Order ON ERROR BEGIN LOG ERROR 'Failed to commit order: ' + $Order/OrderNumber; ROLLBACK $Order; -}; +END ERROR; ``` The handler block can contain any activities -- logging, rollback, showing validation messages, etc. +The handler is flow, so it is written `BEGIN … END ERROR` like every other flow block; add `WITHOUT ROLLBACK` before `BEGIN` to keep the database changes made so far. The older brace form, `ON ERROR { … }`, still parses and builds the same handler, with warning `MDL-DEPR540`; `mxcli fmt --upgrade` rewrites it. + A handler that does not end in `RETURN` or `RAISE ERROR` falls through: after it runs, the microflow continues with the statement after the activity, through a merge placed where the two paths meet. `DESCRIBE` writes the merge's position as @@ -221,10 +225,10 @@ RETRIEVE $Config FROM Admin.SystemConfig LIMIT 1 ON ERROR CONTINUE; -- Custom error handler for external call $Response = CALL MICROFLOW Integration.CallExternalAPI ( Payload = $RequestBody -) ON ERROR { +) ON ERROR BEGIN LOG ERROR NODE 'Integration' 'External API call failed'; SET $Response = empty; -}; +END ERROR; -- Rollback on commit failure COMMIT $Order ON ERROR ROLLBACK; @@ -236,13 +240,13 @@ COMMIT $Order ON ERROR ROLLBACK; `RAISE ERROR` builds Mendix's **error event**, which re-raises the error currently being handled. Mendix allows one only where an error is in scope, so it belongs -inside an `ON ERROR { ... }` block: +inside an `ON ERROR BEGIN ... END ERROR` block: ```sql -CALL MICROFLOW Integration.CallExternalAPI (Payload = $Body) ON ERROR { +CALL MICROFLOW Integration.CallExternalAPI (Payload = $Body) ON ERROR BEGIN LOG ERROR NODE 'Integration' 'API call failed, re-raising'; RAISE ERROR; -}; +END ERROR; ``` On the **main flow** it is refused as **MDL084**, at any nesting depth — inside an @@ -331,7 +335,7 @@ The following constructs are **not** supported in MDL and will cause parse error | Unsupported | Use Instead | |-------------|-------------| | `CASE ... WHEN 'String' ... ELSE ...` | Bare enum values and a branch per value — see [CASE (Enum Split)](#case-enum-split); `CASE` itself is supported | -| `TRY ... CATCH ... END TRY` | `ON ERROR { ... }` blocks on individual activities | +| `TRY ... CATCH ... END TRY` | `ON ERROR BEGIN ... END ERROR` blocks on individual activities | ## Complete Example @@ -356,11 +360,11 @@ BEGIN @caption 'Process order' CALL MICROFLOW Sales.SUB_ProcessSingleOrder ( Order = $Order - ) ON ERROR { + ) ON ERROR BEGIN LOG ERROR 'Failed to process order: ' + $Order/OrderNumber; SET $ErrorCount = $ErrorCount + 1; CONTINUE; - }; + END ERROR; SET $SuccessCount = $SuccessCount + 1; END LOOP; diff --git a/docs-site/src/language/microflow-patterns.md b/docs-site/src/language/microflow-patterns.md index 469041b48..7b07507dc 100644 --- a/docs-site/src/language/microflow-patterns.md +++ b/docs-site/src/language/microflow-patterns.md @@ -135,11 +135,11 @@ BEGIN BEGIN CALL MICROFLOW Sales.SUB_ProcessOrder ( Order = $Order - ) ON ERROR { + ) ON ERROR BEGIN LOG ERROR NODE 'BatchProcess' 'Failed to process order: ' + $Order/OrderNumber; SET $ErrorCount = $ErrorCount + 1; CONTINUE; - }; + END ERROR; SET $SuccessCount = $SuccessCount + 1; END LOOP; @@ -266,19 +266,19 @@ BEGIN @caption 'Call external API' $Response = CALL MICROFLOW Integration.SUB_CallExternalAPI ( Config = $Config - ) ON ERROR { + ) ON ERROR BEGIN LOG ERROR NODE 'Integration' 'External API call failed'; RETURN false; - }; + END ERROR; IF $Response != empty THEN @caption 'Process response' CALL MICROFLOW Integration.SUB_ProcessResponse ( Response = $Response - ) ON ERROR { + ) ON ERROR BEGIN LOG ERROR NODE 'Integration' 'Failed to process response'; RETURN false; - }; + END ERROR; SET $Success = true; END IF; diff --git a/docs-site/src/reference/microflow/create-microflow.md b/docs-site/src/reference/microflow/create-microflow.md index 24d4e4be4..f991b6ba2 100644 --- a/docs-site/src/reference/microflow/create-microflow.md +++ b/docs-site/src/reference/microflow/create-microflow.md @@ -137,12 +137,12 @@ RETURN $value; -- Suffix on any activity (except EXECUTE DATABASE QUERY) activity ON ERROR CONTINUE; activity ON ERROR ROLLBACK; -activity ON ERROR { +activity ON ERROR BEGIN handler_statements -}; +END ERROR; ``` -Error handling is attached as a suffix to an individual activity. `ON ERROR CONTINUE` suppresses the error and continues. `ON ERROR ROLLBACK` rolls back the current transaction. `ON ERROR { ... }` executes custom error-handling logic. +Error handling is attached as a suffix to an individual activity. `ON ERROR CONTINUE` suppresses the error and continues. `ON ERROR ROLLBACK` rolls back the current transaction. `ON ERROR BEGIN ... END ERROR` executes custom error-handling logic. ### Annotations @@ -257,11 +257,11 @@ BEGIN $Result = CALL MICROFLOW Integration.SUB_FetchExternalData ( SyncResult = $Result - ) ON ERROR { + ) ON ERROR BEGIN CHANGE $Result (Status = 'Failed'); COMMIT $Result; LOG ERROR NODE 'Integration' 'Sync failed'; - }; + END ERROR; CHANGE $Result ( Status = 'Completed', diff --git a/docs-site/src/tutorial/create-microflow.md b/docs-site/src/tutorial/create-microflow.md index 56bbec038..7921d6d88 100644 --- a/docs-site/src/tutorial/create-microflow.md +++ b/docs-site/src/tutorial/create-microflow.md @@ -162,10 +162,10 @@ COMMIT $Product ON ERROR CONTINUE; Options are `CONTINUE` (ignore the error and proceed), `ROLLBACK` (roll back the transaction and continue), or an inline error handler block: ```sql -COMMIT $Product ON ERROR { +COMMIT $Product ON ERROR BEGIN LOG ERROR 'Failed to commit product'; RETURN false; -}; +END ERROR; ``` ## Organizing with folders diff --git a/docs/01-project/MDL_QUICK_REFERENCE.md b/docs/01-project/MDL_QUICK_REFERENCE.md index f9add0af0..9ba1e3502 100644 --- a/docs/01-project/MDL_QUICK_REFERENCE.md +++ b/docs/01-project/MDL_QUICK_REFERENCE.md @@ -589,9 +589,9 @@ it is for pages. | Execute DB query | `$Result = execute database query Module.Conn.Query;` | 3-part name; supports DYNAMIC, params, CONNECTION override | | Import mapping | `[$Var =] import from mapping Module.IMM($SourceVar) [all\|first\|limit [offset ]];` | Apply import mapping to string variable. Trailing clause is Studio Pro's Range; omitted = infer from the mapping's root. `first` binds one OBJECT (`limit 1` is a one-element LIST). Mendix rejects `offset` on a non-list mapping (CE6100) | | Export mapping | `$Var = export to mapping Module.EMM($EntityVar);` | Apply export mapping to entity, returns string | -| Error handling | `... on error continue\|rollback\|{ handler }\|without rollback { handler };` | Goes on the activity that may fail — including `declare`, `set`, `change`, `log`, `show page`, `close page`, `show message` and `validation feedback`, which gained it in mendixlabs/mxcli#1078 so a Studio Pro handler survives DESCRIBE. `on error continue` is refused (MDL076) where Mendix raises CE6035: create, change, commit, log, show page, close page, show message, validation feedback — a custom `{ handler }` is accepted on all of them. The list-operation and aggregate forms of `set` have no error handling at all (MDL077). Not supported on EXECUTE DATABASE QUERY. **In a nanoflow** only `declare` and `set` take a clause at all — `change`, `log`, `show page`, `close page`, `show message` and `validation feedback` are CE6035 there in every form, and are refused. A handler that does not end in `return`/`raise error` merges back into the main flow, so a later variable is out of scope on the error path (CE0108) | -| Re-raise the error | `raise error;` | **Inside an `on error { … }` handler only.** The error event re-raises the error being handled, so Mendix needs one in scope; Studio Pro will not draw the shape and mxbuild rejects it with **CE0710** "The main flow cannot join an error flow or end in an error event". On the main flow — at any nesting depth, and in a rule too — it is **MDL084**. Mendix has no main-flow "throw": call a Java action that throws | -| Named join point | `merge