Skip to content

Commit 78c42dc

Browse files
authored
Merge pull request #363 from hjotha/submit/change-object-refresh-modifier
feat: preserve change object refresh modifier
2 parents 3e384be + 683ab02 commit 78c42dc

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)