Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .claude/skills/mendix/write-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,18 @@ commit $Product with events refresh;

**Best Practice**: Use `with events` when you want before/after commit event handlers to execute. Use `refresh` when the committed object is displayed in the client and you want the UI to update immediately.

## List Operations

```mdl
-- Existing variable form
add $Item to $Items;

-- Expression-valued add, useful when round-tripping Studio Pro list-add values
add head($SourceItems) to $Items;
```

Use expression-valued `add` only when the expression returns an object compatible with the target list element type.

## Database Operations

### RETRIEVE Statement
Expand Down
1 change: 1 addition & 0 deletions docs/01-project/MDL_QUICK_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ authentication basic, session
| Rollback | `rollback $entity [refresh];` | Reverts uncommitted changes |
| Retrieve (DB) | `retrieve $Var from Module.Entity [where condition];` | Database XPath retrieve |
| Retrieve (Assoc) | `retrieve $list from $Parent/Module.AssocName;` | Retrieve by association |
| Add to list | `add expression to $list;` | Also accepts existing `add $item to $list;` form |
| Call microflow | `$Result = call microflow Module.Name (Param = $value);` | |
| Call nanoflow | `$Result = call nanoflow Module.Name (Param = $value);` | |
| Show page | `show page Module.PageName ($Param = $value);` | Also accepts `(Param: $value)` |
Expand Down
33 changes: 33 additions & 0 deletions docs/11-proposals/PROPOSAL_microflow_add_expression_to_list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Proposal: Microflow ADD Expression To List

Status: Draft

## Summary

Allow `add` microflow statements to use any expression as the value being added to a list:

```mdl
add head($SourceList) to $TargetList;
```

The existing variable-only form remains valid:

```mdl
add $Item to $TargetList;
```

## Motivation

Studio Pro stores the value of a list-add action as an expression string. Existing models can therefore contain a list-add value that is not a bare variable. MDL previously parsed only `add $Item to $List`, so describe/exec round-trips could not preserve expression-valued list additions.

## Semantics

The parser stores the add value as an expression. For compatibility, a bare variable expression also populates the legacy `Item` field in the AST. The builder writes the expression source to the Mendix `ChangeListAction.Value` field and falls back to the legacy item variable only when no expression is present.

## Tests And Examples

`mdl-examples/doctype-tests/add_expression_to_list.test.mdl` demonstrates adding `head($SourceList)` to another list. Go regression tests cover parser behavior and builder output for both expression and simple-variable forms.

## Open Questions

- Should validation infer the list element type and reject expressions that cannot produce an object compatible with the target list?
1 change: 1 addition & 0 deletions docs/11-proposals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ BSON schema Registry ◄──── multi-version Support
| [Page Styling Support](page-styling-support.md) | Partial | CSS classes, inline styles, dynamic classes, design properties. Phase 1 (Class/Style) done | — |
| [Page Composition](proposal_page_composition.md) | Proposed | Fragment definitions and ALTER PAGE for partial page editing | Page Syntax V2, Page Styling |
| [XPath Gaps](xpath-gaps-proposal.md) | Partial | XPath constraint support gap analysis. ~85% complete, association paths and nested predicates remain | — |
| [Microflow ADD Expression To List](PROPOSAL_microflow_add_expression_to_list.md) | Draft | Preserve expression-valued list-add actions in microflow round-trips | — |
| [LLM MDL Assistance](PROPOSAL_llm_mdl_assistance.md) | Proposed | Enhanced error messages with examples, reorganized skills by use case | — |

### Testing & Evaluation
Expand Down
17 changes: 17 additions & 0 deletions mdl-examples/doctype-tests/add_expression_to_list.test.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
create module AddListExample;

create persistent entity AddListExample.Item (
Name: string
);
/

create microflow AddListExample.AddFirstItem (
$SourceItems: list of AddListExample.Item
)
returns list of AddListExample.Item as $Items
begin
$Items = create list of AddListExample.Item;
add head($SourceItems) to $Items;
return $Items;
end;
/
5 changes: 3 additions & 2 deletions mdl/ast/ast_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,9 +504,10 @@ type CreateListStmt struct {

func (s *CreateListStmt) isMicroflowStatement() {}

// AddToListStmt represents: ADD $Item TO $List
// AddToListStmt represents: ADD expr TO $List
type AddToListStmt struct {
Item string // Item variable to add
Item string // Item variable to add, kept for simple $Var compatibility
Value Expression // Item expression to add
List string // Target list variable
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
}
Expand Down
6 changes: 5 additions & 1 deletion mdl/executor/cmd_microflows_builder_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -686,11 +686,15 @@ func (fb *flowBuilder) addCreateListAction(s *ast.CreateListStmt) model.ID {

// addAddToListAction creates an ADD TO list statement.
func (fb *flowBuilder) addAddToListAction(s *ast.AddToListStmt) model.ID {
value := fb.exprToString(s.Value)
if value == "" && s.Item != "" {
value = "$" + s.Item
}
action := &microflows.ChangeListAction{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Type: microflows.ChangeListTypeAdd,
ChangeVariable: s.List,
Value: "$" + s.Item,
Value: value,
}

activity := &microflows.ActionActivity{
Expand Down
58 changes: 58 additions & 0 deletions mdl/executor/cmd_microflows_builder_add_to_list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: Apache-2.0

package executor

import (
"testing"

"github.com/mendixlabs/mxcli/mdl/ast"
"github.com/mendixlabs/mxcli/sdk/microflows"
)

func TestAddToListBuilderUsesExpressionValue(t *testing.T) {
fb := &flowBuilder{}

fb.addAddToListAction(&ast.AddToListStmt{
Value: &ast.AttributePathExpr{
Variable: "Order",
Path: []string{"Number"},
},
List: "Numbers",
})

action := lastChangeListAction(t, fb)
if action.Value != "$Order/Number" {
t.Fatalf("Value = %q, want $Order/Number", action.Value)
}
}

func TestAddToListBuilderKeepsSimpleVariableFallback(t *testing.T) {
fb := &flowBuilder{}

fb.addAddToListAction(&ast.AddToListStmt{
Item: "Order",
List: "Orders",
})

action := lastChangeListAction(t, fb)
if action.Value != "$Order" {
t.Fatalf("Value = %q, want $Order", action.Value)
}
}

func lastChangeListAction(t *testing.T, fb *flowBuilder) *microflows.ChangeListAction {
t.Helper()

if len(fb.objects) == 0 {
t.Fatal("Expected builder to create an action activity")
}
activity, ok := fb.objects[len(fb.objects)-1].(*microflows.ActionActivity)
if !ok {
t.Fatalf("Last object = %T, want ActionActivity", fb.objects[len(fb.objects)-1])
}
action, ok := activity.Action.(*microflows.ChangeListAction)
if !ok {
t.Fatalf("Action = %T, want ChangeListAction", activity.Action)
}
return action
}
2 changes: 1 addition & 1 deletion mdl/grammar/MDLParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -1867,7 +1867,7 @@ createListStatement
* ```
*/
addToListStatement
: ADD VARIABLE TO VARIABLE
: ADD expression TO VARIABLE
;

/**
Expand Down
2 changes: 1 addition & 1 deletion mdl/grammar/parser/MDLParser.interp

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion mdl/grammar/parser/mdl_lexer.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 22 additions & 14 deletions mdl/grammar/parser/mdl_parser.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion mdl/grammar/parser/mdlparser_base_listener.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion mdl/grammar/parser/mdlparser_listener.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 70 additions & 0 deletions mdl/visitor/visitor_add_to_list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: Apache-2.0

package visitor

import (
"testing"

"github.com/mendixlabs/mxcli/mdl/ast"
)

func TestAddToListAcceptsExpressionValue(t *testing.T) {
input := `CREATE MICROFLOW Sales.CollectLabels ($Order: Sales.Order)
RETURNS Boolean
BEGIN
DECLARE $Labels List of String = empty;
ADD $Order/Number TO $Labels;
RETURN true;
END;`

prog, errs := Build(input)
if len(errs) > 0 {
for _, err := range errs {
t.Errorf("Parse error: %v", err)
}
return
}

mf := prog.Statements[0].(*ast.CreateMicroflowStmt)
addStmt, ok := mf.Body[1].(*ast.AddToListStmt)
if !ok {
t.Fatalf("Expected AddToListStmt, got %T", mf.Body[1])
}
if addStmt.List != "Labels" {
t.Fatalf("List = %q, want Labels", addStmt.List)
}
path, ok := addStmt.Value.(*ast.AttributePathExpr)
if !ok {
t.Fatalf("Value = %T, want AttributePathExpr", addStmt.Value)
}
if path.Variable != "Order" || len(path.Path) != 1 || path.Path[0] != "Number" {
t.Fatalf("Value path = %#v, want $Order/Number", path)
}
}

func TestAddToListKeepsSimpleVariableCompatibility(t *testing.T) {
input := `CREATE MICROFLOW Sales.CollectOrders ($Order: Sales.Order)
RETURNS Boolean
BEGIN
DECLARE $Orders List of Sales.Order = empty;
ADD $Order TO $Orders;
RETURN true;
END;`

prog, errs := Build(input)
if len(errs) > 0 {
for _, err := range errs {
t.Errorf("Parse error: %v", err)
}
return
}

mf := prog.Statements[0].(*ast.CreateMicroflowStmt)
addStmt, ok := mf.Body[1].(*ast.AddToListStmt)
if !ok {
t.Fatalf("Expected AddToListStmt, got %T", mf.Body[1])
}
if addStmt.Item != "Order" {
t.Fatalf("Item = %q, want Order", addStmt.Item)
}
}
15 changes: 8 additions & 7 deletions mdl/visitor/visitor_microflow_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ func buildCreateListStatement(ctx parser.ICreateListStatementContext) *ast.Creat
}

// buildAddToListStatement converts add to list statement context to AddToListStmt.
// Grammar: ADD VARIABLE TO VARIABLE
// Grammar: ADD expression TO VARIABLE
func buildAddToListStatement(ctx parser.IAddToListStatementContext) *ast.AddToListStmt {
if ctx == nil {
return nil
Expand All @@ -642,13 +642,14 @@ func buildAddToListStatement(ctx parser.IAddToListStatementContext) *ast.AddToLi

stmt := &ast.AddToListStmt{}

// Get both variables
vars := addCtx.AllVARIABLE()
if len(vars) >= 1 {
stmt.Item = strings.TrimPrefix(vars[0].GetText(), "$")
if expr := addCtx.Expression(); expr != nil {
stmt.Value = buildExpression(expr)
if varExpr, ok := stmt.Value.(*ast.VariableExpr); ok {
stmt.Item = varExpr.Name
}
}
if len(vars) >= 2 {
stmt.List = strings.TrimPrefix(vars[1].GetText(), "$")
if v := addCtx.VARIABLE(); v != nil {
stmt.List = strings.TrimPrefix(v.GetText(), "$")
}

return stmt
Expand Down
Loading