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
20 changes: 20 additions & 0 deletions .claude/skills/mendix/write-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,26 @@ download file $GeneratedReport show in browser;
download file $GeneratedExport;
```

## Empty Java-Action Argument (`empty`)

When `describe` round-trips a Java-action call that has an unbound parameter
in Studio Pro, it emits `empty` as the argument value. In this Java-action
argument context, `empty` preserves the
underlying empty `BasicCodeActionParameterValue.Argument` so that the next
`describe → exec → describe` cycle stays symmetric.

```mdl
$Total = call java action SampleModule.Recalculate(
CompanyId = empty,
RecalculateAll = true,
ItemList = empty
);
```

New scripts should bind every parameter to a real expression. Use `empty`
for a Java-action argument only when regenerating MDL from an existing project
that already had an unbound parameter.

## Error Handling

MDL supports error handling for activities that may fail (microflow calls, commits, external service calls, etc.).
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 @@ -784,6 +784,7 @@ Module.OrderResponse_CustomerInfo/Module.CustomerInfo as customer {
| Create exposed action | `... exposed as 'caption' in 'Category' as $$ ... $$;` | Toolbox-visible in Studio Pro |
| Drop Java action | `drop java action Module.Name;` | Delete a Java action |
| Call from microflow | `$Result = call java action Module.Name(Param = value);` | Inside BEGIN...END |
| Empty argument | `call java action Module.Name(Param = empty);` | Unbound code-action parameter preserved as empty mapping |

**Parameter Types:** `string`, `integer`, `long`, `decimal`, `boolean`, `datetime`, `Module.Entity`, `list of Module.Entity`, `enum Module.EnumName`, `enumeration(Module.EnumName)`, `stringtemplate(sql)`, `stringtemplate(Oql)`, `entity <pEntity>` (type parameter declaration), bare `pEntity` (type parameter reference).

Expand Down
73 changes: 73 additions & 0 deletions docs/11-proposals/PROPOSAL_microflow_empty_java_action_argument.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Empty Java Action Argument

Status: Implemented

## Summary

Use the existing MDL `empty` literal to represent an intentionally unbound Java
action argument in microflow call statements.

```mdl
$Total = call java action SampleModule.Recalculate(
CompanyId = empty,
RecalculateAll = true,
ItemList = empty
);
```

In this Java-action argument context, `empty` produces a parameter binding with
an empty `Argument` string in the serialized BSON
(`Microflows$BasicCodeActionParameterValue.Argument = ""`). Re-executing the
script reproduces the same empty binding, so `describe -> exec -> describe`
stays symmetric for existing Studio Pro projects that have unbound code-action
parameters.

## Motivation

Studio Pro's Java-action call dialog allows a developer to leave individual
parameters empty. The on-disk representation is a
`Microflows$JavaActionParameterMapping` whose value is a
`BasicCodeActionParameterValue` with `Argument: ""`.

Emitting `''` would create a literal empty string expression, not an unbound
parameter. Dropping the parameter would lose the original mapping. The existing
`empty` literal is already valid MDL expression syntax and is clearer than
introducing a new placeholder token for this one case.

## Semantics

- In Java-action call arguments, `empty` maps to an empty
`BasicCodeActionParameterValue.Argument`.
- If the Java action parameter type is a microflow callback, `empty` maps to a
`Microflows$MicroflowParameterValue` with an empty `Microflow` reference.
- Outside Java-action call arguments, `empty` keeps its normal MDL literal
meaning.

## Examples

```mdl
-- Java action call with two unbound and one bound argument.
$Total = call java action SampleModule.Recalculate(
CompanyId = empty,
RecalculateAll = true,
ItemList = empty
);
```

The Mendix BSON for the unbound arguments is:

```text
JavaActionParameterMapping {
Parameter: 'SampleModule.Recalculate.CompanyId',
Value: BasicCodeActionParameterValue { Argument: '' }
}
```

## Tests And Examples

- Builder coverage: `TestBuildJavaAction_EmptyArgumentPreservesEmptyBasicValue`
and `TestBuildJavaAction_EmptyMicroflowArgumentUsesMicroflowParameterValue`
in `mdl/executor/cmd_microflows_builder_java_action_test.go`.
- Example script:
`mdl-examples/doctype-tests/empty_java_action_argument.test.mdl`.

1 change: 1 addition & 0 deletions docs/11-proposals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ BSON schema Registry ◄──── multi-version Support
| [MDL Syntax Improvements v2](PROPOSAL_mdl_syntax_improvements_v2.md) | Proposed | Consolidated v2: unified variable declaration, C-style braces, fluent list ops | Syntax Improvements v1 |
| [Microflow Free Annotation](PROPOSAL_microflow_free_annotation.md) | Draft | Order-sensitive `@annotation` handling for free-floating visual notes in microflows | — |
| [Microflow Download File Statement](PROPOSAL_microflow_download_file_statement.md) | Draft | `download file $FileDocument [show in browser]` for `DownloadFileAction` round-trip and authoring | — |
| [Empty Java Action Argument](PROPOSAL_microflow_empty_java_action_argument.md) | Implemented | `empty` for unbound Java-action parameters; round-trip preservation of empty `BasicCodeActionParameterValue` bindings | — |
| [Page Syntax V2](PROPOSAL_page_syntax_v2.md) | Superseded | Page/widget syntax with `{}` blocks and `->` binding. Superseded by V3 (archived) | — |
| [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 |
Expand Down
25 changes: 25 additions & 0 deletions mdl-examples/doctype-tests/empty_java_action_argument.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
create microflow SampleModule.ACT_RecalculateOpenItems ()
returns Void
begin
call java action SampleModule.Recalculate(
CompanyId = empty,
RecalculateAll = true,
ItemList = empty
);
return;
end;
/

create microflow SampleModule.ACT_RecalculateForCompany (
$CompanyId: String
)
returns Void
begin
call java action SampleModule.Recalculate(
CompanyId = $CompanyId,
RecalculateAll = false,
ItemList = empty
);
return;
end;
/
33 changes: 30 additions & 3 deletions mdl/executor/cmd_microflows_builder_calls.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,13 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model.

// Build a map of parameter name -> param type for the Java action
entityTypeParams := make(map[string]bool)
microflowTypeParams := make(map[string]bool)
if jaDef != nil {
for _, p := range jaDef.Parameters {
if _, ok := p.ParameterType.(*javaactions.EntityTypeParameterType); ok {
entityTypeParams[p.Name] = true
} else if _, ok := p.ParameterType.(*javaactions.MicroflowType); ok {
microflowTypeParams[p.Name] = true
}
}
}
Expand Down Expand Up @@ -276,12 +279,31 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model.
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Entity: entityName,
}
} else if isPlaceholderExpression(arg.Value) {
if microflowTypeParams[arg.Name] {
value = &microflows.MicroflowParameterValue{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Microflow: "",
}
} else {
value = &microflows.BasicCodeActionParameterValue{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Argument: "",
}
}
} else {
// Regular parameter: expression-based value
valueExpr := fb.exprToString(arg.Value)
value = &microflows.BasicCodeActionParameterValue{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Argument: valueExpr,
if microflowTypeParams[arg.Name] {
value = &microflows.MicroflowParameterValue{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Microflow: strings.Trim(valueExpr, "'"),
}
} else {
value = &microflows.BasicCodeActionParameterValue{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Argument: valueExpr,
}
}
}

Expand Down Expand Up @@ -437,6 +459,11 @@ func (fb *flowBuilder) addCallJavaScriptActionAction(s *ast.CallJavaScriptAction
return activity.ID
}

func isPlaceholderExpression(expr ast.Expression) bool {
lit, ok := expr.(*ast.LiteralExpr)
return ok && (lit.Kind == ast.LiteralEmpty || lit.Kind == ast.LiteralNull)
}

// addCallExternalActionAction creates a CALL EXTERNAL ACTION statement.
func (fb *flowBuilder) addCallExternalActionAction(s *ast.CallExternalActionStmt) model.ID {
serviceQN := s.ServiceName.Module + "." + s.ServiceName.Name
Expand Down
116 changes: 116 additions & 0 deletions mdl/executor/cmd_microflows_builder_java_action_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// SPDX-License-Identifier: Apache-2.0

package executor

import (
"testing"

"github.com/mendixlabs/mxcli/mdl/ast"
"github.com/mendixlabs/mxcli/mdl/backend/mock"
"github.com/mendixlabs/mxcli/model"
"github.com/mendixlabs/mxcli/sdk/javaactions"
"github.com/mendixlabs/mxcli/sdk/microflows"
)

func TestBuildJavaAction_EmptyArgumentPreservesEmptyBasicValue(t *testing.T) {
fb := &flowBuilder{posX: 100, posY: 100, spacing: HorizontalSpacing}
stmt := &ast.CallJavaActionStmt{
ActionName: ast.QualifiedName{Module: "SampleModule", Name: "Recalculate"},
Arguments: []ast.CallArgument{
{Name: "CompanyId", Value: &ast.LiteralExpr{Kind: ast.LiteralEmpty}},
{Name: "RecalculateAll", Value: &ast.LiteralExpr{Kind: ast.LiteralBoolean, Value: true}},
{Name: "ItemList", Value: &ast.LiteralExpr{Kind: ast.LiteralEmpty}},
},
}

id := fb.addCallJavaActionAction(stmt)
var activity *microflows.ActionActivity
for _, obj := range fb.objects {
if obj.GetID() == id {
activity, _ = obj.(*microflows.ActionActivity)
break
}
}
if activity == nil {
t.Fatal("expected Java action activity")
}
action, ok := activity.Action.(*microflows.JavaActionCallAction)
if !ok {
t.Fatalf("action = %T, want *JavaActionCallAction", activity.Action)
}
if len(action.ParameterMappings) != 3 {
t.Fatalf("parameter mappings = %d, want 3", len(action.ParameterMappings))
}

for _, idx := range []int{0, 2} {
value, ok := action.ParameterMappings[idx].Value.(*microflows.BasicCodeActionParameterValue)
if !ok {
t.Fatalf("mapping %d value = %T, want *BasicCodeActionParameterValue", idx, action.ParameterMappings[idx].Value)
}
if value.Argument != "" {
t.Fatalf("mapping %d argument = %q, want empty string", idx, value.Argument)
}
}

value, ok := action.ParameterMappings[1].Value.(*microflows.BasicCodeActionParameterValue)
if !ok {
t.Fatalf("boolean mapping value = %T, want *BasicCodeActionParameterValue", action.ParameterMappings[1].Value)
}
if value.Argument != "true" {
t.Fatalf("boolean argument = %q, want true", value.Argument)
}
}

func TestBuildJavaAction_EmptyMicroflowArgumentUsesMicroflowParameterValue(t *testing.T) {
fb := &flowBuilder{
posX: 100,
posY: 100,
spacing: HorizontalSpacing,
backend: &mock.MockBackend{
ReadJavaActionByNameFunc: func(qualifiedName string) (*javaactions.JavaAction, error) {
if qualifiedName != "SampleModule.StartAsync" {
t.Fatalf("java action lookup = %q", qualifiedName)
}
return &javaactions.JavaAction{
Parameters: []*javaactions.JavaActionParameter{
{
Name: "Callback",
ParameterType: &javaactions.MicroflowType{
BaseElement: model.BaseElement{ID: "param-type"},
},
},
},
}, nil
},
},
}
stmt := &ast.CallJavaActionStmt{
ActionName: ast.QualifiedName{Module: "SampleModule", Name: "StartAsync"},
Arguments: []ast.CallArgument{
{Name: "Callback", Value: &ast.LiteralExpr{Kind: ast.LiteralEmpty}},
},
}

id := fb.addCallJavaActionAction(stmt)
var activity *microflows.ActionActivity
for _, obj := range fb.objects {
if obj.GetID() == id {
activity, _ = obj.(*microflows.ActionActivity)
break
}
}
if activity == nil {
t.Fatal("expected Java action activity")
}
action, ok := activity.Action.(*microflows.JavaActionCallAction)
if !ok {
t.Fatalf("action = %T, want *JavaActionCallAction", activity.Action)
}
value, ok := action.ParameterMappings[0].Value.(*microflows.MicroflowParameterValue)
if !ok {
t.Fatalf("mapping value = %T, want *MicroflowParameterValue", action.ParameterMappings[0].Value)
}
if value.Microflow != "" {
t.Fatalf("placeholder microflow = %q, want empty string", value.Microflow)
}
}
12 changes: 11 additions & 1 deletion mdl/executor/cmd_microflows_format_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,17 @@ func formatAction(
case *microflows.ExpressionBasedCodeActionParameterValue:
valueStr = v.Expression
case *microflows.BasicCodeActionParameterValue:
valueStr = v.Argument
if v.Argument == "" {
valueStr = "empty"
} else {
valueStr = v.Argument
}
case *microflows.MicroflowParameterValue:
if v.Microflow != "" {
valueStr = mdlQuote(v.Microflow)
} else {
valueStr = "empty"
}
case *microflows.EntityTypeCodeActionParameterValue:
if v.Entity != "" {
valueStr = mdlQuote(v.Entity)
Expand Down
26 changes: 26 additions & 0 deletions mdl/executor/cmd_microflows_format_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,32 @@ func TestFormatAction_JavaActionCall(t *testing.T) {
}
}

func TestFormatAction_JavaActionCall_EmptyParameterValues(t *testing.T) {
e := newTestExecutor()
action := &microflows.JavaActionCallAction{
JavaAction: "MyModule.Recalculate",
ParameterMappings: []*microflows.JavaActionParameterMapping{
{
Parameter: "MyModule.Recalculate.CompanyId",
Value: &microflows.BasicCodeActionParameterValue{Argument: ""},
},
{
Parameter: "MyModule.Recalculate.RecalculateAll",
Value: &microflows.BasicCodeActionParameterValue{Argument: "true"},
},
{
Parameter: "MyModule.Recalculate.Callback",
Value: &microflows.MicroflowParameterValue{Microflow: ""},
},
},
}
got := e.formatAction(action, nil, nil)
want := "call java action MyModule.Recalculate(CompanyId = empty, RecalculateAll = true, Callback = empty);"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}

func TestFormatAction_CallExternal(t *testing.T) {
e := newTestExecutor()
action := &microflows.CallExternalAction{
Expand Down
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.

2 changes: 1 addition & 1 deletion 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.

Loading
Loading