Skip to content

Commit 528e19d

Browse files
akoclaude
andcommitted
fix(modelsdk): serialize microflow annotations (@annotation was dropped)
@annotation 'text' on a microflow activity was silently dropped on the modelsdk (default) engine: the write path had no case for Microflows$Annotation and never emitted the AnnotationFlow that links a note to its activity. The legacy engine wrote both; modelsdk produced 0 annotation objects. Add an Annotation case to microflowObjectToGen and serialize the ObjectCollection's AnnotationFlows in microflowToGen (version-gated line shape, mirroring the legacy serializeAnnotationFlow). @annotation now round-trips through modelsdk (write → read → describe) and mx check reports 0 errors. Also corrects the doctype example M021_2: Mendix for-loops have no editable caption (LoopedActivity has no Caption property — confirmed against a Studio-Pro-saved loop, which stores an Annotation note + AnnotationFlow, not a loop caption). The example now uses @annotation instead of @caption, matching what Studio Pro produces. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e9883c6 commit 528e19d

4 files changed

Lines changed: 153 additions & 4 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
-- ============================================================================
2+
-- Microflow @annotation dropped on the modelsdk engine
3+
-- ============================================================================
4+
--
5+
-- Symptom (before fix):
6+
-- @annotation 'text' on a microflow activity (including a loop) was silently
7+
-- dropped on the modelsdk (default) engine — no Microflows$Annotation object
8+
-- and no AnnotationFlow were written. The legacy engine wrote them; modelsdk
9+
-- had no serialization for either type.
10+
--
11+
-- (Related: for-loops have no caption in Mendix — LoopedActivity has no
12+
-- Caption property — so @caption on a loop is not representable. An
13+
-- annotation note is the supported way to label a loop, matching what Studio
14+
-- Pro produces when you attach a note to the loop.)
15+
--
16+
-- After fix:
17+
-- microflowObjectToGen serializes Microflows$Annotation and microflowToGen
18+
-- emits the AnnotationFlow, so @annotation round-trips through the modelsdk
19+
-- engine and mx check reports 0 errors.
20+
--
21+
-- Usage:
22+
-- mxcli exec mdl-examples/bug-tests/microflow-annotation-modelsdk.mdl -p app.mpr
23+
-- mxcli -p app.mpr -c "describe microflow AnnTest.MF_Loop" # shows @annotation
24+
-- ============================================================================
25+
26+
create module AnnTest;
27+
create persistent entity AnnTest.Thing ( Name: string(50) );
28+
/
29+
30+
create microflow AnnTest.MF_Loop ( $Things: list of AnnTest.Thing )
31+
returns integer as $count
32+
begin
33+
declare $count integer = 0;
34+
@annotation 'Process the things'
35+
loop $Thing in $Things
36+
begin
37+
set $count = $count + 1;
38+
end loop;
39+
return $count;
40+
end;
41+
/

mdl-examples/doctype-tests/02-microflow-examples.mdl

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -679,9 +679,13 @@ end;
679679
/
680680

681681
/**
682-
* Example 6.1b: LOOP with custom caption
682+
* Example 6.1b: LOOP with an annotation note
683683
*
684-
* Demonstrates activity metadata on a loop.
684+
* Mendix for-loops have no editable caption (LoopedActivity has no Caption
685+
* property — Studio Pro auto-labels them from the iterator). To document a
686+
* loop, attach an annotation note with @annotation, which creates a
687+
* Microflows$Annotation connected by an AnnotationFlow — the same thing you
688+
* get by drawing a note onto the loop in Studio Pro.
685689
*/
686690
create microflow MfTest.M021_2_LoopWithCaption (
687691
$ProductList: list of MfTest.Product
@@ -690,7 +694,7 @@ returns integer as $count
690694
begin
691695
declare $count integer = 0;
692696

693-
@caption 'Process products'
697+
@annotation 'Process products'
694698
loop $Product in $ProductList
695699
begin
696700
set $count = $count + 1;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package modelsdkbackend
4+
5+
import (
6+
"testing"
7+
8+
"github.com/mendixlabs/mxcli/model"
9+
"github.com/mendixlabs/mxcli/modelsdk/codec"
10+
"github.com/mendixlabs/mxcli/sdk/microflows"
11+
"go.mongodb.org/mongo-driver/v2/bson"
12+
)
13+
14+
// TestMicroflowObjectToGen_Annotation guards that a microflow annotation note
15+
// (from @annotation) is serialized. The modelsdk write path previously had no
16+
// case for it, so @annotation was silently dropped on the default engine.
17+
func TestMicroflowObjectToGen_Annotation(t *testing.T) {
18+
ann := &microflows.Annotation{
19+
BaseMicroflowObject: microflows.BaseMicroflowObject{
20+
BaseElement: model.BaseElement{ID: model.ID("ann-1")},
21+
Position: model.Point{X: 100, Y: 100},
22+
Size: model.Size{Width: 200, Height: 50},
23+
},
24+
Caption: "Process products",
25+
}
26+
g := microflowObjectToGen(ann)
27+
if g == nil {
28+
t.Fatal("microflowObjectToGen(Annotation) returned nil — annotation dropped")
29+
}
30+
raw, err := (&codec.Encoder{}).Encode(g)
31+
if err != nil {
32+
t.Fatalf("encode: %v", err)
33+
}
34+
r := bson.Raw(raw)
35+
if got := r.Lookup("$Type").StringValue(); got != "Microflows$Annotation" {
36+
t.Errorf("$Type = %q, want Microflows$Annotation", got)
37+
}
38+
if got := r.Lookup("Caption").StringValue(); got != "Process products" {
39+
t.Errorf("Caption = %q, want Process products", got)
40+
}
41+
}
42+
43+
// TestAnnotationFlowToGen guards the AnnotationFlow that connects a note to its
44+
// activity, including the version-specific line shape (Line/BezierCurve on 10+).
45+
func TestAnnotationFlowToGen(t *testing.T) {
46+
af := &microflows.AnnotationFlow{
47+
BaseElement: model.BaseElement{ID: model.ID("af-1")},
48+
OriginID: model.ID("ann-1"),
49+
DestinationID: model.ID("act-1"),
50+
}
51+
raw, err := (&codec.Encoder{}).Encode(annotationFlowToGen(af, 11))
52+
if err != nil {
53+
t.Fatalf("encode: %v", err)
54+
}
55+
r := bson.Raw(raw)
56+
if got := r.Lookup("$Type").StringValue(); got != "Microflows$AnnotationFlow" {
57+
t.Errorf("$Type = %q, want Microflows$AnnotationFlow", got)
58+
}
59+
if _, err := r.LookupErr("OriginPointer"); err != nil {
60+
t.Errorf("OriginPointer missing: %v", err)
61+
}
62+
if _, err := r.LookupErr("DestinationPointer"); err != nil {
63+
t.Errorf("DestinationPointer missing: %v", err)
64+
}
65+
if _, err := r.LookupErr("Line"); err != nil {
66+
t.Errorf("Line (BezierCurve) missing on major>=10: %v", err)
67+
}
68+
}

mdl/backend/modelsdk/microflow_write.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,15 @@ func microflowToGen(mf *microflows.Microflow, major int) *genMf.Microflow {
204204
}
205205
out.SetObjectCollection(oc)
206206

207-
// Flows live on the microflow, not in the object collection.
207+
// Flows live on the microflow, not in the object collection. Sequence flows
208+
// and annotation flows share the same Flows list.
208209
if mf.ObjectCollection != nil {
209210
for _, f := range mf.ObjectCollection.Flows {
210211
out.AddFlows(sequenceFlowToGen(f, major))
211212
}
213+
for _, af := range mf.ObjectCollection.AnnotationFlows {
214+
out.AddFlows(annotationFlowToGen(af, major))
215+
}
212216
}
213217

214218
if major >= 10 {
@@ -314,11 +318,43 @@ func microflowObjectToGen(obj microflows.MicroflowObject) element.Element {
314318
g.SetRelativeMiddlePoint(pointStr(o.Position))
315319
g.SetSize(sizeStr(o.Size))
316320
return g
321+
case *microflows.Annotation:
322+
// A microflow annotation (yellow note); its caption is the note text.
323+
// Connected to an activity by an AnnotationFlow (serialized in
324+
// microflowToGen). Without this, `@annotation` was silently dropped.
325+
g := genMf.NewAnnotation()
326+
g.SetID(element.ID(o.ID))
327+
g.SetCaption(o.Caption)
328+
g.SetRelativeMiddlePoint(pointStr(o.Position))
329+
g.SetSize(sizeStr(o.Size))
330+
return g
317331
default:
318332
return nil // unsupported object type (added in later activity groups)
319333
}
320334
}
321335

336+
// annotationFlowToGen builds a Microflows$AnnotationFlow connecting an Annotation
337+
// (origin) to the activity it documents (destination). The line shape is
338+
// version-specific, mirroring the legacy serializeAnnotationFlow.
339+
func annotationFlowToGen(af *microflows.AnnotationFlow, major int) element.Element {
340+
g := genMf.NewAnnotationFlow()
341+
g.SetID(element.ID(af.ID))
342+
g.SetOriginID(element.ID(af.OriginID))
343+
g.SetDestinationID(element.ID(af.DestinationID))
344+
g.SetOriginConnectionIndex(0)
345+
g.SetDestinationConnectionIndex(0)
346+
if major <= 9 {
347+
g.SetOriginBezierVector("0;0")
348+
g.SetDestinationBezierVector("0;0")
349+
} else {
350+
line := genMf.NewBezierCurve()
351+
line.SetOriginControlVector("0;0")
352+
line.SetDestinationControlVector("0;0")
353+
g.SetLine(line)
354+
}
355+
return g
356+
}
357+
322358
// loopSourceToGen builds a loop's source (iterate-over-list or while-condition).
323359
func loopSourceToGen(ls microflows.LoopSource) element.Element {
324360
switch s := ls.(type) {

0 commit comments

Comments
 (0)