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
11 changes: 11 additions & 0 deletions .claude/skills/mendix/write-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,17 @@ rest call delete 'https://api.example.com/items/{1}' with (

**REST CALL supports full error handling** (`on error continue`, `on error rollback`, custom error handlers).

## File Downloads

Use `download file` to stream a `System.FileDocument` from a microflow. Add
`show in browser` when the action should open the file inline instead of forcing
a download.

```mdl
download file $GeneratedReport show in browser;
download file $GeneratedExport;
```

## Error Handling

MDL supports error handling for activities that may fail (microflow calls, commits, external service calls, etc.).
Expand Down
2 changes: 2 additions & 0 deletions cmd/mxcli/lsp_completions_gen.go

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

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 @@ -225,6 +225,7 @@ authentication basic, session
| Call nanoflow | `$Result = call nanoflow Module.Name (Param = $value);` | |
| Show page | `show page Module.PageName ($Param = $value);` | Also accepts `(Param: $value)` |
| Close page | `close page;` | |
| Download file | `download file $FileDocument [show in browser];` | Streams a `System.FileDocument` |
| Validation | `validation feedback $entity/attribute message 'message';` | Requires attribute path + MESSAGE |
| Log | `log info\|warning\|error [node 'name'] 'message';` | |
| Position | `@position(x, y)` | Canvas position (before activity) |
Expand Down
49 changes: 49 additions & 0 deletions docs/11-proposals/PROPOSAL_microflow_download_file_statement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Microflow Download File Statement

Status: Draft

## Summary

Add MDL syntax for Mendix `Microflows$DownloadFileAction`:

```mdl
download file $GeneratedReport;
download file $GeneratedReport show in browser;
```

The feature is intentionally small: it exposes an existing Studio Pro microflow
activity without adding new semantics.

## Motivation

Projects that already contain download-file actions should survive
describe/exec/describe without losing the activity or falling back to an
unsupported-action comment. The statement also gives users a straightforward way
to author file downloads when the file document variable already exists.

## Syntax

```antlr
downloadFileStatement
: DOWNLOAD FILE_KW VARIABLE (SHOW IN BROWSER)? onErrorClause? SEMICOLON
;
```

Examples:

```mdl
download file $GeneratedExport;
download file $GeneratedReport show in browser on error rollback;
```

## Semantics

- The operand must be a variable containing a `System.FileDocument`.
- `show in browser` maps to Studio Pro's `ShowInBrowser` flag.
- Error handling follows the normal microflow action `on error ...` forms.

## Tests And Examples

- Parser/visitor coverage for both normal and `show in browser` forms.
- Builder/writer coverage for `DownloadFileAction`.
- Example script: `mdl-examples/doctype-tests/download_file.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 @@ -43,6 +43,7 @@ BSON schema Registry ◄──── multi-version Support
|----------|--------|---------|------------|
| [MDL Syntax Improvements v1](PROPOSAL_mdl_syntax_improvements.md) | Draft | Go-style assignment, C-style braces, fluent list APIs | — |
| [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 Download File Statement](PROPOSAL_microflow_download_file_statement.md) | Draft | `download file $FileDocument [show in browser]` for `DownloadFileAction` round-trip and authoring | — |
| [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
19 changes: 19 additions & 0 deletions mdl-examples/doctype-tests/download_file.test.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
create microflow SampleFiles.ACT_DownloadReport (
$GeneratedReport: System.FileDocument
)
returns Void
begin
download file $GeneratedReport show in browser;
return;
end;
/

create microflow SampleFiles.ACT_DownloadExport (
$GeneratedExport: System.FileDocument
)
returns Void
begin
download file $GeneratedExport;
return;
end;
/
10 changes: 10 additions & 0 deletions mdl/ast/ast_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,16 @@ type ShowMessageStmt struct {

func (s *ShowMessageStmt) isMicroflowStatement() {}

// DownloadFileStmt represents: DOWNLOAD FILE $FileDocument [SHOW IN BROWSER]
type DownloadFileStmt struct {
FileDocument string // File document variable without $ prefix
ShowInBrowser bool // Whether the file opens in the browser
ErrorHandling *ErrorHandlingClause // Optional ON ERROR clause
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
}

func (s *DownloadFileStmt) isMicroflowStatement() {}

// ValidationFeedbackStmt represents: VALIDATION FEEDBACK $Var/Attr MESSAGE 'message' OBJECTS [$Var1, $Var2];
type ValidationFeedbackStmt struct {
AttributePath *AttributePathExpr // The attribute to associate with the feedback
Expand Down
2 changes: 2 additions & 0 deletions mdl/executor/cmd_microflows_builder_annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ func getStatementAnnotations(stmt ast.MicroflowStatement) *ast.ActivityAnnotatio
return s.Annotations
case *ast.ShowMessageStmt:
return s.Annotations
case *ast.DownloadFileStmt:
return s.Annotations
case *ast.ValidationFeedbackStmt:
return s.Annotations
case *ast.RestCallStmt:
Expand Down
29 changes: 29 additions & 0 deletions mdl/executor/cmd_microflows_builder_calls.go
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,35 @@ func (fb *flowBuilder) addShowMessageAction(s *ast.ShowMessageStmt) model.ID {
return activity.ID
}

// addDownloadFileAction creates a DOWNLOAD FILE statement.
func (fb *flowBuilder) addDownloadFileAction(s *ast.DownloadFileStmt) model.ID {
action := &microflows.DownloadFileAction{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
FileDocument: s.FileDocument,
ShowInBrowser: s.ShowInBrowser,
ErrorHandlingType: microflows.ErrorHandlingTypeRollback,
}
if s.ErrorHandling != nil {
action.ErrorHandlingType = convertErrorHandlingType(s.ErrorHandling)
}

activity := &microflows.ActionActivity{
BaseActivity: microflows.BaseActivity{
BaseMicroflowObject: microflows.BaseMicroflowObject{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Position: model.Point{X: fb.posX, Y: fb.posY},
Size: model.Size{Width: ActivityWidth, Height: ActivityHeight},
},
AutoGenerateCaption: true,
},
Action: action,
}

fb.objects = append(fb.objects, activity)
fb.posX += fb.spacing
return activity.ID
}

// addClosePageAction creates a CLOSE PAGE statement.
func (fb *flowBuilder) addClosePageAction(s *ast.ClosePageStmt) model.ID {
numPages := s.NumberOfPages
Expand Down
52 changes: 52 additions & 0 deletions mdl/executor/cmd_microflows_builder_download_file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: Apache-2.0

package executor

import (
"testing"

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

func TestBuildFlowGraph_DownloadFileCreatesRealAction(t *testing.T) {
fb := &flowBuilder{
posX: 100,
posY: 100,
spacing: HorizontalSpacing,
varTypes: map[string]string{"GeneratedReport": "System.FileDocument"},
declaredVars: map[string]string{"GeneratedReport": "System.FileDocument"},
}

fb.buildFlowGraph([]ast.MicroflowStatement{
&ast.DownloadFileStmt{
FileDocument: "GeneratedReport",
ShowInBrowser: true,
ErrorHandling: &ast.ErrorHandlingClause{Type: ast.ErrorHandlingContinue},
},
}, nil)

var action *microflows.DownloadFileAction
for _, obj := range fb.objects {
activity, ok := obj.(*microflows.ActionActivity)
if !ok {
continue
}
if dl, ok := activity.Action.(*microflows.DownloadFileAction); ok {
action = dl
break
}
}
if action == nil {
t.Fatal("expected DownloadFileAction")
}
if action.FileDocument != "GeneratedReport" {
t.Fatalf("FileDocument = %q, want GeneratedReport", action.FileDocument)
}
if !action.ShowInBrowser {
t.Fatal("ShowInBrowser = false, want true")
}
if action.ErrorHandlingType != microflows.ErrorHandlingTypeContinue {
t.Fatalf("ErrorHandlingType = %q, want Continue", action.ErrorHandlingType)
}
}
2 changes: 2 additions & 0 deletions mdl/executor/cmd_microflows_builder_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ func (fb *flowBuilder) addStatement(stmt ast.MicroflowStatement) model.ID {
return fb.addShowHomePageAction(s)
case *ast.ShowMessageStmt:
return fb.addShowMessageAction(s)
case *ast.DownloadFileStmt:
return fb.addDownloadFileAction(s)
case *ast.ValidationFeedbackStmt:
return fb.addValidationFeedbackAction(s)
case *ast.RestCallStmt:
Expand Down
5 changes: 5 additions & 0 deletions mdl/executor/cmd_microflows_builder_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ func (fb *flowBuilder) validateStatement(stmt ast.MicroflowStatement) {
fb.validateStatements(s.ErrorHandling.Body)
}

case *ast.DownloadFileStmt:
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
fb.validateStatements(s.ErrorHandling.Body)
}

case *ast.ExecuteDatabaseQueryStmt:
if s.OutputVariable != "" {
fb.declaredVars[s.OutputVariable] = "Unknown"
Expand Down
11 changes: 11 additions & 0 deletions mdl/executor/cmd_microflows_format_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,17 @@ func formatAction(
}
return result + ";"

case *microflows.DownloadFileAction:
fileDocument := a.FileDocument
if fileDocument != "" && !strings.HasPrefix(fileDocument, "$") {
fileDocument = "$" + fileDocument
}
result := fmt.Sprintf("download file %s", fileDocument)
if a.ShowInBrowser {
result += " show in browser"
}
return result + ";"

case *microflows.ValidationFeedbackAction:
// Get the message text from template translations (prefer en_US, fallback to any)
msgText := "'...'"
Expand Down
14 changes: 14 additions & 0 deletions mdl/executor/cmd_microflows_format_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,20 @@ func TestFormatAction_ShowMessage_EscapesMultiline(t *testing.T) {
}
}

func TestFormatAction_DownloadFile(t *testing.T) {
e := newTestExecutor()
action := &microflows.DownloadFileAction{
FileDocument: "GeneratedReport",
ShowInBrowser: true,
}

got := e.formatAction(action, nil, nil)
want := "download file $GeneratedReport show in browser;"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}

func TestFormatAction_ValidationFeedback(t *testing.T) {
e := newTestExecutor()
action := &microflows.ValidationFeedbackAction{
Expand Down
2 changes: 2 additions & 0 deletions mdl/executor/cmd_microflows_show_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,8 @@ func getActionErrorHandlingType(activity *microflows.ActionActivity) microflows.
return action.ErrorHandlingType
case *microflows.CommitObjectsAction:
return action.ErrorHandlingType
case *microflows.DownloadFileAction:
return action.ErrorHandlingType
default:
// Fall back to activity level for action types without ErrorHandlingType field
return activity.ErrorHandlingType
Expand Down
4 changes: 4 additions & 0 deletions mdl/executor/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,10 @@ func getErrorHandlerBody(stmt ast.MicroflowStatement) []ast.MicroflowStatement {
if s.ErrorHandling != nil && s.ErrorHandling.Body != nil {
return s.ErrorHandling.Body
}
case *ast.DownloadFileStmt:
if s.ErrorHandling != nil && s.ErrorHandling.Body != nil {
return s.ErrorHandling.Body
}
case *ast.ExecuteDatabaseQueryStmt:
if s.ErrorHandling != nil && s.ErrorHandling.Body != nil {
return s.ErrorHandling.Body
Expand Down
2 changes: 2 additions & 0 deletions mdl/executor/validate_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,8 @@ func stmtErrorHandling(stmt ast.MicroflowStatement) *ast.ErrorHandlingClause {
return s.ErrorHandling
case *ast.CallJavaActionStmt:
return s.ErrorHandling
case *ast.DownloadFileStmt:
return s.ErrorHandling
case *ast.ExecuteDatabaseQueryStmt:
return s.ErrorHandling
}
Expand Down
2 changes: 1 addition & 1 deletion mdl/formatter/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ var mdlKeywords = []string{
"REQUIRED", "UNIQUE", "INDEXED",
"BEGIN", "END", "IF", "THEN", "ELSE", "LOOP", "IN", "RETURN",
"RETRIEVE", "WHERE", "LIMIT", "FIRST", "LIST", "OF",
"CHANGE", "DELETE", "COMMIT", "ROLLBACK",
"CHANGE", "DELETE", "COMMIT", "ROLLBACK", "DOWNLOAD", "BROWSER",
"SHOW_PAGE", "CLOSE_PAGE", "SHOW_MESSAGE",
"PARAMETER", "PARAMETERS", "VARIABLE", "DECLARE",
"JAVA_ACTION", "CALL", "CALL_MICROFLOW", "CALL_NANOFLOW",
Expand Down
2 changes: 2 additions & 0 deletions mdl/grammar/MDLLexer.g4
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ RETURN: R E T U R N;
THROW: T H R O W;
LOG: L O G;
CALL: C A L L;
DOWNLOAD: D O W N L O A D;
BROWSER: B R O W S E R;
JAVA: J A V A;
JAVASCRIPT: J A V A S C R I P T;
ACTION: A C T I O N;
Expand Down
9 changes: 7 additions & 2 deletions mdl/grammar/MDLParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -1275,6 +1275,7 @@ microflowStatement
| annotation* closePageStatement SEMICOLON?
| annotation* showHomePageStatement SEMICOLON?
| annotation* showMessageStatement SEMICOLON?
| annotation* downloadFileStatement SEMICOLON?
| annotation* throwStatement SEMICOLON?
| annotation* listOperationStatement SEMICOLON?
| annotation* aggregateListStatement SEMICOLON?
Expand Down Expand Up @@ -1558,6 +1559,10 @@ showMessageStatement
: SHOW MESSAGE expression (TYPE identifierOrKeyword)? (OBJECTS LBRACKET expressionList RBRACKET)?
;

downloadFileStatement
: DOWNLOAD FILE_KW VARIABLE (SHOW IN BROWSER)? onErrorClause?
;

throwStatement
: THROW expression
;
Expand Down Expand Up @@ -3792,8 +3797,8 @@ annotationParenValue
*/
keyword
// DDL / DML
: ADD | ALTER | BATCH | CHANGE | CLOSE | COMMIT | CREATE | DECLARE | DELETE | DESCRIBE
| DROP | EXECUTE | EXPORT | GENERATE | IMPORT | INSERT | INTO | MODIFY | MOVE | REFRESH
: ADD | ALTER | BATCH | BROWSER | CHANGE | CLOSE | COMMIT | CREATE | DECLARE | DELETE | DESCRIBE
| DOWNLOAD | DROP | EXECUTE | EXPORT | GENERATE | IMPORT | INSERT | INTO | MODIFY | MOVE | REFRESH
| REMOVE | RENAME | REPLACE | RETRIEVE | RETURN | ROLLBACK | SET | UPDATE

// Entity / Domain model
Expand Down
8 changes: 7 additions & 1 deletion mdl/grammar/parser/MDLLexer.interp

Large diffs are not rendered by default.

Loading
Loading