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 @@ -172,6 +172,7 @@ to the symptom table below, so the next similar issue costs fewer reads.
| A microflow expression calls a function that doesn't exist (e.g. `randomInt(9)` — in some docs but not a Mendix built-in): parses, passes `mxcli check`, then fails the build with CE0117 "Error(s) in expression". Also: a Decimal-returning function (`random()`, `secondsBetween`, the duration `*Between` family) assigned to an Integer/Long variable fails CE0117, but `mxcli check` only caught bare `div` | (1) The func checker only validated arity for *known* functions; an unknown call name was ignored. (2) `checkNumericAssignment` used `SourceIsArithmeticDecimal`, which fires only on arithmetic roots, so a Decimal *function* result slipped through. Also the `*Between` duration funcs were mistyped Integer in `funcTable` | `mdl/exprcheck/unknown_funcs.go` (`UnknownFunctionCalls`, `SourceRejectedForIntegerTarget`, `nearestFunc`) + `mdl/exprcheck/func_checker.go` (`funcTable` between-date return kinds) + `mdl/executor/validate_microflow.go` (`checkExprFunctions` → MDL044; `checkNumericAssignment` now uses `SourceRejectedForIntegerTarget`) | Walk each expression for `CallExpr` names not in `funcTable` (which lists *every* built-in — a bare `name(...)` is always a built-in call) → **MDL044** with a Levenshtein/prefix "did you mean" hint. Extend the Decimal-into-Integer check to Decimal-returning non-rounding functions → **MDL041**. Correct `secondsBetween`/`minutesBetween`/`hoursBetween`/`daysBetween`/`weeksBetween` to Decimal (calendar variants stay Integer, millisecondsBetween stays Long). When adding a new Mendix built-in, add it to `funcTable` or MDL044 will false-positive. Bug-tests `f1-unknown-expression-function.fail.mdl`, `f2-decimal-func-into-integer.fail.mdl`. Findings #1, #2 |
| `index name on (cols)` inside `create entity` (or `alter entity add index name on (cols)`) fails with `extraneous input 'on' expecting '('` — the SQL-like form docs/users expect. The bare `index name (cols)` worked | `indexDefinition` had no `ON` token: `IDENTIFIER? LPAREN indexAttributeList RPAREN` | `mdl/grammar/domains/MDLDomainModel.g4` (`indexDefinition`) | Make `ON` optional: `IDENTIFIER? ON? LPAREN indexAttributeList RPAREN`, regen grammar. `buildIndex` reads columns from `IndexAttributeList` only, so ON can't be mistaken for a column and no visitor change is needed. Covers both CREATE (entityOption) and ALTER ADD INDEX (shared rule). Bug-test `mdl-examples/bug-tests/f4-entity-index-on.mdl`. Findings #4 |
| `retrieve … where [Seq = $Game/MoveSeq + 1]` fails with a bare `mismatched input '+' expecting ']'` — no hint that Mendix XPath can't compute values (this is a Mendix limitation, not an mxcli bug) | Mendix XPath constraints take a literal/token/variable/path on the value side, never an arithmetic expression; the parse error named the token but not the cause | `mdl/visitor/visitor.go` (`enhanceErrorMessage`, `looksLikeXPathArithmetic`/`xpathArithmeticRe`) | Do NOT add grammar support (mxbuild would still reject the XPath). Add an error hint keyed on `mismatched input '<+|*|div|mod>' expecting ']'` (`expecting ']'` only occurs inside a `[…]` constraint) explaining the limitation and the workaround: compute into a variable first, then compare. Also documented in `xpath-constraints.md`. Bug-test `mdl-examples/bug-tests/f8-xpath-arithmetic.fail.mdl`. Findings #8 |
| Design properties are written free-form: a `ColorPicker`/`ToggleButtonGroup` value serializes as a plain option (wrong `$Type` for Studio Pro's Appearance tab), and a typo'd key/value (they're case-sensitive) passes `mxcli check`. Also `show design properties <widget>` reports "No design properties found for widget type container" for a valid widget | Root bug: `resolveDesignPropsKey` upper-cased the MDL keyword but the lookup map is **lowercase-keyed**, so `container`→`DivContainer` never resolved — leaving `resolveDesignPropertyValueType` dead code and the theme registry unused on the write/validate paths | `mdl/executor/theme_reader.go` (`resolveDesignPropsKey` case fix) + `mdl/executor/cmd_pages_builder_v3.go` (`astDesignPropToValue` takes theme props) + `mdl/executor/validate_design_properties.go` (new, MDL-WIDGET11/12) wired from `cmd/mxcli/cmd_check.go` + `cmd/mxcli/lsp_diagnostics.go` (cached `themeRegistry`) | Fix `resolveDesignPropsKey` to lower-case the lookup. On write, resolve each flat value's type from the registry (`ColorPicker`/`ToggleButtonGroup`→custom). On check (`-p` only, when themesource defines properties), walk page/snippet/alter-page widget trees and warn: **MDL-WIDGET11** unknown key (case-sensitivity hint / valid-key list), **MDL-WIDGET12** invalid value (lists allowed values). Warnings, not errors — a newer theme may add keys/values (forward-compat, per `page-styling-support.md:402`). Skip compound (registry doesn't model sub-props) and widgets with no type-specific metadata (pluggable). Bug-test `mdl-examples/bug-tests/typed-design-properties.mdl` |

---

Expand Down
17 changes: 16 additions & 1 deletion .claude/skills/mendix/theme-styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,25 @@ Sub-property keys are case-sensitive, same as flat keys.

`alter styling` cannot find widgets in pages created by the MDL page builder because `walkPageWidgets` traverses `LayoutCall.Arguments` but the page parser doesn't fully reconstruct the widget tree when re-reading builder-created pages. These commands work on pages originally created in Studio Pro.

## Validation with `mxcli check -p`

When a project is supplied (`mxcli check page.mdl -p app.mpr`), design properties are
validated against the project's theme registry (`themesource/*/web/design-properties.json`):

- **MDL-WIDGET11** — a design-property key not defined for that widget type (with a
case-sensitivity hint, or the list of valid keys).
- **MDL-WIDGET12** — an option value that isn't allowed; the message **lists the
allowed values** (case-sensitive), which is the fastest way to fix a casing typo.

Both are warnings (a newer theme may add keys/values), so they inform without blocking.
`show design properties <widget>` lists the same allowed keys/values up front. On the
write side, the value's BSON type is taken from the registry (a `ColorPicker` /
`ToggleButtonGroup` property serializes as a custom value, not a plain option).

## Checklist

- [ ] Never apply `style` directly to DYNAMICTEXT — wrap in a CONTAINER
- [ ] Design property keys are case-sensitive — match `design-properties.json` exactly
- [ ] Design property keys are case-sensitive — match `design-properties.json` exactly (`check -p` flags mismatches as MDL-WIDGET11/12)
- [ ] Compound/nested design properties (e.g. grouped Spacing) use a nested list: `'Spacing': ['margin-top': 'Large']`
- [ ] For CSS changes, run `docker build` then `docker reload --css`
- [ ] Use `describe styling` to verify changes after modification
Expand Down
5 changes: 5 additions & 0 deletions cmd/mxcli/cmd_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ Examples:
// Check for intra-script duplicate definitions (CREATE X … CREATE X without DROP)
violations = append(violations, executor.CheckScriptDuplicates(prog)...)

// Validate design properties against the project's theme registry
// (themesource design-properties.json) — flags unknown keys and invalid
// option values, listing the allowed values. Only runs with --project.
violations = append(violations, executor.ValidateDesignProperties(prog, projectPath)...)

// Validate pluggable widget properties against widget definitions —
// catches typos in property keys before MxBuild does. Uses built-in
// definitions alone when no project is given; with --project, also
Expand Down
13 changes: 13 additions & 0 deletions cmd/mxcli/lsp.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ type mdlServer struct {
widgetCompletionsOnce sync.Once
widgetCompletionItems []protocol.CompletionItem
widgetRegistry *executor.WidgetRegistry // populated by the same Once

// Design-property (theme) registry cache (lazily populated once per session,
// keeping design-property file I/O out of the per-keystroke path).
themeRegistryOnce sync.Once
themeRegistry *executor.ThemeRegistry
}

// ensureThemeRegistry loads the project's design-property registry once per
// server lifetime. Nil when there is no project/themesource metadata.
func (s *mdlServer) ensureThemeRegistry() {
s.themeRegistryOnce.Do(func() {
s.themeRegistry = executor.LoadThemeRegistryForProject(s.mprPath)
})
}

func newMDLServer(client protocol.Client) *mdlServer {
Expand Down
4 changes: 4 additions & 0 deletions cmd/mxcli/lsp_diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ func (s *mdlServer) runSemanticValidation(text string) []protocol.Diagnostic {
// filesystem isn't walked on every keystroke. Nil-safe: when the registry
// hasn't loaded (no project context, etc.) widget validation is skipped.
s.ensureWidgetRegistry()
s.ensureThemeRegistry()

var diags []protocol.Diagnostic
for i, stmt := range prog.Statements {
Expand All @@ -287,6 +288,9 @@ func (s *mdlServer) runSemanticValidation(text string) []protocol.Diagnostic {
if s.widgetRegistry != nil {
violations = append(violations, executor.ValidateWidgetPropertiesForStatement(stmt, s.widgetRegistry)...)
}
if s.themeRegistry != nil {
violations = append(violations, executor.ValidateDesignPropertiesForStatement(stmt, s.themeRegistry)...)
}

lineNum := uint32(0)
if i < len(stmtLines) {
Expand Down
20 changes: 20 additions & 0 deletions mdl-examples/bug-tests/typed-design-properties.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- Typed design properties: with `mxcli check -p app.mpr`, design-property keys
-- and values are validated against the project's theme registry
-- (themesource/*/web/design-properties.json):
-- * MDL-WIDGET11 — unknown key for the widget (case-sensitive; keys are listed)
-- * MDL-WIDGET12 — value not in the allowed set (allowed values are listed)
-- On write, a ColorPicker / ToggleButtonGroup value is serialized as a custom
-- value (correct BSON $Type), not a plain option. Simple toggles/dropdowns and
-- compound (Spacing) are unchanged. This file itself is valid MDL.
create page TypedDP.Home (
title: 'Home',
layout: Atlas_Core.Atlas_Default
) {
container hero (designproperties: [
'Background color': 'Brand Primary',
'Card style': on,
'Spacing': ['margin-bottom': 'Large']
]) {
dynamictext t1 (content: 'Welcome', rendermode: H2)
}
}
27 changes: 23 additions & 4 deletions mdl/executor/cmd_pages_builder_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -545,9 +545,17 @@ func applyWidgetAppearance(widget pages.Widget, w *ast.WidgetV3, theme *ThemeReg
// Apply design properties
astProps := w.GetDesignProperties()
if len(astProps) > 0 {
// Resolve the widget's design-property definitions from the theme registry
// (when loaded) so each value's BSON type is taken from metadata
// (ColorPicker/ToggleButtonGroup → custom) rather than guessed. Nil/empty
// when no project/themesource — the converter then falls back to option.
var themeProps []ThemeProperty
if theme != nil {
themeProps = theme.GetPropertiesForWidget(resolveDesignPropsKey(w.Type))
}
var dpValues []pages.DesignPropertyValue
for _, p := range astProps {
if dp, ok := astDesignPropToValue(p); ok {
if dp, ok := astDesignPropToValue(p, themeProps); ok {
dpValues = append(dpValues, dp)
}
}
Expand All @@ -567,11 +575,18 @@ func applyWidgetAppearance(widget pages.Widget, w *ast.WidgetV3, theme *ThemeReg
// pages.DesignPropertyValue. Compound entries (a key whose value is a nested
// list, e.g. 'Spacing': ['margin-top': 'Large', …]) recurse into sub-properties.
// Returns ok=false for an entry that should be skipped (a flat toggle set OFF).
func astDesignPropToValue(p ast.DesignPropertyEntryV3) (pages.DesignPropertyValue, bool) {
//
// themeProps are the widget's design-property definitions from the theme registry
// (nil/empty when no project/themesource). When available, a flat value's BSON
// type is taken from the property's declared Type — a ColorPicker or
// ToggleButtonGroup materializes as Forms$CustomDesignPropertyValue rather than
// the option default (findings: typed design properties). Without metadata the
// prior syntactic behaviour (on→toggle, else→option) is preserved.
func astDesignPropToValue(p ast.DesignPropertyEntryV3, themeProps []ThemeProperty) (pages.DesignPropertyValue, bool) {
if len(p.Nested) > 0 {
dp := pages.DesignPropertyValue{Key: p.Key, ValueType: "compound"}
for _, sub := range p.Nested {
if sv, ok := astDesignPropToValue(sub); ok {
if sv, ok := astDesignPropToValue(sub, themeProps); ok {
dp.Compound = append(dp.Compound, sv)
}
}
Expand All @@ -583,7 +598,11 @@ func astDesignPropToValue(p ast.DesignPropertyEntryV3) (pages.DesignPropertyValu
case "off":
return pages.DesignPropertyValue{}, false // toggle absence — skip
default:
return pages.DesignPropertyValue{Key: p.Key, ValueType: "option", Option: p.Value}, true
return pages.DesignPropertyValue{
Key: p.Key,
ValueType: resolveDesignPropertyValueType(p.Key, themeProps),
Option: p.Value,
}, true
}
}

Expand Down
10 changes: 5 additions & 5 deletions mdl/executor/theme_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,12 @@ var mdlKeywordToDesignPropsKey = map[string]string{
"footer": "Footer",
}

// resolveDesignPropsKey converts an MDL widget type keyword (e.g., "CONTAINER")
// to the design-properties.json key (e.g., "DivContainer"). Falls back to the input as-is
// for unrecognized types (e.g., pluggable widget identifiers).
// resolveDesignPropsKey converts an MDL widget type keyword (e.g., "container",
// "CONTAINER") to the design-properties.json key (e.g., "DivContainer"). The
// lookup is case-insensitive against the lowercase-keyed map. Falls back to the
// input as-is for unrecognized types (e.g., pluggable widget identifiers).
func resolveDesignPropsKey(mdlKeyword string) string {
upper := strings.ToUpper(mdlKeyword)
if key, ok := mdlKeywordToDesignPropsKey[upper]; ok {
if key, ok := mdlKeywordToDesignPropsKey[strings.ToLower(mdlKeyword)]; ok {
return key
}
return mdlKeyword
Expand Down
Loading
Loading