Skip to content

Commit d091837

Browse files
authored
Merge pull request #336 from hjotha/submit/microflow-ellipsis-placeholder-expression
feat: support empty Java action arguments
2 parents 986b455 + 916d4a2 commit d091837

16 files changed

Lines changed: 392 additions & 5 deletions

.claude/skills/mendix/write-microflows.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,26 @@ download file $GeneratedReport show in browser;
885885
download file $GeneratedExport;
886886
```
887887

888+
## Empty Java-Action Argument (`empty`)
889+
890+
When `describe` round-trips a Java-action call that has an unbound parameter
891+
in Studio Pro, it emits `empty` as the argument value. In this Java-action
892+
argument context, `empty` preserves the
893+
underlying empty `BasicCodeActionParameterValue.Argument` so that the next
894+
`describe → exec → describe` cycle stays symmetric.
895+
896+
```mdl
897+
$Total = call java action SampleModule.Recalculate(
898+
CompanyId = empty,
899+
RecalculateAll = true,
900+
ItemList = empty
901+
);
902+
```
903+
904+
New scripts should bind every parameter to a real expression. Use `empty`
905+
for a Java-action argument only when regenerating MDL from an existing project
906+
that already had an unbound parameter.
907+
888908
## Error Handling
889909

890910
MDL supports error handling for activities that may fail (microflow calls, commits, external service calls, etc.).

docs/01-project/MDL_QUICK_REFERENCE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,7 @@ Module.OrderResponse_CustomerInfo/Module.CustomerInfo as customer {
789789
| Rename Java action (dry run) | `rename java action Module.Old to New dry run;` | Preview reference changes without modifying |
790790
| Drop Java action | `drop java action Module.Name;` | Deletes MPR unit and .java source file |
791791
| Call from microflow | `$Result = call java action Module.Name(Param = value);` | Inside BEGIN...END |
792+
| Empty argument | `call java action Module.Name(Param = empty);` | Unbound code-action parameter preserved as empty mapping |
792793

793794
**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).
794795

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Empty Java Action Argument
2+
3+
Status: Implemented
4+
5+
## Summary
6+
7+
Use the existing MDL `empty` literal to represent an intentionally unbound Java
8+
action argument in microflow call statements.
9+
10+
```mdl
11+
$Total = call java action SampleModule.Recalculate(
12+
CompanyId = empty,
13+
RecalculateAll = true,
14+
ItemList = empty
15+
);
16+
```
17+
18+
In this Java-action argument context, `empty` produces a parameter binding with
19+
an empty `Argument` string in the serialized BSON
20+
(`Microflows$BasicCodeActionParameterValue.Argument = ""`). Re-executing the
21+
script reproduces the same empty binding, so `describe -> exec -> describe`
22+
stays symmetric for existing Studio Pro projects that have unbound code-action
23+
parameters.
24+
25+
## Motivation
26+
27+
Studio Pro's Java-action call dialog allows a developer to leave individual
28+
parameters empty. The on-disk representation is a
29+
`Microflows$JavaActionParameterMapping` whose value is a
30+
`BasicCodeActionParameterValue` with `Argument: ""`.
31+
32+
Emitting `''` would create a literal empty string expression, not an unbound
33+
parameter. Dropping the parameter would lose the original mapping. The existing
34+
`empty` literal is already valid MDL expression syntax and is clearer than
35+
introducing a new placeholder token for this one case.
36+
37+
## Semantics
38+
39+
- In Java-action call arguments, `empty` maps to an empty
40+
`BasicCodeActionParameterValue.Argument`.
41+
- If the Java action parameter type is a microflow callback, `empty` maps to a
42+
`Microflows$MicroflowParameterValue` with an empty `Microflow` reference.
43+
- Outside Java-action call arguments, `empty` keeps its normal MDL literal
44+
meaning.
45+
46+
## Examples
47+
48+
```mdl
49+
-- Java action call with two unbound and one bound argument.
50+
$Total = call java action SampleModule.Recalculate(
51+
CompanyId = empty,
52+
RecalculateAll = true,
53+
ItemList = empty
54+
);
55+
```
56+
57+
The Mendix BSON for the unbound arguments is:
58+
59+
```text
60+
JavaActionParameterMapping {
61+
Parameter: 'SampleModule.Recalculate.CompanyId',
62+
Value: BasicCodeActionParameterValue { Argument: '' }
63+
}
64+
```
65+
66+
## Tests And Examples
67+
68+
- Builder coverage: `TestBuildJavaAction_EmptyArgumentPreservesEmptyBasicValue`
69+
and `TestBuildJavaAction_EmptyMicroflowArgumentUsesMicroflowParameterValue`
70+
in `mdl/executor/cmd_microflows_builder_java_action_test.go`.
71+
- Example script:
72+
`mdl-examples/doctype-tests/empty_java_action_argument.mdl`.

docs/11-proposals/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ BSON schema Registry ◄──── multi-version Support
4545
| [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 |
4646
| [Microflow Free Annotation](PROPOSAL_microflow_free_annotation.md) | Draft | Order-sensitive `@annotation` handling for free-floating visual notes in microflows ||
4747
| [Microflow Download File Statement](PROPOSAL_microflow_download_file_statement.md) | Draft | `download file $FileDocument [show in browser]` for `DownloadFileAction` round-trip and authoring ||
48+
| [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 ||
4849
| [Microflow Call Web Service Statement](PROPOSAL_microflow_call_web_service_statement.md) | Draft | Structured and raw MDL syntax for legacy SOAP `CallWebServiceAction` round-trip preservation ||
4950
| [Page Syntax V2](PROPOSAL_page_syntax_v2.md) | Superseded | Page/widget syntax with `{}` blocks and `->` binding. Superseded by V3 (archived) ||
5051
| [Page Styling Support](page-styling-support.md) | Partial | CSS classes, inline styles, dynamic classes, design properties. Phase 1 (Class/Style) done ||
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
create microflow SampleModule.ACT_RecalculateOpenItems ()
2+
returns Void
3+
begin
4+
call java action SampleModule.Recalculate(
5+
CompanyId = empty,
6+
RecalculateAll = true,
7+
ItemList = empty
8+
);
9+
return;
10+
end;
11+
/
12+
13+
create microflow SampleModule.ACT_RecalculateForCompany (
14+
$CompanyId: String
15+
)
16+
returns Void
17+
begin
18+
call java action SampleModule.Recalculate(
19+
CompanyId = $CompanyId,
20+
RecalculateAll = false,
21+
ItemList = empty
22+
);
23+
return;
24+
end;
25+
/

mdl/executor/cmd_microflows_builder_calls.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,10 +246,13 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model.
246246

247247
// Build a map of parameter name -> param type for the Java action
248248
entityTypeParams := make(map[string]bool)
249+
microflowTypeParams := make(map[string]bool)
249250
if jaDef != nil {
250251
for _, p := range jaDef.Parameters {
251252
if _, ok := p.ParameterType.(*javaactions.EntityTypeParameterType); ok {
252253
entityTypeParams[p.Name] = true
254+
} else if _, ok := p.ParameterType.(*javaactions.MicroflowType); ok {
255+
microflowTypeParams[p.Name] = true
253256
}
254257
}
255258
}
@@ -278,12 +281,31 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model.
278281
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
279282
Entity: entityName,
280283
}
284+
} else if isEmptyJavaActionArgument(arg.Value) {
285+
if microflowTypeParams[arg.Name] {
286+
value = &microflows.MicroflowParameterValue{
287+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
288+
Microflow: "",
289+
}
290+
} else {
291+
value = &microflows.BasicCodeActionParameterValue{
292+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
293+
Argument: "",
294+
}
295+
}
281296
} else {
282297
// Regular parameter: expression-based value
283298
valueExpr := fb.exprToString(arg.Value)
284-
value = &microflows.BasicCodeActionParameterValue{
285-
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
286-
Argument: valueExpr,
299+
if microflowTypeParams[arg.Name] {
300+
value = &microflows.MicroflowParameterValue{
301+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
302+
Microflow: strings.Trim(valueExpr, "'"),
303+
}
304+
} else {
305+
value = &microflows.BasicCodeActionParameterValue{
306+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
307+
Argument: valueExpr,
308+
}
287309
}
288310
}
289311

@@ -439,6 +461,11 @@ func (fb *flowBuilder) addCallJavaScriptActionAction(s *ast.CallJavaScriptAction
439461
return activity.ID
440462
}
441463

464+
func isEmptyJavaActionArgument(expr ast.Expression) bool {
465+
lit, ok := expr.(*ast.LiteralExpr)
466+
return ok && (lit.Kind == ast.LiteralEmpty || lit.Kind == ast.LiteralNull)
467+
}
468+
442469
// addCallWebServiceAction creates a legacy SOAP WebServiceCallAction.
443470
func (fb *flowBuilder) addCallWebServiceAction(s *ast.CallWebServiceStmt) model.ID {
444471
activityX := fb.posX
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package executor
4+
5+
import (
6+
"testing"
7+
8+
"github.com/mendixlabs/mxcli/mdl/ast"
9+
"github.com/mendixlabs/mxcli/mdl/backend/mock"
10+
"github.com/mendixlabs/mxcli/model"
11+
"github.com/mendixlabs/mxcli/sdk/javaactions"
12+
"github.com/mendixlabs/mxcli/sdk/microflows"
13+
)
14+
15+
func TestBuildJavaAction_EmptyArgumentPreservesEmptyBasicValue(t *testing.T) {
16+
fb := &flowBuilder{posX: 100, posY: 100, spacing: HorizontalSpacing}
17+
stmt := &ast.CallJavaActionStmt{
18+
ActionName: ast.QualifiedName{Module: "SampleModule", Name: "Recalculate"},
19+
Arguments: []ast.CallArgument{
20+
{Name: "CompanyId", Value: &ast.LiteralExpr{Kind: ast.LiteralEmpty}},
21+
{Name: "RecalculateAll", Value: &ast.LiteralExpr{Kind: ast.LiteralBoolean, Value: true}},
22+
{Name: "ItemList", Value: &ast.LiteralExpr{Kind: ast.LiteralEmpty}},
23+
},
24+
}
25+
26+
id := fb.addCallJavaActionAction(stmt)
27+
var activity *microflows.ActionActivity
28+
for _, obj := range fb.objects {
29+
if obj.GetID() == id {
30+
activity, _ = obj.(*microflows.ActionActivity)
31+
break
32+
}
33+
}
34+
if activity == nil {
35+
t.Fatal("expected Java action activity")
36+
}
37+
action, ok := activity.Action.(*microflows.JavaActionCallAction)
38+
if !ok {
39+
t.Fatalf("action = %T, want *JavaActionCallAction", activity.Action)
40+
}
41+
if len(action.ParameterMappings) != 3 {
42+
t.Fatalf("parameter mappings = %d, want 3", len(action.ParameterMappings))
43+
}
44+
45+
for _, idx := range []int{0, 2} {
46+
value, ok := action.ParameterMappings[idx].Value.(*microflows.BasicCodeActionParameterValue)
47+
if !ok {
48+
t.Fatalf("mapping %d value = %T, want *BasicCodeActionParameterValue", idx, action.ParameterMappings[idx].Value)
49+
}
50+
if value.Argument != "" {
51+
t.Fatalf("mapping %d argument = %q, want empty string", idx, value.Argument)
52+
}
53+
}
54+
55+
value, ok := action.ParameterMappings[1].Value.(*microflows.BasicCodeActionParameterValue)
56+
if !ok {
57+
t.Fatalf("boolean mapping value = %T, want *BasicCodeActionParameterValue", action.ParameterMappings[1].Value)
58+
}
59+
if value.Argument != "true" {
60+
t.Fatalf("boolean argument = %q, want true", value.Argument)
61+
}
62+
}
63+
64+
func TestBuildJavaAction_EmptyMicroflowArgumentUsesMicroflowParameterValue(t *testing.T) {
65+
fb := &flowBuilder{
66+
posX: 100,
67+
posY: 100,
68+
spacing: HorizontalSpacing,
69+
backend: &mock.MockBackend{
70+
ReadJavaActionByNameFunc: func(qualifiedName string) (*javaactions.JavaAction, error) {
71+
if qualifiedName != "SampleModule.StartAsync" {
72+
t.Fatalf("java action lookup = %q", qualifiedName)
73+
}
74+
return &javaactions.JavaAction{
75+
Parameters: []*javaactions.JavaActionParameter{
76+
{
77+
Name: "Callback",
78+
ParameterType: &javaactions.MicroflowType{
79+
BaseElement: model.BaseElement{ID: "param-type"},
80+
},
81+
},
82+
},
83+
}, nil
84+
},
85+
},
86+
}
87+
stmt := &ast.CallJavaActionStmt{
88+
ActionName: ast.QualifiedName{Module: "SampleModule", Name: "StartAsync"},
89+
Arguments: []ast.CallArgument{
90+
{Name: "Callback", Value: &ast.LiteralExpr{Kind: ast.LiteralEmpty}},
91+
},
92+
}
93+
94+
id := fb.addCallJavaActionAction(stmt)
95+
var activity *microflows.ActionActivity
96+
for _, obj := range fb.objects {
97+
if obj.GetID() == id {
98+
activity, _ = obj.(*microflows.ActionActivity)
99+
break
100+
}
101+
}
102+
if activity == nil {
103+
t.Fatal("expected Java action activity")
104+
}
105+
action, ok := activity.Action.(*microflows.JavaActionCallAction)
106+
if !ok {
107+
t.Fatalf("action = %T, want *JavaActionCallAction", activity.Action)
108+
}
109+
value, ok := action.ParameterMappings[0].Value.(*microflows.MicroflowParameterValue)
110+
if !ok {
111+
t.Fatalf("mapping value = %T, want *MicroflowParameterValue", action.ParameterMappings[0].Value)
112+
}
113+
if value.Microflow != "" {
114+
t.Fatalf("placeholder microflow = %q, want empty string", value.Microflow)
115+
}
116+
}

mdl/executor/cmd_microflows_format_action.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,17 @@ func formatAction(
550550
case *microflows.ExpressionBasedCodeActionParameterValue:
551551
valueStr = v.Expression
552552
case *microflows.BasicCodeActionParameterValue:
553-
valueStr = v.Argument
553+
if v.Argument == "" {
554+
valueStr = "empty"
555+
} else {
556+
valueStr = v.Argument
557+
}
558+
case *microflows.MicroflowParameterValue:
559+
if v.Microflow != "" {
560+
valueStr = mdlQuote(v.Microflow)
561+
} else {
562+
valueStr = "empty"
563+
}
554564
case *microflows.EntityTypeCodeActionParameterValue:
555565
if v.Entity != "" {
556566
valueStr = mdlQuote(v.Entity)

mdl/executor/cmd_microflows_format_action_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,32 @@ func TestFormatAction_JavaActionCall(t *testing.T) {
416416
}
417417
}
418418

419+
func TestFormatAction_JavaActionCall_EmptyParameterValues(t *testing.T) {
420+
e := newTestExecutor()
421+
action := &microflows.JavaActionCallAction{
422+
JavaAction: "MyModule.Recalculate",
423+
ParameterMappings: []*microflows.JavaActionParameterMapping{
424+
{
425+
Parameter: "MyModule.Recalculate.CompanyId",
426+
Value: &microflows.BasicCodeActionParameterValue{Argument: ""},
427+
},
428+
{
429+
Parameter: "MyModule.Recalculate.RecalculateAll",
430+
Value: &microflows.BasicCodeActionParameterValue{Argument: "true"},
431+
},
432+
{
433+
Parameter: "MyModule.Recalculate.Callback",
434+
Value: &microflows.MicroflowParameterValue{Microflow: ""},
435+
},
436+
},
437+
}
438+
got := e.formatAction(action, nil, nil)
439+
want := "call java action MyModule.Recalculate(CompanyId = empty, RecalculateAll = true, Callback = empty);"
440+
if got != want {
441+
t.Errorf("got %q, want %q", got, want)
442+
}
443+
}
444+
419445
func TestFormatAction_CallExternal(t *testing.T) {
420446
e := newTestExecutor()
421447
action := &microflows.CallExternalAction{

0 commit comments

Comments
 (0)