Skip to content
Merged
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
1 change: 1 addition & 0 deletions .claude/skills/fix-issue/findings/mdl-executor.jsonl

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ commit $Product;
- `@annotation` before activity-binding metadata such as `@position`, `@caption`, `@color`, `@excluded`, or `@anchor` stays free-floating when later metadata binds the following activity
- `@annotation` at the end (no following activity) creates a free-floating note
- Escape single quotes by doubling: `@annotation 'Don''t forget'`
- **Leave `@position` out unless you are reproducing a hand-made diagram.** Without it the builder lays the flow out itself: the main line wraps onto rows past two canvas widths, a guard's branch drops into the lane below while the main line carries on above it, and a `case` of four or more branches leaves the decision in three groups so its lines do not cross. A statement with `@position` is never moved and is not measured against what is placed around it, so a few hand-placed statements in an otherwise automatic flow are what produces overlaps (mendixlabs/mxcli#1154)
- `@position` always appears in DESCRIBE output; `@caption` only when custom; `@color` only when not Default
- DESCRIBE MICROFLOW shows `@` annotations before their activities
- `@start(x, y)` positions the **start event** and goes on the first statement, because the start has no statement of its own. Omit it and the start is derived — one spacing unit (160) left of the first activity, on its centre line — and a rewrite re-derives it so the start follows the activities when they move. A start that is not at the derived spot was placed by hand (in Studio Pro or with `@start`): it survives a rewrite that does not mention it, and DESCRIBE emits `@start` for it. An explicit `@start` overrides both (#951)
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Changed

- **Microflows written without `@position` are laid out to be read, not just to be valid** (mendixlabs/mxcli#1154). Geometry and connection sides only: no MDL syntax changes, a statement carrying `@position` is never moved, and `describe` → `exec` still reports `Unchanged microflow`. Measured on a generated app of 41 microflows, none with an `@position`: the widest flow went from 6930×160 px on one row to 3220 px, with 0 overlapping elements and `mx check` at 0 errors before and after.

- **A long main line wraps onto rows.** Past 2880 px (two Studio Pro canvases) the next statement starts a new row under everything the current row occupies — branch lanes, loop boxes and notes included, measured after they are built rather than estimated. The line joining two rows leaves the bottom of the last element and arrives on top of the first, so it runs through the band between the rows instead of back across the activities. A tail short enough to finish within two more activities stays on its row rather than becoming a stub underneath.
- **The main line no longer waits for a guard's branch.** `if … then …; return; end if` draws its branch in the lane below and ends it there, yet the next element used to be placed past the branch's far end: 370 px from the split, against 40 px between any two activities. It now stands in the column the branch starts in, and only an element that reaches down into that lane — another decision, a loop, an activity with an error handler — waits for the lane to clear. A merge stands one ordinary gap after its branch (was 120 px), a branch ending in `return` is measured with the end event it draws, and a loop box keeps the same 40 px gap from its neighbours as an activity.
- **An enumeration split of four or more cases draws its lines in three groups** — the upper third from the split's top corner, the middle from its right, the lower from its bottom, every one arriving on the left of its activity, mirrored on the merge. The anchor pair on these flows is where the `case` order is stored, and past the third case the table behind it used sides no drawing would choose: case 4 left the split's left corner, cases 5–8 arrived on top of their activity. `describe` now reads the order as side, then position on the canvas; a model written with one pair per case reads exactly as before, and a split of up to three cases is unchanged.
- **A note sits above its element, not inside it.** Notes were offset from the element's centre, which is inside any loop box; they are now placed above its top edge, and the connector runs from the bottom of the note to the top of the element instead of top to top.

### Fixed

- **A page's image-collection reference passed `mxcli check --references` and failed the build** (mendixlabs/mxcli#1149) — `staticimage imgAll (Image: 'Atlas_UI_Resources.Atlas_Icons.checkbox_checked')` in a Selection helper's custom state checked clean, exec'd cleanly and then came back as `[error] [CE1613] "The selected image … no longer exists."`, once per state. The report asks for syntax, but the syntax landed with #1057 — describe emits the three `staticimage` lines and re-running the description reports `Unchanged page`, measured on a blank 11.14.0 project. What was missing is that nothing resolved the name #1057 had made writable.
Expand Down
9 changes: 9 additions & 0 deletions cmd/mxcli/syntax/features_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,15 @@ func init() {
"pixel offset from its end of the line. (0, 0) at both ends is straight.\n" +
"@position on a split belongs to the SPLIT, so its end-if join has its own\n" +
"annotation. Container Size is still computed, not authorable.\n\n" +
"WITHOUT @position the builder places everything. The main line runs left to\n" +
"right and wraps onto a new row past 2880px; the line joining two rows leaves\n" +
"the bottom of one and arrives on top of the next. A guard (if … return; end\n" +
"if) drops its branch into the lane below and the main line carries straight\n" +
"on over it. A CASE of four or more branches leaves the split in three groups\n" +
"— top, right, bottom — so its lines do not cross. A statement that carries\n" +
"@position is never moved, and starts the row for what follows it. Prefer no\n" +
"@position at all to a few: hand-placed statements are not measured against\n" +
"what the builder puts around them. (#1154)\n\n" +
"@start and @merge position the two nodes that have no statement of their\n" +
"own, so each is written on the statement it belongs to. Omit @start and the\n" +
"start is placed one spacing unit left of the first activity, on its centre\n" +
Expand Down
9 changes: 9 additions & 0 deletions docs-site/src/language/microflow-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ Set the canvas position of the next activity:
$Order = CREATE Sales.Order (Status = 'New');
```

Positions are optional. A microflow written without them is laid out by mxcli: the
main line runs left to right and wraps onto a new row once it passes two canvas
widths (2880 px); a guard — `if … then …; return; end if` — drops its branch into the
lane below while the main line carries on above it; and a `case` of four or more
branches leaves the decision in three groups (top, right, bottom) so its lines do not
cross. A statement that carries `@position` is never moved, and becomes the start of
the row for the statements after it — so either place everything or nothing: a few
hand-placed statements are not measured against what is laid out around them.

### Start event

The start event has no statement of its own, so `@start` goes on the **first**
Expand Down
112 changes: 112 additions & 0 deletions mdl-examples/bug-tests/microflow-layout-rows-lanes-cases.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
-- Auto-layout of a microflow written with no @position: long main lines, guards
-- and wide enumeration splits.
--
-- Measured on a generated app of 41 microflows (Mendix 11.12.1, mxcli v0.23.0), none
-- of them carrying an @position: nothing overlapped, and the widest flow was
-- 6930x160 px - four screens of sideways scrolling. Every guard left 370px of bare
-- main line above its own branch (40px between any two activities), a merge stood
-- 120px after its branch, and a 7-case split sent its fourth case out of the split's
-- LEFT corner and its fifth onwards onto the TOP of their activities, because the
-- anchor pair was storing the case order.
--
-- Open the three flows in Studio Pro after exec:
-- MF_LayoutLong wraps onto rows; the joining line runs between the rows
-- MF_LayoutGuards each activity after a guard stands in the branch's column
-- MF_LayoutCases case lines leave the split top / right / bottom, 2-3-2
--
-- mendixlabs/mxcli#1154

CREATE ENUMERATION MyFirstModule.LayoutStatus (
NewOrder 'New', Confirmed 'Confirmed', InProgress 'In progress', Shipped 'Shipped',
Delivered 'Delivered', Invoiced 'Invoiced'
);

CREATE MICROFLOW MyFirstModule.MF_LayoutLong ()
BEGIN
LOG INFO 'step 1';
LOG INFO 'step 2';
LOG INFO 'step 3';
LOG INFO 'step 4';
LOG INFO 'step 5';
LOG INFO 'step 6';
LOG INFO 'step 7';
LOG INFO 'step 8';
LOG INFO 'step 9';
LOG INFO 'step 10';
LOG INFO 'step 11';
LOG INFO 'step 12';
LOG INFO 'step 13';
LOG INFO 'step 14';
LOG INFO 'step 15';
LOG INFO 'step 16';
LOG INFO 'step 17';
LOG INFO 'step 18';
LOG INFO 'step 19';
LOG INFO 'step 20';
LOG INFO 'step 21';
LOG INFO 'step 22';
LOG INFO 'step 23';
LOG INFO 'step 24';
LOG INFO 'step 25';
LOG INFO 'step 26';
LOG INFO 'step 27';
LOG INFO 'step 28';
LOG INFO 'step 29';
LOG INFO 'step 30';
LOG INFO 'step 31';
LOG INFO 'step 32';
LOG INFO 'step 33';
LOG INFO 'step 34';
LOG INFO 'step 35';
LOG INFO 'step 36';
LOG INFO 'step 37';
LOG INFO 'step 38';
LOG INFO 'step 39';
LOG INFO 'step 40';
END;
/

CREATE MICROFLOW MyFirstModule.MF_LayoutGuards ($Amount: Integer, $Limit: Integer)
RETURNS Boolean AS $IsAccepted
BEGIN
IF $Amount = 0 THEN
LOG WARNING 'nothing to place';
RETURN false;
END IF;
LOG INFO 'amount present';
IF $Amount > $Limit THEN
LOG WARNING 'over the limit';
RETURN false;
END IF;
IF $Limit < 0 THEN
LOG WARNING 'no credit at all';
RETURN false;
END IF;
LOG INFO 'accepted';
RETURN true;
END;
/

CREATE MICROFLOW MyFirstModule.MF_LayoutCases ($Status: MyFirstModule.LayoutStatus)
RETURNS String AS $Next
BEGIN
DECLARE $Next String = '';
CASE $Status
WHEN NewOrder THEN
SET $Next = 'Confirmed';
WHEN Confirmed THEN
SET $Next = 'InProgress';
WHEN InProgress THEN
SET $Next = 'Shipped';
WHEN Shipped THEN
SET $Next = 'Delivered';
WHEN Delivered THEN
SET $Next = 'Invoiced';
WHEN Invoiced THEN
SET $Next = 'Invoiced';
WHEN (empty) THEN
SET $Next = 'NewOrder';
END CASE;
RETURN $Next;
END;
/
5 changes: 4 additions & 1 deletion mdl/backend/modelsdk/microflow_write.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,10 @@ func annotationFlowToGen(af *microflows.AnnotationFlow, major int) element.Eleme
g.SetID(element.ID(af.ID))
g.SetOriginID(element.ID(af.OriginID))
g.SetDestinationID(element.ID(af.DestinationID))
g.SetOriginConnectionIndex(0)
// A note sits ABOVE the element it documents, so its line leaves the note's
// bottom edge and enters the element's top. Both indexes were 0 (top), which
// drew the line out of the top of the note and back down around it.
g.SetOriginConnectionIndex(2)
g.SetDestinationConnectionIndex(0)
if major <= 9 {
g.SetOriginBezierVector("0;0")
Expand Down
4 changes: 2 additions & 2 deletions mdl/executor/bugfix_regression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func TestAddLoopStatement_PreservesAnnotatedPosition(t *testing.T) {
if loop.Position.X != 350 || loop.Position.Y != 200 {
t.Fatalf("got loop position (%d, %d), want (350, 200)", loop.Position.X, loop.Position.Y)
}
wantNextX := 350 + loop.Size.Width/2 + HorizontalSpacing
wantNextX := 350 + loop.Size.Width/2 + ActivityWidth/2 + (HorizontalSpacing - ActivityWidth) // the next activity sits one activity-gap past the box
if fb.posX != wantNextX {
t.Fatalf("got next posX %d, want %d", fb.posX, wantNextX)
}
Expand Down Expand Up @@ -136,7 +136,7 @@ func TestAddWhileStatement_PreservesAnnotatedPosition(t *testing.T) {
if loop.Position.X != 420 || loop.Position.Y != 180 {
t.Fatalf("got while position (%d, %d), want (420, 180)", loop.Position.X, loop.Position.Y)
}
wantNextX := 420 + loop.Size.Width/2 + HorizontalSpacing
wantNextX := 420 + loop.Size.Width/2 + ActivityWidth/2 + (HorizontalSpacing - ActivityWidth) // the next activity sits one activity-gap past the box
if fb.posX != wantNextX {
t.Fatalf("got next posX %d, want %d", fb.posX, wantNextX)
}
Expand Down
2 changes: 2 additions & 0 deletions mdl/executor/cmd_microflows_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ func buildMicroflowFromStmt(ctx *ExecContext, s *ast.CreateMicroflowStmt, opts b
varTypes: varTypes,
declaredVars: declaredVars,
measurer: &layoutMeasurer{varTypes: varTypes},
allowWrap: true,
backend: ctx.Backend,
hierarchy: hierarchy,
restServices: restServices,
Expand Down Expand Up @@ -685,6 +686,7 @@ func buildNanoflowFromStmt(ctx *ExecContext, s *ast.CreateNanoflowStmt, opts bui
varTypes: varTypes,
declaredVars: declaredVars,
measurer: &layoutMeasurer{varTypes: varTypes},
allowWrap: true,
backend: ctx.Backend,
hierarchy: hierarchy,
restServices: restServices,
Expand Down
10 changes: 10 additions & 0 deletions mdl/executor/cmd_microflows_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ type flowBuilder struct {
// pendingJoin is the `join` addStatement just saw, waiting for the enclosing
// body loop to say which activity the path had reached.
pendingJoin *ast.JoinStmt
// lowerLane is how far right the lane under each main line (keyed by the line's
// y) is occupied by a guard's branch. See layout_lanes.go.
lowerLane map[int]int
// allowWrap turns on row wrapping for this builder: the main line breaks onto a
// new row past MaxRowWidth instead of running off the canvas (layout_rows.go).
// Only the builders that lay out a whole microflow set it; a loop body builds in
// its own coordinate space inside a box that is sized to fit, so wrapping there
// would fight the box rather than help the reader.
allowWrap bool
row rowTracker
}

type flowBuilderVariableState struct {
Expand Down
Loading
Loading