Skip to content

Commit c1d6a85

Browse files
committed
feat: preserve change object refresh modifier
Mendix change-object actions can have RefreshInClient=true even when they also change members, but MDL had no syntax for that flag. Describing and rebuilding such an action therefore rewrote the flag to false. The grammar now accepts an optional `refresh` modifier on change-object statements, the AST carries that flag, the formatter emits it for refreshed change actions, and the builder writes it back to ChangeObjectAction.RefreshInClient. Tests cover parser, formatter, and builder behavior with and without member assignments. The branch also adds a draft proposal, doctype fixture, quick-reference entry, and skill guidance; `mxcli check` on the fixture plus make build, make lint-go, and make test pass locally.
1 parent 6f4534f commit c1d6a85

15 files changed

Lines changed: 6511 additions & 6298 deletions

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,9 @@ $NewProduct = create Test.Product (
378378
change $Product (
379379
Name = $NewName,
380380
ModifiedDate = [%CurrentDateTime%]);
381+
382+
-- Refresh the changed object in the client
383+
change $Product (Name = $NewName) refresh;
381384
```
382385

383386
**Note**: Only specify attributes you want to change. Syntax aligned with CREATE.

docs/01-project/MDL_QUICK_REFERENCE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ authentication basic, session
218218
| List declaration | `declare $list list of Module.Entity = empty;` | |
219219
| Assignment | `set $Var = expression;` | Variable must be declared first |
220220
| Create object | `$Var = create Module.Entity (attr = value);` | |
221-
| Change object | `change $entity (attr = value);` | |
221+
| Change object | `change $entity (attr = value) [refresh];` | `refresh` updates the changed object in the client |
222222
| Commit | `commit $entity [with events] [refresh];` | |
223223
| Delete | `delete $entity;` | |
224224
| Rollback | `rollback $entity [refresh];` | Reverts uncommitted changes |
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Proposal: Microflow CHANGE Refresh Modifier
2+
3+
Status: Draft
4+
5+
## Summary
6+
7+
Allow `change` microflow statements to explicitly preserve the Mendix `RefreshInClient` flag:
8+
9+
```mdl
10+
change $Customer (Name = 'Jane') refresh;
11+
change $Customer refresh;
12+
```
13+
14+
## Motivation
15+
16+
Mendix change-object actions can refresh the changed object in the client independently from committing the object. MDL previously had no syntax for that flag, so a describe/exec round-trip could rewrite `RefreshInClient = true` to `false` for change actions with member assignments.
17+
18+
## Semantics
19+
20+
The `refresh` modifier maps directly to `ChangeObjectAction.RefreshInClient`. Omitting it preserves the existing default behavior. The modifier is accepted both with member assignments and on a memberless change action.
21+
22+
## Tests And Examples
23+
24+
`mdl-examples/doctype-tests/change_refresh_modifier.test.mdl` demonstrates both forms. Go tests cover formatter output, parser behavior, and builder serialization.
25+
26+
## Open Questions
27+
28+
- Should a memberless `change $Object;` continue to infer refresh in separate validity-focused fixes, or should the explicit `refresh` modifier be the only authoring form?

docs/11-proposals/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ BSON schema Registry ◄──── multi-version Support
4949
| [Page Styling Support](page-styling-support.md) | Partial | CSS classes, inline styles, dynamic classes, design properties. Phase 1 (Class/Style) done ||
5050
| [Page Composition](proposal_page_composition.md) | Proposed | Fragment definitions and ALTER PAGE for partial page editing | Page Syntax V2, Page Styling |
5151
| [XPath Gaps](xpath-gaps-proposal.md) | Partial | XPath constraint support gap analysis. ~85% complete, association paths and nested predicates remain ||
52+
| [Microflow CHANGE Refresh Modifier](PROPOSAL_microflow_change_refresh_modifier.md) | Draft | Preserve `RefreshInClient` on change-object actions ||
5253
| [LLM MDL Assistance](PROPOSAL_llm_mdl_assistance.md) | Proposed | Enhanced error messages with examples, reorganized skills by use case ||
5354

5455
### Testing & Evaluation
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
create module ChangeRefreshExample;
2+
3+
create persistent entity ChangeRefreshExample.Customer (
4+
Name: string
5+
);
6+
/
7+
8+
create microflow ChangeRefreshExample.RefreshChangedCustomer (
9+
$Customer: ChangeRefreshExample.Customer
10+
)
11+
returns boolean
12+
begin
13+
change $Customer (Name = 'Jane') refresh;
14+
change $Customer refresh;
15+
return true;
16+
end;
17+
/

mdl/ast/ast_microflow.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,10 @@ func (s *CreateObjectStmt) isMicroflowStatement() {}
192192

193193
// ChangeObjectStmt represents: CHANGE $Var (assignments)
194194
type ChangeObjectStmt struct {
195-
Variable string // Variable name
196-
Changes []ChangeItem // SET assignments
197-
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
195+
Variable string // Variable name
196+
Changes []ChangeItem // SET assignments
197+
RefreshInClient bool // Whether to refresh in client
198+
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
198199
}
199200

200201
func (s *ChangeObjectStmt) isMicroflowStatement() {}

mdl/executor/cmd_microflows_builder_actions.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -239,14 +239,13 @@ func (fb *flowBuilder) addRollbackAction(s *ast.RollbackStmt) model.ID {
239239

240240
// addChangeObjectAction creates a CHANGE statement.
241241
func (fb *flowBuilder) addChangeObjectAction(s *ast.ChangeObjectStmt) model.ID {
242+
// Empty non-committing changes need RefreshInClient to satisfy Studio Pro
243+
// consistency checks; explicit `refresh` keeps the same flag for all changes.
242244
action := &microflows.ChangeObjectAction{
243-
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
244-
ChangeVariable: s.Variable,
245-
Commit: microflows.CommitTypeNo,
246-
// Studio Pro rejects an empty non-committing change action unless it
247-
// refreshes in client. The CE0032 message mentions only items/commit,
248-
// but mx check accepts RefreshInClient=true as the third valid escape.
249-
RefreshInClient: len(s.Changes) == 0,
245+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
246+
ChangeVariable: s.Variable,
247+
Commit: microflows.CommitTypeNo,
248+
RefreshInClient: s.RefreshInClient || len(s.Changes) == 0,
250249
}
251250

252251
// Look up entity type from variable scope
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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/sdk/microflows"
10+
)
11+
12+
func TestChangeObjectBuilderWritesRefreshInClient(t *testing.T) {
13+
fb := &flowBuilder{}
14+
15+
fb.addChangeObjectAction(&ast.ChangeObjectStmt{
16+
Variable: "Customer",
17+
RefreshInClient: true,
18+
Changes: []ast.ChangeItem{
19+
{Attribute: "Name", Value: &ast.LiteralExpr{Kind: ast.LiteralString, Value: "Jane"}},
20+
},
21+
})
22+
23+
action := lastChangeObjectAction(t, fb)
24+
if !action.RefreshInClient {
25+
t.Fatal("Expected builder to write RefreshInClient")
26+
}
27+
}
28+
29+
func lastChangeObjectAction(t *testing.T, fb *flowBuilder) *microflows.ChangeObjectAction {
30+
t.Helper()
31+
32+
if len(fb.objects) == 0 {
33+
t.Fatal("Expected builder to create an action activity")
34+
}
35+
activity, ok := fb.objects[len(fb.objects)-1].(*microflows.ActionActivity)
36+
if !ok {
37+
t.Fatalf("Last object = %T, want ActionActivity", fb.objects[len(fb.objects)-1])
38+
}
39+
action, ok := activity.Action.(*microflows.ChangeObjectAction)
40+
if !ok {
41+
t.Fatalf("Action = %T, want ChangeObjectAction", activity.Action)
42+
}
43+
return action
44+
}

mdl/executor/cmd_microflows_format_action.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,14 @@ func formatAction(
241241
}
242242
members = append(members, fmt.Sprintf("%s = %s", memberName, escapeExpressionValue(m.Value)))
243243
}
244+
if a.RefreshInClient {
245+
return fmt.Sprintf("change $%s (%s) refresh;", varName, strings.Join(members, ", "))
246+
}
244247
return fmt.Sprintf("change $%s (%s);", varName, strings.Join(members, ", "))
245248
}
249+
if a.RefreshInClient {
250+
return fmt.Sprintf("change $%s refresh;", varName)
251+
}
246252
return fmt.Sprintf("change $%s;", varName)
247253

248254
case *microflows.CommitObjectsAction:

mdl/executor/cmd_microflows_format_action_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,30 @@ func TestFormatAction_ChangeObject_NoChanges(t *testing.T) {
9696
}
9797
}
9898

99+
func TestFormatAction_ChangeObject_WithRefresh(t *testing.T) {
100+
e := newTestExecutor()
101+
action := &microflows.ChangeObjectAction{
102+
ChangeVariable: "Customer",
103+
RefreshInClient: true,
104+
Changes: []*microflows.MemberChange{
105+
{AttributeQualifiedName: "MyModule.Customer.Name", Value: "'Jane'"},
106+
},
107+
}
108+
got := e.formatAction(action, nil, nil)
109+
if got != "change $Customer (Name = 'Jane') refresh;" {
110+
t.Errorf("got %q", got)
111+
}
112+
}
113+
114+
func TestFormatAction_ChangeObject_NoChangesWithRefresh(t *testing.T) {
115+
e := newTestExecutor()
116+
action := &microflows.ChangeObjectAction{ChangeVariable: "Obj", RefreshInClient: true}
117+
got := e.formatAction(action, nil, nil)
118+
if got != "change $Obj refresh;" {
119+
t.Errorf("got %q", got)
120+
}
121+
}
122+
99123
func TestFormatAction_DeleteObject(t *testing.T) {
100124
e := newTestExecutor()
101125
action := &microflows.DeleteObjectAction{DeleteVariable: "Customer"}

0 commit comments

Comments
 (0)