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.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ cases for these three BSON types — they fell to `default: return nil`.
| A **Decimal/DateTime** in `dynamictext` always rendered with the hardcoded default format ("5068.38000000"); no MDL way to set the per-parameter Format, and a widget-level `decimalPrecision:` was **silently dropped** (mxcli check ✓, exec ✓, but gone) | The model always stored `ClientTemplateParameter.FormattingInfo`, but **all three writers hardcoded it** (`DecimalPrecision:2, GroupDigits:false, DateFormat:Date, EnumFormat:Text`) and ignored `param.FormattingInfo`; the grammar had no syntax to set it and DESCRIBE dropped it on read too | grammar `mdl/grammar/domains/MDLPage.g4` (`paramAssignmentV3` + `paramFormatV3`), `mdl/ast/ast_page_v3.go` (`ParamFormatV3`), `mdl/visitor/visitor_page_v3.go` (`buildParamFormatV3`), builder `mdl/executor/cmd_pages_builder_v3_widgets.go` (`formattingInfoFromParamFormat`), writers `mdl/backend/modelsdk/widget_write.go` (`formattingInfoToGen`) + `sdk/mpr/writer_widgets.go` (`serializeClientTemplateParameter`), describe `cmd_pages_describe_output.go` (`formatParamFormatSuffix`), validate `validate_widgets.go` (`validateDynamicTextFormatting`/MDL-WIDGET18) | Add a per-param `FORMAT (decimalPrecision: N, groupDigits: bool, dateFormat: …, customDateFormat: '…', enumFormat: …)` block: `{1} = Amount format (decimalPrecision: 2, groupDigits: true)`. The **FORMAT keyword is required** — a bare `(…)` after the value is ambiguous with a function call because `:` is OQL division in expressions. Writers use the param's FormattingInfo when set, else the same hardcoded defaults (nil → byte-identical to before, zero risk to existing widgets). MDL-WIDGET18 turns a widget-level format key into an actionable error (no more silent drop) and validates keys/enums. Verified: exec → `mx check` (11.12.1) 0 errors + DESCRIBE round-trip. Repro `mdl-examples/bug-tests/ledger-75-dynamictext-formatting.mdl`. Ledger #75 |
| dynamic-text `format (…)` writes valid FormattingInfo, `mx check` ✓, but **renders unformatted** at runtime — a Decimal shows `-12` not `-12.00`, dates ignore the format | The parameter was serialized as `Expression: toString($currentObject/Attr)` (a non-String attribute was wrapped in `toString()`), and Mendix applies FormattingInfo **only to attribute-bound** params — an Expression param bypasses it. The BSON was valid but inert; `mx check` and DESCRIBE can't catch a *render* problem (the exact trap the runtime-verify skill exists for — I shipped #75 without it and the tester caught this) | `mdl/executor/cmd_pages_builder_v3.go` (`resolveTemplateAttributePathFull`, the bare-attribute branch) | Bind a bare non-String attribute as a structured **`AttributeRef`**, not a `toString()` Expression — the runtime then renders it through FormattingInfo, exactly as Studio Pro does. `toString()` was never required by mxbuild (AttributeRef for a Decimal/DateTime in a text template passes `mx check` → 0 errors). Engine read-parity holds. Note the `$param.Attr` non-String path (same function) still uses `toString()` — rarer, left for follow-up. Ledger #76 |
| A **DataGrid2 dynamic-text column** (`column x (ShowContentAs: dynamicText, Content: '{1}', ContentParams: [{1} = Attr format (…)])`) fails to open with **CE0463**, *and* its `format (…)` block is silently dropped. `mxcli docker check` hides the CE0463 because it runs `mx update-widgets` first; raw `mx check` and `mxbuild --serve` (run --local) surface it | Two independent gaps in the full-page object-list column path. (1) The shared `buildClientTemplateParams` never read the parsed `p.Format`, and the column-scoped serializer `SerializeColumnClientTemplateParameter` **hardcoded** FormattingInfo — so a column param's format was dropped at both write points. (2) A dynamic-text column has no attribute and no content widgets, so `detectObjectListItemKind` classified it as the **default** kind, which has no empty-ClientTemplate rules → its `tooltip` serialized as `TextTemplate:null`. Studio Pro stores an **empty `Forms$ClientTemplate`** there (as for an attribute column), so the widget failed to load | `mdl/executor/cmd_pages_builder_v3_widgets.go` (`buildClientTemplateParams` → apply `formattingInfoFromParamFormat(p.Format)`), `mdl/backend/widgetobj/builder.go` (`SerializeColumnClientTemplateParameter` honours `param.FormattingInfo`; new `itemKindDynamicText` + `emptyClientTemplateRules` tooltip entry; `detectObjectListItemKind`), describe `mdl/executor/cmd_pages_describe_pluggable.go` (`extractTextTemplateParameters` zips the format suffix) | Route the FORMAT block through the *shared* params helper (fixes the object-list column path **and** the ALTER PAGE column path at once) and stop hardcoding FormattingInfo in the column serializer. Classify a `showContentAs: dynamicText` column as its own item kind and give it the attribute column's `tooltip → empty CT` rule (exportValue stays null). **Diagnosis method that found the CE0463**: `update-widgets` on a *copy* cleared it → Case B (our BSON); a path-level flatten-diff of the datagrid subtree, mine vs the reconciled reference, isolated the single differing path `columns[dynamicText]/tooltip/TextTemplate` null↔empty. **Trap**: `docker check`'s built-in `update-widgets` masks the very defect you're hunting — measure with raw `mx check` or the serve build. Repro `mdl-examples/bug-tests/ledger-77-datagrid-dynamictext-column.mdl`; verified end-to-end (raw `mx check` 0 errors + Playwright cell renders `-1,234.50`). Ledger #77 |
| A workflow **`DECISION`** whose expression uses the documented lowercase `$workflowContext` fails `mx check` with **`[error] [CE0117] "Error(s) in expression." at Decision 'Decision'`**, while the *same spelling* in a `CALL MICROFLOW … WITH` clause works. `mxcli check` and `mxcli exec` both report success | The context parameter is named `WorkflowContext` and Mendix expressions are case-sensitive on 11.9+, so `$workflowContext` is an undefined variable. `normalizeWorkflowContextExpr` existed and was well-tested, but was only *applied* in `autoBindCallMicroflow` (the FINDINGS #39 fix) — `buildExclusiveSplit` stored `n.Expression` verbatim. The working WITH clause is what disguised it: the user reasonably concludes the spelling is fine | `mdl/executor/cmd_workflows_write.go` (`buildExclusiveSplit`, and the sibling `buildWaitForTimer` whose delay may reference a context date attribute) | Run the authored expression through the existing `normalizeWorkflowContextExpr` at every site that accepts a user expression — there are three in the workflow writer, and only the parameter-mapping one was covered. **Generalisable — the shape to look for**: a *normalizer that exists and is unit-tested* is not evidence it is *called*; grep the call sites, not the helper. When one input spelling works and an identical one fails, compare the two code paths before questioning the data. **Also fix the docs that teach the broken form** — `.claude/skills/mendix/write-workflows.md` showed lowercase in its DECISION example and is synced into user projects by `mxcli init` via `cmd/mxcli/skills/`, so the bug propagated to every generated project. Repro `mdl-examples/bug-tests/845-workflow-decision-context-casing.mdl`; verified end-to-end (`mx check` 11.13.0: 1 error → 0). Issue #845 |

**Key insight:** `microflows$ListRange` stores offset/limit inside a nested
`CustomRange` map — must cast `raw["CustomRange"].(map[string]any)` before
Expand Down
7 changes: 5 additions & 2 deletions .claude/skills/mendix/write-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ begin

-- Call a microflow (server logic); optional parameter mapping + outcomes
call microflow Module.ACT_Validate
with (Module.ACT_Validate.Item = '$workflowContext');
with (Module.ACT_Validate.Item = '$WorkflowContext');

-- Decision: a boolean or enum exclusive split
decision '$workflowContext/Total > 1000'
decision '$WorkflowContext/Total > 1000'
outcomes
true -> { call microflow Module.ACT_Escalate; }
false -> { call microflow Module.ACT_AutoApprove; };
Expand Down Expand Up @@ -182,6 +182,9 @@ documented in `system-module.md`.
- A user task / decision with a single outcome and no activity can trip
`CE1876` — give each branch a body or a distinct outcome.
- The context **Parameter entity must be persistent**.
- Write the context variable as **`$WorkflowContext`**, matching the parameter
name exactly. Mendix expressions are case-sensitive on 11.9+, so a lowercase
`$workflowContext` is an undefined variable and yields `CE0117`.

## Validate before presenting

Expand Down
47 changes: 47 additions & 0 deletions mdl-examples/bug-tests/845-workflow-decision-context-casing.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
-- Bug #845: workflow DECISION expressions were not case-normalized.
--
-- The workflow context parameter is stored as "WorkflowContext". Mendix
-- expressions are case-sensitive on 11.9+, so a user-written `$workflowContext`
-- is an undefined variable. CALL MICROFLOW `WITH` expressions were already
-- normalized (FINDINGS #39), but DECISION expressions were stored verbatim:
--
-- "Expression": "$workflowContext/IsExclusive"
--
-- mx check then reported, against an otherwise clean project:
--
-- [error] [CE0117] "Error(s) in expression." at Decision 'Decision'
--
-- The inconsistency is what hid the bug — the same spelling works in a WITH
-- clause and fails in a DECISION. The shipped write-workflows skill documented
-- the lowercase form in its DECISION example, so following the docs produced a
-- project that would not build.
--
-- Fix: buildExclusiveSplit (and its sibling buildWaitForTimer, whose delay may
-- reference a context date attribute) run the authored expression through
-- normalizeWorkflowContextExpr, the same helper autoBindCallMicroflow uses.
--
-- Manual verification (needs a project, so `make check-mdl` only syntax-checks this):
--
-- mxcli exec 845-workflow-decision-context-casing.mdl -p app.mpr
-- mx check -p app.mpr
--
-- Expect 0 errors. Before the fix this produced CE0117 on the decision.

create module Issue845;
create module role Issue845.User;

@position(100, 100)
create persistent entity Issue845.Ctx (
IsExclusive: boolean,
Total: decimal
);

-- Lowercase on purpose: this is the form the docs used to show.
create workflow Issue845."WF_DecisionCasing"
parameter $WorkflowContext: Issue845.Ctx
begin
decision '$workflowContext/IsExclusive'
outcomes
true -> { }
false -> { };
end workflow;
46 changes: 46 additions & 0 deletions mdl/executor/cmd_workflows_decision_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package executor

import (
"testing"

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

// TestBuildExclusiveSplit_NormalizesWorkflowContext guards issue #845.
//
// The workflow context parameter is stored as "WorkflowContext", and Mendix
// expressions are case-sensitive on 11.9+, so a user-written `$workflowContext`
// is an undefined variable. autoBindCallMicroflow already normalizes CALL
// MICROFLOW `WITH` expressions (FINDINGS #39), but DECISION expressions were
// stored verbatim, so the lowercase form reached the .mpr and mx check reported
//
// [error] [CE0117] "Error(s) in expression." at Decision 'Decision'
//
// The inconsistency is what made this hard to spot: the same `$workflowContext`
// spelling works in a WITH clause and fails in a DECISION.
func TestBuildExclusiveSplit_NormalizesWorkflowContext(t *testing.T) {
cases := map[string]string{
"$workflowContext/IsExclusive": "$WorkflowContext/IsExclusive",
"$WORKFLOWCONTEXT/Total > 100": "$WorkflowContext/Total > 100",
"$WorkflowContext/IsExclusive": "$WorkflowContext/IsExclusive",
"$Other/Field": "$Other/Field",
}
for in, want := range cases {
act := buildExclusiveSplit(&ast.WorkflowDecisionNode{Expression: in})
if act.Expression != want {
t.Errorf("buildExclusiveSplit(%q).Expression = %q, want %q", in, act.Expression, want)
}
}
}

// TestBuildWaitForTimer_NormalizesWorkflowContext covers the same defect in the
// sibling expression site: a WAIT FOR TIMER delay may reference a date attribute
// on the workflow context, and was likewise stored verbatim.
func TestBuildWaitForTimer_NormalizesWorkflowContext(t *testing.T) {
act := buildWaitForTimer(&ast.WorkflowWaitForTimerNode{
DelayExpression: "$workflowContext/DueDate",
})
if want := "$WorkflowContext/DueDate"; act.DelayExpression != want {
t.Errorf("buildWaitForTimer.DelayExpression = %q, want %q", act.DelayExpression, want)
}
}
8 changes: 6 additions & 2 deletions mdl/executor/cmd_workflows_write.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,10 @@ func buildCallWorkflowActivity(n *ast.WorkflowCallWorkflowNode) *workflows.CallW
func buildExclusiveSplit(n *ast.WorkflowDecisionNode) *workflows.ExclusiveSplitActivity {
act := &workflows.ExclusiveSplitActivity{}
act.ID = model.ID(generateWorkflowUUID())
act.Expression = n.Expression
// Same case-sensitivity trap as CALL MICROFLOW parameter mappings (#845):
// the context parameter is named "WorkflowContext", so a user-written
// `$workflowContext` is an undefined variable and mx check reports CE0117.
act.Expression = normalizeWorkflowContextExpr(n.Expression)
act.Caption = n.Caption

if act.Caption == "" {
Expand Down Expand Up @@ -459,7 +462,8 @@ func buildJumpTo(n *ast.WorkflowJumpToNode) *workflows.JumpToActivity {
func buildWaitForTimer(n *ast.WorkflowWaitForTimerNode) *workflows.WaitForTimerActivity {
act := &workflows.WaitForTimerActivity{}
act.ID = model.ID(generateWorkflowUUID())
act.DelayExpression = n.DelayExpression
// A delay may reference a date attribute on the workflow context (#845).
act.DelayExpression = normalizeWorkflowContextExpr(n.DelayExpression)
act.Caption = n.Caption

if act.Caption == "" {
Expand Down
Loading