Sync ako/mxcli: List View specialization templates, packaging fixes, and folder placement for every document type - #942
Merged
Merged
Conversation
…doctype A document's folder is its unit's ContainerID — a column in the Unit row, not a field in the document's contents. Every Update* on both engines rewrites contents only, so a handler that resolved a folder, stored it on the model object and called Update had the placement silently dropped: the statement reported success, resolveFolder had already created the folder as a side effect, and the document stayed where it was. Reported against JSON structures (mendixlabs#932); it was true of every doctype with a FOLDER clause. Verified against raw Unit-table containment counts rather than mxcli's own read paths, because the same symptom was once a read illusion (mendixlabs#722 / Bug 12b): before the fix a modify-with-folder left "Documents in Modules" and "Documents in Folders" unchanged, while an explicit MOVE of a supported doctype — the positive control — shifted them by one. Fixed as a class. One type-agnostic MoveDocument(unitID, containerID) on the backend rather than a Move<Doctype> per document type: placement is the same row update whatever the document is, and a per-doctype method means the next document type inherits the bug simply by not having one written for it. FindDocumentUnit resolves a name through the unit table for the same reason. Two things the row update has to do for itself, because a move changes no byte of the document and is therefore invisible to content-based no-op elision (ADR-0008): count in WriteStats, so ReportMutation stops calling a real move "Unchanged", and skip when the unit is already in that container, so re-running an in-sync script leaves the .mpr byte-identical. Removing either fails TestMoveDocumentPersistsAndIsIdempotent. An omitted FOLDER must leave placement alone — resolveFolder("") answers with the module root, so passing it through would unfile every foldered document on the next CREATE OR MODIFY, which is worse than the no-op being fixed. resolveRequestedFolder returns "" for "not asked" and applyDocumentFolder ignores "", so no call site has to remember the distinction. Doctypes rewritten as delete+create (snippets, rest clients) had the inverse bug already live — they really do re-apply the container — and now carry the stored one forward. mx check: 1078 errors before and after on the probe project (all pre-existing CE6083 design-property errors), so no new build errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
… listing hiding them MOVE listed nine doctypes. The other twenty-two were never unimplemented, merely unlisted — a top-level document's move is one containment row whatever the document is — so `MOVE IMPORT MAPPING …` was a parse error while MoveImportMapping sat fully wired on the interface, both engines and the mock, called from nowhere. Reported in mendixlabs#932 for mappings and JSON structures. The doctype list is now a grammar RULE rather than an inline alternation, and that is the substantive part. MOVE FOLDER is told from a document move by the ABSENCE of a doctype, and that discriminator was a hand-written negation of every doctype keyword (mxcli-formula1 #32 flagged it after adding two). With twenty-two more it would not have stayed in step; as a sub-rule it is one check that cannot go stale. The document is resolved through the unit table rather than a per-kind list, so one lookup covers every doctype including ones added later. Deliberately no doctype-to-$Type table to validate against: two such tables already exist here and disagree with each other and with a real project (unit_types.go says JavaActions$JavaScriptAction, while the writer inserts and Studio Pro stores JavaScriptActions$JavaScriptAction). The kind is derived from the stored $Type instead, and a mismatch is refused only when the derived kind is itself a doctype MOVE can spell — so a derivation that cannot be vouched for defers to the name lookup instead of blocking a legitimate move. Output names the kind found, not the kind the statement asked for, since the two can differ. LIST FOLDERS had the same shape of defect and it made moves unverifiable: its hand-maintained kind list still hid layouts, menus, JavaScript actions and building blocks after mendixlabs#892 added five kinds to it. It now fills its gaps from the same type-agnostic walk — measured 78 to 259 documents listed on a 7-module project, with nothing removed. Docs, syntax help and the organize-project skill carried a doctype list that was stale before this change, omitting three types MOVE already accepted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
Import and export mappings had none, which is half of mendixlabs#932 — and so did queues, scheduled events, regular expressions, workflows, menus, image collections, Java and JavaScript actions, database connections, data transformers and the four AI-agent documents. All of them could only ever be created at the module root, and DESCRIBE emitted no folder either, so describing one that Studio Pro had filed away and replaying it recreated it unfiled. The clause goes in one place — straight after the qualified name — rather than matching each rule's local style. Pages/snippets (Folder: 'path' property) and microflows (folder 'path' before BEGIN) already disagree with each other; a third convention per doctype would make the syntax unlearnable. Two rules already contained a direct STRING_LITERAL, and one of them was a trap. createWorkflowStatement read DISPLAY / DESCRIPTION / DUE DATE by counting AllSTRING_LITERAL() in order, so a folder path — now the rule's first string — silently became the display name and shifted every later clause by one. Fixed by labelling the tokens in the grammar and reading them by label, not by placing the new clause last: positional counting is wrong for any combination of optional clauses, not just this one. Reverting the visitor to positional makes TestWorkflowHeaderClausesAreReadByLabel report the folder path as the display name. Same treatment for the data transformer's source path. The executor side is uniform: containerForDocument encodes the precedence (named folder, else current placement, else module root) once, so no handler decides it again — and "no clause" keeps meaning "leave placement alone". Verified by replaying a DESCRIBE into a module where the document does not exist, which is the only way to see whether placement survived; replaying over the original reports Unchanged either way. mx check: 1078 errors before and after on the probe project, all pre-existing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
Audit of the doc surfaces the PR checklist names turned up gaps the code changes had left behind. `mxcli syntax` was the biggest one: sixteen per-doctype topics showed a CREATE form that now accepts a folder clause and did not mention it. Each Syntax field gained the clause where it actually goes, and the cross-cutting rules — which statement shapes take it as a property vs a keyword, what an omitted clause means, that CREATE OR MODIFY moves — live in one new `document-folder` topic, following the precedent set by `create-modifiers`: documented where the question comes up rather than repeated across all 27. `folder` is aliased to it, since that is the word someone reaches for. TestExamplesParse earned its keep here, rejecting two examples in that topic that did not parse. Also: seven docs-site reference pages (synopsis + a FOLDER parameter entry linking to MOVE), the two quick references, and the three skills that describe authoring these documents. No new lexer tokens, so LSP completions regenerate unchanged from MDLLexer.g4 at build time, and no new LSP capability, so the VS Code extension is unaffected. MOVE has no Cobra subcommand — it is MDL only — so there is no CLI help to update beyond the syntax registry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
`mxcli test tests/ -p app.mpr --require-assertions` — no --local, the command in the report — accepted the flag and exited 0 on a suite where every test asserted nothing. `--help` listed it, it parsed, and the summary even printed "1 test(s) asserted nothing beyond \"did not throw\"", so the run read as having noticed and chosen to pass. The flag was consulted in exactly one of the two result-assembly loops. `vacuousResult` had a single caller, in the endpoint runner (`--local`, `--attach`); the after-startup runner (Docker default, and `--local --legacy-runner`) assembles its results in `ParseLogResults(logReader, suite)`, whose signature had no parameter the flag could arrive through — it could not honour it even in principle. Give the pre-run verdicts — the ones settled by the parsed test case rather than by the run — one function, `preRunResult`, and route both loops through it. `TestPreRunVerdictsHaveOneCallSite` pins that it is their only caller, so a third verdict or a third runner cannot reintroduce the asymmetry. Same lesson as the `newResult` constructor: a decision reachable from one path and not the other is the defect. Implemented rather than refused. `rejectVerifyOnLegacyRunner` refuses @verify on this runner because it genuinely cannot evaluate it — its tests run during boot, so there is no point at which to query the app. `AssertionCount()` is a static property of the parsed test case, known before any runner starts, so refusing here would have denied a capability the runner has. Controls: with the flag re-severed on the log path the new test fails with the reported symptom ("status = PASS, want ERROR"), and with the endpoint loop restored to calling the two helpers directly the call-site guard fails naming both sites. The pre-existing `TestRequireAssertionsMakesVacuousTestsErrors` is why this survived — it calls `vacuousResult` directly, proving the helper works and saying nothing about whether any runner calls it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
…PAGE describing the page `SHOW ACCESS ON ENTITY Mod.Entity` was a parse error while the MICROFLOW and PAGE spellings parsed, though the CLAUDE.md that `mxcli init` writes into every project documents all three: $ mxcli -p app.mpr -c "SHOW ACCESS ON ENTITY eShop.CatalogBrand" Parse error: line 1:60 extraneous input '.' expecting the start of a statement There was no `ACCESS ON ENTITY` grammar alternative, and ENTITY is in the `keyword` rule, so `showOrList ACCESS ON qualifiedName` matched the word ENTITY as the whole name; the statement ended there and the real name was extraneous input. The command itself always existed — the bare `SHOW ACCESS ON Mod.Entity` is the same AST node and the same handler, so only the explicit spelling was unreachable. Found by the test sweep for that fix, and fixed with it: `SHOW ACCESS ON PAGE` parsed but answered the wrong question. `ExitShowStatement` is a long if/else chain, and the singular `SHOW PAGE` → DESCRIBE PAGE alias branch matches on `ctx.PAGE()` alone ~200 lines earlier, so the command printed the page's entire widget tree instead of its allowed module roles. That is why the issue reports the PAGE variant as working. The ENTITY branch would have swallowed the new alternative the same way, so both are now guarded with `ctx.ACCESS() == nil`. Controls, each reverted separately against the new tests: - no grammar alternative → the original extraneous-input parse error - no ENTITY visitor guard → ObjectType = ENTITY (the definition) - no PAGE visitor guard → *ast.DescribeStmt (the widget tree) Verified end-to-end on a real 10.24 project: entity access rules and page allowed roles are reported, while `show entity` and `show page` still describe. `TestEveryShowAlternativeProducesAStatement` cannot catch this class — a statement is produced, just the wrong one — so the new tests assert the ObjectType rather than mere presence. Fixes mendixlabs#925
… syntax Investigation of mendixlabs#913 ("MDL decision (split) syntax inconsistent"), reproduced against mxbuild 11.13.0. The reporter is right that the two multi-way splits are inconsistent, but the investigation found a defect the issue does not name and that matters more than the spelling: `else` on a `split type` is the `(empty)` flow — it runs when the object is null, not when no `case` matched. Proven with mxbuild: `case Dog` + `else` still fails CE0090 demanding flows for `Cat` and the base `Animal`; covering every type while keeping the `else` gives 0 errors, so `else` contributes nothing to type coverage. Also documented: DESCRIBE indents the two splits inconsistently and neither matches if/else (enum split puts the branch body at the same column as `when`; the type split puts `case` flush with `split type`), and no test pins either, which is why it shipped. The issue's proposed remedy — a Java-shaped `switch` — is rejected in the proposal: ADR-0003 commits MDL to being SQL-shaped, and MDL already has a second `case … when … then … end` in caseExpression, so `switch` would make three spellings rather than one. The proposal instead moves the type split onto the enum split's `when … then` / `when (empty) then`, which satisfies the reporter's consistency complaint, names the empty branch honestly, and keeps the old spelling parsing. Adds a fix-issue symptom row for the `else` trap, which persists regardless of whether the proposal is adopted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
A type split now takes `when <Entity> then` branches and a `when (empty) then`
empty branch, so the two multi-way splits are one statement with two subjects:
case $Status split type $Animal
when Open, Pending then when Zoo.Dog then
... ...
when (empty) then when (empty) then
... ...
end case; end split;
Fixes three defects from mendixlabs#913.
`else` was the worst of them. It reads as a default branch and is not one: it
maps to Mendix's `(empty)` outgoing flow, taken when the object is null.
Measured on mxbuild 11.13.0, a split with one `case` and an `else` still fails
CE0090 demanding a flow for every other subtype AND for the base entity; cover
every type while keeping the `else` and it is 0 errors. The branch is now
spelled for what it does, and MDL065's message states the semantics and the
CE0090 consequence rather than reading as a rename.
`case` also meant two things — subject introducer in `case $x when V then` and
in caseExpression, branch introducer here. It now introduces a subject
everywhere.
Branch bodies used to render at the same column as their branch keyword, so a
nested `if`'s `else` landed exactly where a reader expects a case branch — in
output where `else` on a `case` is an MDL008 error. Both splits are fixed, in
both emitters: cmd_diff_mdl.go had the identical bug and was found by grepping
for the pattern rather than by the report.
The legacy `case`/`else` spelling still parses, indefinitely — scripts in the
wild use it and both spellings build the identical flow. MDL065 warns. It stays
a warning because exec's pre-flight gate halts on errors and would otherwise
refuse every existing script; TestMDL065_DoesNotBlockExecution pins that.
Verified end to end: two projects built from the two spellings describe
identically and both pass mx check with 0 errors; the three pre-existing
fixtures using the legacy spelling still build clean and now warn once each;
DESCRIBE output re-executes with no warnings. The indentation test was run
against reverted emitters and fails with the reported symptom before passing
after. The three DESCRIBE tests pinning the old spelling failed on the change
and were updated deliberately, not loosened.
Also documents the type split in docs-site and `mxcli syntax`, where it was
absent entirely, and corrects the two proposals whose examples contradicted
shipped behaviour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdvGwVewkdgNQGDCxpQPZ
… needs
`mx create-module-package` aborted on any module mxcli created:
Exception occurred: Unable to cast object of type
'Newtonsoft.Json.Linq.JValue' to type 'Newtonsoft.Json.Linq.JObject'
at MprProperty.Init → MprUnit.get_Contents → MprDocumentHasher.Write
while `mx check` reported 0 errors. Reported against an Accordion; measured to
have nothing to do with pluggable widgets — `create module M; create page M.P {
DYNAMICTEXT }` reproduces it, and so does a binary built from origin/main.
`Forms$Page.AllowedModuleRoles` was written with typed-array marker 3. The
marker tells Mendix's reader what the entries are, and the entries are
qualified-name strings, so a non-empty list under marker 3 is a JValue where
the reader wants a JObject. An empty list never crashed — which is why it took
a page carrying a role, and `create module` auto-grants one. The legacy engine
has always written 1; only the modelsdk (default) engine had this.
Measured on mxbuild 11.13 at security Off, Prototype and Production: marker 1
is 0 errors and exports; marker 3 is 0 errors and crashes. Studio Pro writes 1
on all 16 pages of a blank app, empty lists included. The comment on
NewByNameRefListV3 asserting that marker 1 raises CE0557 does not reproduce at
any of the three levels, and is corrected rather than left to mislead again.
Two keys mxcli omitted are added at the same time, both found by handing an
mxcli-authored page to `mx update-widgets` and diffing the unit it wrote back:
- Forms$Appearance always carries a DesignProperties list, emitted as the
empty marker [3]. The codec omits an empty, never-appended PartList, so
mxcli's widgets had no such key — which is also why ALTER STYLING's design
property was a silent no-op (next commit).
- CustomWidgets$CustomWidget always carries LabelTemplate, null when unset.
The repair is not retroactive: idempotent writes leave an unchanged page alone,
so an existing project keeps the old marker until each page is rewritten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
…arker
`ALTER STYLING ON PAGE p WIDGET w SET '<design property>' = '<value>'` printed
"Updated styling on widget" and wrote nothing, on any widget mxcli had created.
The same statement works on a Studio Pro-authored widget, and the inline
`DesignProperties: [...]` form works everywhere.
Two defects composing into a silent no-op. `bsonnav.DSet` takes a bson.D by
value, so it can only overwrite an existing key — it returns false and does
nothing when the key is absent, and DSetArray ignored that result. And the key
was absent: until the previous commit, mxcli's Forms$Appearance carried no
DesignProperties array at all.
Fixing the append alone would have been worse than the bug. A Mendix array's
first entry is its typed-array marker, and DSetArray only preserved a marker it
could see. Measured on mxbuild 11.13, a DesignProperties array written bare
makes the project fail to LOAD ("Type OptionDesignPropertyValue does not
contain a constructor with a parameter of type Appearance") and
create-module-package die with "Unknown export error"; a doubled marker throws
StorageLoadException. So:
- DSetArray returns bool and REFUSES a markerless non-empty write rather than
producing a document Mendix cannot open.
- DSetArrayIn is the form that can create the property: it takes the parent,
so the grown child can be written back, and the caller supplies the marker.
- The design-property mutators go through it with marker 3 (measured on every
page of a blank 11.13 app) and return an error instead of reporting success.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
…rmless ones
A pluggable widget's editorConfig.js hides properties under some configurations
of the same widget, and mxbuild evaluates that logic: a hidden property must
hold its DEFAULT value, or it is CE0463 "the definition of this widget has
changed" — which fails `mx check` and makes create-module-package refuse the
module.
On the reported Accordion, mxcli's MDL-WIDGET10 fired four times, on four
properties the script had left at their defaults, and said nothing about the
two that break the build. Measured on mxbuild 11.13, one property per run:
collapsible: false + InitialCollapsedState: 'expanded' → CE0463
collapsible: false + InitiallyCollapsed: 'false' → CE0463
collapsible: false + expandBehavior: multipleExpanded → CE0463
collapsible: false + animate: false → CE0463
collapsible: TRUE + each of those values → 0 errors
collapsible: false + every value at its default → 0 errors
Three gaps, all in the editorConfig extractor:
- Nested hides were skipped by design (mendixlabs#574 Phase 1). hideNestedPropertiesIn
and the 5-argument hidePropertyIn are now lifted as rules carrying the
object list they belong to, and evaluated per item.
- A nested condition can read the ITEM instead of the widget — the Accordion
hides initiallyCollapsed unless the GROUP's own initialCollapsedState is
"dynamic". Conditions carry a scope, and each is looked up in its own
values; reading an item condition against the widget finds an absent key
and silently reports nothing.
- A comparison guard reached through a `||` connector or a ternary's ELSE
branch was recorded with the wrong polarity (eq where the code means ne).
This also flipped several pre-existing top-level rules that were inverted,
e.g. Combobox attributeBoolean.
Two smaller ones fall out. `X && Y || hide(…)` soundly implies "hide when Y
falsy" whatever X is, so the Accordion's compound guard is liftable; `X && Y &&
hide(…)` is not, and stays unlifted. And the shipped compilation is
`forEach((function(n,o){…}))` — a double paren the callback-parameter regex read
as the parameter name, scoping every nested condition to the widget.
The diagnostic now splits on the VALUE: a hidden property set to its default is
merely redundant (warning, as before), a non-default one is an error naming the
default. mxcli's verdict matches mxbuild on all nine measured configurations.
Widget defs are cached per project, so WidgetDefGeneratorVersion is bumped to
force a refresh; note the refresh happens during exec, so the run that
regenerates them still validates against the old set.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
…on script Three rows appended to the fix-issue symptom table — the hidden-property CE0463, the silent ALTER STYLING design property, and the create-module-package hasher crash — each with the measurement that settled it and the trap to avoid next time: minimise before believing the reporter's attribution, check a comment's premise before honouring it, and never fix an append without fixing the marker. The bug-test script executes cleanly and exports cleanly; the two failing widget shapes are kept as comments so the trap stays documented without breaking the project-less `make check-mdl` gate, which cannot see a project's widget defs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CqQLyxnppZGfqPYbUHvSWG
fix(placement): FOLDER and MOVE work for every document type (mendixlabs#932)
fix(show): make SHOW ACCESS ON ENTITY parse, and stop SHOW ACCESS ON PAGE describing the page
feat(mdl): unify the type split onto the enum split's branch syntax (mendixlabs#913)
fix: three defects behind mendixlabs#931 — hidden widget properties, uncreatable arrays, and the marker that crashed the exporter
fix(test): --require-assertions was a silent no-op on the default runner
…ixlabs#941) DESCRIBE PAGE emitted MDL that would not re-execute for any non-database datasource. Three faces, one cause. A chart series bound to a microflow came back as `DataSource: database from Module.TheMicroflow`, and re-executing it reported "entity not found". A pluggable list whose source carried no entity came back as `DataSource: database from ` with an empty slot, which the parser reads as an entity called "from". And a gallery bound over an association lost its datasource entirely — silently — while a listview bound the same way kept it. The datasource switch had been copied per widget family: five copies on the read side and six on the write side, drifted apart. The pluggable read copy, which every Gallery, DataGrid2 and chart goes through, knew microflow and nanoflow sources but not associations, listen targets or context sources, so it dropped them; its CustomWidgetXPathSource case returned an empty database source where its own near-twin guarded against exactly that; and the object-list item emitter had no type switch at all. So the fix is one reader and one renderer rather than three patches. A datasource means the same thing wherever it sits, and the copies were the defect — patching only the pluggable switch would have left five to drift again. The $Type set comes from generated/metamodel's DataSource interface; sorting is read from whichever shape is stored (SortBar.SortItems or Sort.Paths) so the reader stays container-agnostic. Unknown type and known-type-with-empty-payload are kept apart. The first is mxcli's gap and is now reported as a comment naming the stored $Type — never dropped, never guessed into a `database from` that cannot re-execute. The second is the model's own incompleteness and still yields nothing, which an existing test deliberately pinned; collapsing the two broke it. Verified by describing every page in a 21-page project before and after: the four intended changes and nothing else. That diff also surfaced two WHERE and SORT clauses the combobox emitter had been dropping, now round-tripped. 10 of 21 pages still fail describe→check, identically before and after — unquoted XPath containing '[', duplicate generated widget names, context errors — all separate round-trip gaps. mx check: 950 errors before and after, all pre-existing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
…ixlabs#941's control Copying a project for an `mx check` baseline with `cp rp.mpr mprcontents/` is not a copy of the project. The theme lives in `theme/`, so the copy reports 924 CE6083 "Design property is not supported by your theme" plus CE0535 for the missing widgets/ — a baseline of ~950 errors where the real project has 0. Before and after still compare equal, so the control looks like it passed. It had almost no power: a genuine new error would be one line in a thousand, and every theme-dependent check never ran. Re-run against a complete copy, the project is 0 errors before and 0 after — which is a control that can actually detect something. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
Restoring the WHERE clause in mendixlabs#941 collided with two older gaps in the same six lines, and the result was a regression: stock Administration.Account_New described to MDL that parsed before the change and did not parse after it. Every project from the Mendix template carries that page. The constraint was emitted raw, so a stored XPath carrying an inner predicate — System.grantableRoles[reversed()]/… — ended the MDL expression parse at the '['. The grammar accepts either a bare expression or a bracketed xpathConstraint; only the second is safe for arbitrary XPath. So the emitter now always uses the bracketed production instead of trying to decide: the bare form reads better but parses only for simple comparisons, and whether a given XPath also happens to be a valid MDL expression is not something the emitter can know. Splitting goes through visitor.SplitXPathPredicateGroups — the quote- and nesting-aware helper written for mendixlabs#772 — rather than a second copy. The old code took the outer brackets off by testing the first and last byte, which turns [a][b] into the mangled a][b: precisely the bug that helper exists to prevent. The guard was also strings.Trim(s, ""), whose empty cutset trims nothing, so a whitespace-only constraint emitted a bare `where`. Verified as a set rather than a count, which is what the original claim got wrong: describing every page in the project and parsing the result gives 8 pass on the parent, 7 with the PR as submitted, 8 with this fix, and joining the pass-sets names Account_New as the only page that regressed and shows none left. Control: revert the bracketing and it stops parsing again. Reported in mxcli-formula1 §57.1 and §57.3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
A microflow used as a widget datasource needs an argument for every parameter, exactly as a call action does — mendixlabs#835 fixed the write side. DESCRIBE never read them back, so `microflow Mod.DS(Race: $Race)` described as `microflow Mod.DS` and replaying that description failed the build with CE1571 "No argument has been selected for parameter", the very error mendixlabs#835 was about. `check --references` calls the argument-less form valid, so nothing caught it before mxbuild. Plain dataviews lose them too, not just pluggable widgets. Older than mendixlabs#941 — the previous code printed the whole line wrong, so the loss hid behind a larger bug. Fixed here because the renderer is now one function, which is the right place to do it once rather than six times. The shape came from dumping a real unit rather than from the writers, which disagree in spelling: bindings live in <Flow>Settings.ParameterMappings, each {Parameter, Expression} behind typed-array marker 3, with Parameter qualified by the flow ("Mod.DS_Filtered.Term") while MDL names the parameter alone. The prefix is stripped by matching the flow's own qualified name rather than by cutting at the last dot, so a document storing the name bare does not lose its first segment. A parameterless flow still renders without parentheses. Verified by replaying a description: CE1571 goes 4 to 0 on a two-widget page, and the remaining two errors are the probe page's own pre-existing CE0117, one per copy. The whole-project page sweep is unchanged at 8 passing with no regressions against the parent. Control: drop the reader and the arguments vanish from the output again. Reported in mxcli-formula1 §57.2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
An entity used only inside a List View specialization template reported `0 pages, 0 widgets` from SHOW REFERENCES. Anything using reference counts to decide "unused, safe to delete" would delete a document in active use. `Templates` was not a recognised widget-container key. extractWidgetsRecursive knew Widgets, Rows/Columns, FooterWidgets, TabPages, pluggable Object.Properties[].Value.Widgets and NavigationList Items[].Widgets — not Templates — so nothing inside a template reached the widgets table, and the refs projection is built from that table. Measured on a list view holding two templates, two data containers and two buttons: 1 row indexed, 5 expected. The same omission had a second, opposite effect the report did not find. Templates was missing from widgetChildKeys too, so the list view's own ref scan descended into its templates, collected each one's Entity, and returned the lexicographically smallest candidate — a specialization sorting before the list view's real datasource silently replaced it. Measured: a list view over MyModule.BaseItem with a template for MyModule.AAA_First recorded its datasource entity as AAA_First. The wrong entity, decided by alphabetical accident. Walk each template as a container in its own right, so the row it produces carries the specialization it renders and its children are indexed like any other nested widgets; and add the key to widgetChildKeys so the parent stops absorbing them. Both halves are needed: recursion alone leaves the displacement in place, the key alone hides the contents without indexing them. The two are sides of one contract, and the two omissions partly masked each other — a stray ref still leaked through — which is why this read as intermittent. Items stays asymmetric on purpose: a nav item's own Form/Entity belongs to the NavigationList because items are not emitted as rows, and the walk already stops at the item's Widgets. Controls: with the recursion removed the first test fails naming all four missing widgets; with the key removed the second fails with EntityRef = MyModule.AAA_First. Each half fails independently on its own symptom. No MDL repro script: MDL has no surface for authoring a specialization template (which is the reporter's separate point), so the unit tests are the repro. Scoped to the catalog walkers. DESCRIBE PAGE still omits templates, and SEARCH inherits that because the source table is built from describe output — those need an entity field on the model type and an MDL surface first. Refs mendixlabs#940 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
… by one already present CI caught this: `use building block Atlas_Web_Content.List_Cards (datasource: …)` began failing with "the block has no datasource widget to rebind", on both engines, in 15c-fragment-bindings-examples.mdl. Bisected to e50ddac — the mendixlabs#941 unification — and reproduced against its parent, which passes. The rebind expands a block by re-rendering it to MDL through the describe emitter and re-parsing that text, then looked for a widget already carrying a DataSource property. A reusable block is a template: its datasource is unbound. That used to render as the malformed `DataSource: database from ,` — precisely the output mendixlabs#941 fixed — and the re-parse turned it into a DataSource property, so the rebind found its target. Emitting nothing for an empty datasource, which is correct, left it with nothing to match. So the target is matched by widget type — what can carry a datasource, not what does. The sibling ActionOverride branch already did exactly this, and its comment says why: Atlas blocks "ship placeholder buttons with no action". Same fact about the same blocks; this branch had not learned it. The wider lesson, now in fix-issue.md: a consumer that re-parses another component's rendered output silently depends on that output's bugs, so fixing a renderer can break code that never reads the model. No emitter unit test can see it — only the integration suite did. mx check on the rebound page: 0 errors. Control: restore the property-based predicate and the stock-block rebind fails again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dp1syGhH8yr7Hve2wwjzqg
A List View over a generalization can render a different body per
specialization. MDL had no surface for authoring one, and — worse — no read path
either: DESCRIBE PAGE dropped the templates silently, and because DESCRIBE emits
`create or modify page`, re-executing its output rebuilt the page without them.
Measured on ako/TestApp's Pages.Vehicle_Overview (4 templates, Studio Pro
authored) with the previous binary: describe -> exec left 0 templates. `mx check`
reported 0 errors before and after. The document stays valid, it just holds less,
which is why nothing downstream flagged it.
Syntax — a template is identified by the entity it renders, not by a name:
listview vehicleListView (DataSource: database from Pages.Vehicle) {
dynamictext defaultVehicle (...) -- default body
template for Pages.Bus { dynamictext busLabel (...) }
template for Pages.Truck { dynamictext truckLabel (...) }
}
Four things worth knowing, each of which bites if missed:
1. modelsdk/gen binds the entity under the SDK name "Specialization"; every
Studio Pro document stores "Entity". generated/metamodel agrees with the
documents. Patched as a STORAGE-NAME OVERRIDE on both the encode key
(initListViewTemplate) and the decode key (InitFromRaw).
2. Order is authored, not derived. TestApp's is Bus, Truck, Car, SUV — neither
alphabetical nor domain-model order — so a DESCRIBE that sorted them would
not round-trip to the same document.
3. Inside a template the context object is the specialization, so an attribute
only that specialization has resolves there. Both the builder and DESCRIBE
thread that context.
4. FOR is in the parser's `keyword` rule, so the new widgetV3 alternative comes
FIRST or it collides with Gallery's pre-existing `template <name>` content
slot. That is a different construct and keeps working; a Gallery slot named
`for` now needs quoting, the same escape hatch reserved names already use.
Refused rather than written, each naming the problem: an entity that is not the
list view's entity or a specialization of it (it could never match), a second
template for the same entity, and a nested template.
Verified end to end on a real project, both engines: authoring the example
writes three templates with the right entities and `mx check` reports 0 errors;
describe -> exec -> describe on TestApp's Studio Pro authored page is byte
identical with all four templates and their order intact. SHOW REFERENCES TO a
specialization now finds the page, and SEARCH finds content that lives only
inside a template — the catalog's source table is built from DESCRIBE output, so
that half followed from the read path.
Controls: with DESCRIBE's Templates read removed the describe test fails naming
all four missing blocks; with the legacy writer's Entity key removed the writer
test reports keys [$ID $Type Widgets]; with the gen override reverted the
storage-name test reports the property bound as "Specialization".
Refs mendixlabs#940
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
fix(describe): read and render a widget datasource in one place (mendixlabs#941)
Adding a template reuses INSERT INTO with the same `template for` block CREATE
PAGE uses, so a template has one spelling everywhere. Removing one needs its own
form, because a template has no name to put in a widget ref:
alter page Pages.Vehicle_Overview {
insert into vehicleListView {
template for Pages.Motorcycle { dynamictext mcLabel (...) }
};
drop template for Pages.SUV in vehicleListView
};
Naming the list view in the drop is required, not optional: one page can hold
two list views with a template for the same entity.
INSERT is not free despite reusing the existing syntax. INSERT INTO appends to a
container's Widgets, and a list view's Widgets is its DEFAULT BODY — a
Forms$ListViewTemplate is not a widget, so routing one through InsertWidget
appends a non-widget to the widget list and produces a page Studio Pro cannot
open. Templates therefore take a dedicated mutator path, exactly as DataGrid2
columns already do for the same reason. The control makes the failure concrete:
with the routing disabled the list view's default body gains two widgets.
Two new PageMutator methods (InsertListViewTemplates, DropListViewTemplate),
implemented in pagemutator over raw BSON, stubbed in the mock, and refused with a
reason by the MCP backend. Both check the target is a list view, so a gallery's
`template <name>` — a named content slot, a different construct — is not
reachable through them.
Most template edits need none of this. Widgets inside a template are ordinary
named widgets, so `set Content = '…' on busLabel` and `insert after busLabel { … }`
already work and land in the right template. Replacing a whole template is a drop
plus an insert in one block, since operations apply in order.
Refused, each naming the problem: insert before/after a template (they are not
siblings of the body), templates mixed with widgets in one insert (they go to
different arrays), a template for an entity outside the list view's hierarchy, a
duplicate, and dropping a template that is not there — that last one names the
templates that ARE there, because dropping nothing and reporting success is how a
typo in a specialization name becomes a silent no-op.
Verified on ako/TestApp's Studio Pro authored page: inserting Motorcycle and
dropping SUV leaves Bus, Truck, Car, Motorcycle in order with `mx check` at 0
errors, and all five refusals were exercised against the real project.
Controls: with the insert routing disabled the templates land in the default
body; with the drop's not-found check removed a bogus drop reports success; with
the append targeting Widgets the default body grows instead of the Templates
array.
Refs mendixlabs#940
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s476QkXr9CFMvKspVzcvu
fix(catalog): index List View specialization template contents
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Syncs
ako/mxcli:mainintomendixlabs/mxcli:main.34 commits (20 non-merge, 14 merges), 191 files changed, +8526 −803.
Clean fast-forward —
git merge-base --is-ancestorholds, no rebase or squashdivergence.
Covers PRs #197–#203. Several entries below were found by triaging real
applications' findings against mxbuild 11.13 and against Studio Pro authored
projects, so they are corrections to things mxcli reported as working.
Model content that was silently lost
Each of these reported success and left a project
mx checkcalled clean. Thatis the whole problem: the document stays valid, it just holds less.
view over a generalization can render a body per specialization;
DESCRIBE PAGEnever read theTemplatesarray, and since DESCRIBE emitscreate or modify page, re-executing its output rebuilt the page without them. Measuredon a Studio Pro authored page: 4 templates before, 0 after,
mx checkclean both times.
SHOW REFERENCESalso reported 0 pages, 0 widgets for anentity used only inside a template, so tooling deciding "unused, safe to
delete" would have deleted a document in active use.
FOLDERclause at all — mappings, queues, scheduled events, regularexpressions, workflows, menus, image collections, Java/JavaScript actions,
database connections, data transformers, the AI-agent documents — so they could
only be created at the module root, and describing one Studio Pro had filed
away and replaying it recreated it unfiled.
ALTER STYLING … SET '<design property>'wrote nothing on any widget mxclihad created, while printing "Updated styling on widget". Two defects composing:
a
bsonnav.DSetthat could not create an absent array, and an array writtenwithout its Mendix list marker.
--require-assertionswas a silent no-op on the default runner. It wasconsulted in one of the two result-assembly loops, so
mxcli test tests/ -p app.mpr --require-assertions— without--local— exited 0 on a suite thatasserted nothing.
Projects mxcli produced that Mendix tooling then refused
mx create-module-packageaborted on any module mxcli created, withUnable to cast object of type 'JValue' to type 'JObject'insideMprDocumentHasher. Fixed by writing the page keys Studio Pro writes and themarker its reader needs.
editorConfig.jshides properties under some configurations and mxbuild evaluates that logic: a
hidden property must hold its default or the build fails. Now only the property
that actually fails the build is flagged, not every harmless one.
New authoring surface
template for Module.Entity { … }inside a list view, withINSERT INTOandDROP TEMPLATE FOR … IN …onALTER PAGE. Templates keep their source order(Mendix matches in that order, so it is authored, not derived), and inside one
the context object is the specialization, so an attribute only it has resolves.
Refused rather than written: an entity outside the list view's hierarchy, a
duplicate, and a nested template.
FOLDERclause onCREATEfor every document type, applied onCREATE OR MODIFYtoo, and emitted by DESCRIBE.MOVEaccepts every top-level document type. It listed nine; the othertwenty-two were never unimplemented, merely unlisted — the backend methods sat
fully wired and called from nowhere.
multi-way splits are one statement with two subjects.
DESCRIBE correctness
database fromfor datasources that were not a database-over-entity, sometimeswith the entity slot blank, sometimes with a microflow name in it — which made
its own output fail
mxcli check --references.that happens to already be present.
Commands that did not do what they said
SHOW ACCESS ON ENTITYdid not parse, andSHOW ACCESS ON PAGEdescribedthe page instead of its access.
Docs
mx checkbaseline trap: comparing against aproject that was already dirty weakened a control, so a fix looked verified
when it was not.
documented everywhere it is looked up