Skip to content

Commit c4331e6

Browse files
committed
fix(pages): write a text box's and date picker's own FormattingInfo
GroupDigits / DecimalPrecision on a TEXTBOX and DateFormat / CustomDateFormat on a DATEPICKER (or TEXTBOX) were accepted silently and the writer always emitted the default Forms$FormattingInfo. Read them in the builder, write them through the existing formattingInfoToGen, describe the non-default ones, check bad values (MDL-WIDGET18), and warn (MDL-WIDGET07) when the keys land on a built-in widget that has no FormattingInfo. Field run: ProcureFlow (Mendix 11.14.0) scratch copy — txtTsTouchless (GroupDigits: true) and date pickers dpFrom (Custom 'd MMM yyyy') / dpTo (DateTime): stored FormattingInfo carries the values, describe prints them, mx check 0 errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5dJ4raEjbm8bFiKeYtchS
1 parent 9509176 commit c4331e6

17 files changed

Lines changed: 341 additions & 9 deletions

‎.claude/skills/fix-issue/findings/mdl-executor.jsonl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,3 +713,4 @@
713713
{"area": "mdl/executor", "date": "2026-09-25", "symptom": "`textbox t (Attribute: FullName)` at the top of a page (CREATE PAGE/SNIPPET, a plain container, or ALTER PAGE … INSERT at page level) passed plain `mxcli check`, `exec --no-check`/ALTER reported success, and `bson dump` showed `AttributeRef: null` — mxbuild 11.13.0: CE0544 \"This widget can only function inside a data context\" + CE7005 (textbox/textarea/datepicker/checkbox/radiobuttons/dropdown), CE0402 (dynamictext Attribute:), CE0642 (combobox). Qualified `Mod.Ent.Attr` there is stored and fails CE0544/CE2421/CE1365/CE7247 \"Move this widget into a data container\" + CE7006. `Attribute: $P/Attr` / `$currentObject/Attr` dropped even INSIDE a data view.", "cause": "resolveAttributePath returns the bare name when entityContext is \"\", and attributeRefToGen (and widgetobj setAttributeRefField) write nil for any path with < 2 dots, so the binding vanished between builder and writer; refuseBareAttributeRefs never sees it because no Attribute string is emitted. The only refusal (validatePageContextTree) runs in the --references phase for CREATE PAGE/SNIPPET, so plain check, --no-check and ALTER were unguarded. `$x/Attr` parses via the generic property rule as an *ast.DataSourceV3, so GetAttribute() returns \"\" and every builder skipped it.", "file": "mdl/executor/cmd_pages_input_binding_context.go (inputBindingProblem, checkInputBinding, validateInputBindingContext = MDL-WIDGET34), wired in cmd_pages_builder_v3_widgets.go (6 input builders + buildDynamicTextV3), widget_engine.go (primary Attribute mapping), validate_widgets.go (validateWidgetTreeIn); tests cmd_pages_input_binding_context_test.go; bug-tests input-binding-without-context{,.fail}.mdl", "insight": "Reuse the MDL-PAGEARG01 three-state context (pageArgContext known/present) rather than entityContext==\"\" as the 'outside a data container' signal: entityContext is also empty INSIDE a container whose flow cannot be resolved (excluded ShareFeedback_Logo), where DESCRIBE writes qualified names that must keep building — refusing qualified-on-empty-entity would have broken that round trip. So known-absent context refuses bare AND qualified; unknown context (ALTER) refuses only the bare name the writer provably nulls. Two existing unit tests (OnChangeSurvivesBuilder, DynamicTextV3_AttributeBinds) built inputs with NO entity and passed — the second asserted a bare `Title` AttributeRef counted as 'bound', i.e. it pinned the bug: when a fixture has no entity context, ask what the writer does with its output. The `$P/Attr` drop was found only by dumping the control page, not from the report — print the AST value type with a probe test before assuming a spelling reaches the builder. Evidence: 22 mxbuild errors before on the probe matrix; after, every case refused with nothing written, controls (dataview/listview/gallery/datagrid/snippet dataview/ALTER into dataview) 0 errors, 17/17 stock pages + 4/4 snippets describe→exec round trip.", "refs": ["MDL-WIDGET34"], "ce": ["CE0544", "CE7005", "CE0402", "CE0642", "CE2421", "CE1365", "CE7247", "CE7006"]}
714714
{"area":"mdl/executor","date":"2026-09-25","symptom":"`alter page FeedbackModule.ShareFeedback_Logo { insert after textBox1 { image zzImg (ImageType: imageUrl, ImageUrl: '{1}', ImageUrlParams: [{1} = ImageB64]) } }` passed `check --references`; exec wrote a bare AttributeRef and `mx check` could not LOAD the project (ArgumentNullException setting 'Attribute'). Same at page top level outside any data container; a text box's `Attribute:` there is silently dropped (CE7005).","cause":"ALTER's entity context comes from the STORED document (nearest enclosing data source, or a flow source's return type via resolveDataSourceFlowEntity). With the flow missing (Feedback v4.0.2 ships no DS_FeedbackForm) or no container at all, entityContext is \"\" and resolveAttributePath returns the bare name. CREATE PAGE refused this at check time (relaxExcludedWidgetRefs/unscopedBindings, #678); ALTER's check never opened the document, so nothing could know the scope.","file":"mdl/executor/validate_alter_unscoped.go, mdl/executor/cmd_alter_page.go (alterEntityContext), mdl/executor/validate.go (bindingsWithoutScope)","insight":"For ALTER, scope is a property of the stored document, not the statement: a check-time question about it must open the document (OpenPageForMutation, never Save; validate_alter_set.go already does this) and ask through the SAME function exec uses, so the INSERT/REPLACE entity resolution was lifted into alterEntityContext rather than restated, and the binding walk lifted out of unscopedBindings (bindingsWithoutScope) rather than copied. Controls that keep it from blocking working scripts: skip a target the stored doc lacks (added earlier in the script), a flow the script declares with an entity return (sc.flowParams), DataGrid2 column and list-view-template paths, and documents the script creates. Reproduce with a Studio Pro-authored page whose flow is genuinely absent.","refs":["#678","#685"]}
715715
{"area": "mdl/executor", "date": "2026-09-25", "symptom": "`describe page` on a File Uploader (files mode) emits `DataSource: association …`, and exec of that output fails: \"widget `upFiles` (fileuploader) exposes 2 datasources, so a generic `datasource:` clause is ambiguous — name the one you mean: associatedFiles, associatedImages\"", "cause": "DESCRIBE chose generic vs named by counting CONFIGURED datasources (namedCustomWidgetDataSources drops unset ones, so files mode = 1), while the builder's refuseAmbiguousGenericDataSource counts DECLARED datasource mappings (a generated def maps every top-level datasource = 2). Two sides of one round trip deciding the same question from different evidence.", "file": "mdl/executor/cmd_pages_describe_parse.go", "insight": "When describe and build each decide 'is this ambiguous?', they must count the same set. Fix read the DECLARED count from the stored schema (PropertyTypes with ValueType.Type=DataSource, excluding IsLinked) and excluded widgets with an embedded .def.json — those are hand-written and pick one datasource mapping per mode, so a database-mode ComboBox (two declared) must keep the generic clause. The #956 bug-test script itself authored the refused generic clause on a File Uploader: a bug-test that only runs `mxcli check` without a project can't see an exec-time refusal, so grep bug-tests for the old spelling whenever a builder starts refusing one.", "refs": ["mendixlabs/mxcli#1199", "mendixlabs/mxcli#956", "mendixlabs/mxcli#1109"], "rules": []}
716+
{"area": "mdl/executor", "date": "2026-09-26", "symptom": "`textbox t (…, GroupDigits: true, DecimalPrecision: 2)` and `datepicker d (…, DateFormat: Custom, CustomDateFormat: 'd MMM yyyy')`: check clean, exec 'Created page', describe shows neither, stored FormattingInfo is the default (GroupDigits false, DateFormat Date)", "cause": "The TextBox/DatePicker writers always called newFormattingInfo(); the builder never read the keys; and validateStaticWidgetUnknownProps skipped the dynamic-text format keys (paramFormatKeys) on EVERY widget type, so the silent drop had no MDL-WIDGET07 either.", "file": "mdl/executor/input_formatting.go", "insight": "An exemption list written for one widget (dynamictext's WIDGET18 hand-off) applied type-agnostically becomes a silence for every other widget carrying the same key names. Scope such exemptions to the widget types that consume the key. The FormattingInfo writer (formattingInfoToGen) already existed for dynamic-text parameters — the input widgets just never used it.", "rules": ["MDL-WIDGET07", "MDL-WIDGET18"]}

‎.claude/skills/mendix/create-page/SKILL.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Both are optional and can be changed later with `alter page … { set Class = '
6767
| Widget name | Required after type | `textbox txtName (...)` |
6868
| Attribute binding | `attribute: AttrName` | `textbox txt (label: 'Name', attribute: Name)` |
6969
| Attribute over an association | `attribute: Assoc/Attr` (bare association name, multi-hop OK) | `textbox txt (label: 'Rule', attribute: RuleAction_BusinessRule/Name)` |
70+
| Input formatting | `GroupDigits:`, `DecimalPrecision:` (textbox); `DateFormat:`, `CustomDateFormat:` (datepicker) | `datepicker d (attribute: DueDate, DateFormat: Custom, CustomDateFormat: 'd MMM yyyy')` |
7071
| Password field | `Password: true` | `textbox tbPw (attribute: Secret, Password: true)` |
7172
| Widget validation | `Validation: '<expr>'` + `ValidationMessage: '<text>'` | `Validation: 'length(toString($value)) > 0'` — quoted, not `[bracketed]` |
7273
| Variable binding | `datasource: $Var` | `dataview dv (datasource: $Product) { ... }` |

‎.claude/skills/mendix/create-page/reference/widgets.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,18 @@ radiobuttons rbStatus (label: 'Status', attribute: status)
484484
datepicker dpCreated (label: 'Created Date', attribute: CreatedDate)
485485
```
486486

487+
**Formatting** — a `textbox` and a `datepicker` carry Studio Pro's own
488+
Formatting group, with the same keys a `dynamictext` parameter's `format (…)`
489+
takes: `DecimalPrecision` (integer) and `GroupDigits` (boolean) on a text box
490+
over a number, `DateFormat` (`Date` | `DateTime` | `Time` | `Custom`) and
491+
`CustomDateFormat` (a pattern; implies `Custom`) on a date picker — a text box
492+
cannot bind a date (CE2421). A bad value is MDL-WIDGET18. Other inputs (textarea, checkbox, …)
493+
have no formatting, and the keys there are MDL-WIDGET07:
494+
```sql
495+
textbox tbAmount (label: 'Amount', attribute: Amount, GroupDigits: true, DecimalPrecision: 2)
496+
datepicker dpDue (label: 'Due', attribute: DueDate, DateFormat: Custom, CustomDateFormat: 'd MMM yyyy')
497+
```
498+
487499
**COMBOBOX** - Combo box (pluggable widget):
488500
```sql
489501
-- Enumeration mode (attribute is an enum type):

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1414

1515
### Fixed
1616

17+
- **A text box's Group digits / Decimal precision and a date picker's Date format were dropped without a word** — `textbox t (Attribute: Amount, GroupDigits: true)` and `datepicker d (Attribute: From, DateFormat: Custom, CustomDateFormat: 'd MMM yyyy')` passed `check` (the keys are the dynamic-text format keys, which the unknown-property warning exempted) and the widget was always written with the default `Forms$FormattingInfo`. Both widgets now store `DecimalPrecision`, `GroupDigits`, `DateFormat` and `CustomDateFormat` in their own FormattingInfo, `describe` prints the non-default ones back, a bad value is MDL-WIDGET18 at check time, and the same keys on a widget with no FormattingInfo (a text area, a check box) now draw MDL-WIDGET07 instead of vanishing.
1718
- **Keyword operators were fused in some stored expressions** — a page action's microflow argument `Flag: $a and $b` was stored as `$aand$b`, `if $x then 'a' else 'b'` as `if$xthen'a'else'b'`; the same in `contentparams` values, `send rest request … with (…)` parameters and a dynamic `execute database query`. Those four places took the expression's text without its whitespace; literals, `+` and `$currentObject` were unaffected, which is why it went unnoticed, and `check --references` passed. Measured by decoding the stored units on a Mendix 11.14.0 project. The expression is now stored as written, with MDL comments removed, as the microflow expression sites already did.
1819
- **An input bound to an attribute outside any data container was written with no binding** — `textbox t (Attribute: FullName)` at the top of a page (or of a snippet, or inside a plain container) has no entity to qualify the name with, and the writer stored it as `AttributeRef: null`. Plain `check` passed, `exec --no-check` and `alter page … insert` at page level said success, and mxbuild 11.13.0 failed the page with CE0544 "This widget can only function inside a data context" + CE7005 (text box, text area, date picker, check box, radio buttons, drop-down), CE0402 (dynamic text `Attribute:`) or CE0642 (combo box). A qualified attribute there is stored and fails the same way (CE0544 / CE2421 / CE1365 / CE7247 "Move this widget into a data container"). `Attribute: $P/Name` and `Attribute: $currentObject/Name` never parsed as an attribute path and were dropped even inside a data view. `check` now reports all three as **MDL-WIDGET34** (no project needed), and the page builder refuses them with the widget named, so nothing is written; `alter page` refuses a bare name it has no entity for. Place the widget in a data view, list view, gallery or data grid and bind the attribute by name.
1920
- **An expression property written in brackets was silently dropped** (mendixlabs/mxcli#750) — `dynamicclasses: [ if $currentObject/Featured then 'a' else 'b' ]`, the spelling #750 proposes, parsed as a list that no writer reads: `check` was clean, `exec` said `Created page`, and the widget was stored with no dynamic class. `alter page … set DynamicClasses = [ … ]` said `Altered page` and changed nothing, and a column's `DynamicCellClass` stored the list's text — tokens fused, `[if$x/Ythen'a'else'b']` — as its expression. Measured on a copy of a Mendix 11.14.0 project with the pre-fix binary. `mxcli check` now reports **MDL-WIDGET32** for `DynamicClasses` and `DynamicCellClass` written as a list (no project needed), and ALTER refuses it, so `check -p` reports that too. Write the expression quoted.

‎cmd/mxcli/syntax/features_page.go‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,12 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) {
143143
"-- A FILTER block written on a DATAGRID is not a column filter and not a\n" +
144144
"-- container the grid declares — it used to be dropped on write with no\n" +
145145
"-- diagnostic, and is now refused (MDL-WIDGET30).\n\n" +
146-
"-- Inputs\nTEXTBOX name (Label: 'L', Attribute: Attr)\nTEXTAREA | DATEPICKER | COMBOBOX | CHECKBOX | RADIOBUTTONS\n\n" +
146+
"-- Inputs\nTEXTBOX name (Label: 'L', Attribute: Attr)\nTEXTAREA | DATEPICKER | COMBOBOX | CHECKBOX | RADIOBUTTONS\n" +
147+
"-- The widget's own Formatting: DecimalPrecision / GroupDigits on a TEXTBOX,\n" +
148+
"-- DateFormat (Date | DateTime | Time | Custom) + CustomDateFormat on a\n" +
149+
"-- DATEPICKER (a TEXTBOX cannot bind a date: CE2421):\n" +
150+
"TEXTBOX t (Attribute: Amount, GroupDigits: true, DecimalPrecision: 2)\n" +
151+
"DATEPICKER d (Attribute: DueDate, DateFormat: Custom, CustomDateFormat: 'd MMM yyyy')\n\n" +
147152
"-- Actions\nACTIONBUTTON name (Caption: 'C', Action: SAVE_CHANGES, ButtonStyle: Primary)\nLINKBUTTON name (Caption: 'C', Action: ...)\n\n" +
148153
"-- Display\nDYNAMICTEXT name (Content: 'Hello, {1}!', ContentParams: [{1} = Name])\nTITLE name (Content: 'Heading')\nIMAGE name (Image: 'Module.Collection.ImageName')\nIMAGE name (ImageType: imageUrl, ImageUrl: 'https://…')\n" +
149154
"-- IMAGE needs a source. Its default, `ImageType: image`, shows an entry from an\n" +

‎docs-site/src/language/widget-types.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,16 @@ Date and/or time input:
276276
DATEPICKER dpBirthDate (Label: 'Birth Date', Attribute: BirthDate)
277277
```
278278

279+
A `TEXTBOX` and a `DATEPICKER` have Studio Pro's **Formatting** group. The keys
280+
are those of a `DYNAMICTEXT` parameter's `format (…)`: `DecimalPrecision` and
281+
`GroupDigits` for a number in a text box, and `DateFormat` (`Date`, `DateTime`,
282+
`Time` or `Custom`) with `CustomDateFormat` for a date in a date picker:
283+
284+
```sql
285+
TEXTBOX txtAmount (Label: 'Amount', Attribute: Amount, GroupDigits: true, DecimalPrecision: 2)
286+
DATEPICKER dpDue (Label: 'Due', Attribute: DueDate, DateFormat: Custom, CustomDateFormat: 'd MMM yyyy')
287+
```
288+
279289
### COMBOBOX
280290

281291
Dropdown selection for enumeration values or associations. Uses the pluggable ComboBox widget:

‎docs/01-project/MDL_QUICK_REFERENCE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,6 +1437,7 @@ MDL uses explicit property declarations for pages:
14371437
| Widget name | Required after type | `textbox txtName (...)` |
14381438
| Attribute binding | `attribute: AttrName` | `textbox txt (label: 'Name', attribute: Name)` |
14391439
| Attribute over an association | `attribute: Assoc/Attr` (bare association name, multi-hop OK) | `textbox txt (label: 'Rule', attribute: RuleAction_BusinessRule/Name)` — works on textbox, textarea, datepicker, dropdown, checkbox and radiobuttons, the same as on a data grid column |
1440+
| Input formatting | `DecimalPrecision: n`, `GroupDigits: bool` (textbox); `DateFormat: Date\|DateTime\|Time\|Custom`, `CustomDateFormat: 'pattern'` (datepicker) | `textbox t (attribute: Amount, GroupDigits: true, DecimalPrecision: 2)`, `datepicker d (attribute: DueDate, DateFormat: Custom, CustomDateFormat: 'd MMM yyyy')` — the widget's own Formatting, the same keys as a `dynamictext` parameter's `format (…)`. A bad value is **MDL-WIDGET18**; on a widget with no formatting (textarea, checkbox) the keys are **MDL-WIDGET07** |
14401441
| Password field | `Password: true` on a textbox | `textbox tbPw (attribute: Secret, Password: true)` — omitted when false. Without it a describe → exec round trip turns a password field into a plaintext one |
14411442
| Widget validation | `Validation: '<expression>'`, `ValidationMessage: '<text>'` | `Validation: 'length(toString($value)) > 0'` — a Mendix expression over `$value`, QUOTED not bracketed (`[...]` is the XPath spelling and parses as an array) |
14421443
| Variable binding | `datasource: $Var` | `dataview dv (datasource: $Product) { ... }` |

‎mdl/backend/modelsdk/widget_write.go‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,10 @@ func widgetToGen(w pages.Widget) (element.Element, error) {
463463
g.SetAttributeRef(ref)
464464
}
465465
g.SetEditable(pages.WidgetEditability(&x.BaseWidget))
466-
g.SetFormattingInfo(newFormattingInfo())
466+
// The box's own Formatting group (Decimal precision, Group digits, and
467+
// the date format for a date attribute). Nil is the default Studio Pro
468+
// stores on an unformatted box.
469+
g.SetFormattingInfo(formattingInfoToGen(x.FormattingInfo))
467470
g.SetInputMask("")
468471
g.SetIsPasswordBox(x.IsPassword)
469472
g.SetKeyboardType("Default")
@@ -585,7 +588,8 @@ func widgetToGen(w pages.Widget) (element.Element, error) {
585588
g.SetAttributeRef(ref)
586589
}
587590
g.SetEditable(pages.WidgetEditability(&x.BaseWidget))
588-
g.SetFormattingInfo(newFormattingInfo())
591+
// The picker's "Date format"; nil is the default Date format.
592+
g.SetFormattingInfo(formattingInfoToGen(x.FormattingInfo))
589593
if x.Label != "" {
590594
g.SetLabelTemplate(textAsClientTemplate(textFromString(x.Label)))
591595
}

0 commit comments

Comments
 (0)