Skip to content

feat: pluggable widget engine with data-driven definitions#28

Open
engalar wants to merge 10 commits intomendixlabs:mainfrom
engalar:feat/custom-widget-refactor
Open

feat: pluggable widget engine with data-driven definitions#28
engalar wants to merge 10 commits intomendixlabs:mainfrom
engalar:feat/custom-widget-refactor

Conversation

@engalar
Copy link
Contributor

@engalar engalar commented Mar 25, 2026

Summary

  • Replace hardcoded pluggable widget builders (cmd_pages_builder_v3_pluggable.go, 361 lines) with a data-driven PluggableWidgetEngine that uses .def.json widget definition files
  • Add WidgetRegistry with 3-tier loading: embedded definitions, global (~/.mxcli/widgets/), and project-level (.mxcli/widgets/)
  • Add OperationRegistry with 6 built-in operations (attribute, association, primitive, selection, datasource, widgets)
  • Add mxcli widget extract CLI command to generate skeleton .def.json from .mpk files
  • Add LSP completion support for registered widget types
  • Fix Gallery CE0463 by re-extracting template and fixing BSON augmentation
  • Add DESCRIBE-side pluggable widget support with association property extraction

Test plan

  • WidgetEngine tests: mode selection, condition evaluation, mapping resolution, BSON operations
  • WidgetRegistry tests: embedded loading, case-insensitive lookup, user definitions, validation
  • DESCRIBE pluggable widget tests: association extraction regression coverage
  • CLI widget command tests
  • All existing tests pass (go test ./...)
  • go vet clean

🤖 Generated with Claude Code

engalar and others added 10 commits March 25, 2026 08:43
Define WidgetDefinition, PropertyMapping, ChildSlotMapping, WidgetMode,
and BuildContext types for data-driven widget construction. Implement
OperationRegistry with 5 built-in operations (attribute, association,
primitive, datasource, widgets) that wrap existing helper functions.
…dget engine

Create 7 .def.json definition files (combobox, gallery, datagrid, 4 filters)
that declaratively describe how MDL syntax maps to widget template properties.
Add WidgetRegistry with embedded loading, case-insensitive lookup, and
user-extension support (~/.mxcli/widgets/ and project-level overrides).
…e resolution

Implements the generic build engine that constructs CustomWidget instances
from WidgetDefinition + AST. Supports conditional mode selection (hasDataSource,
hasAttribute, hasProp:X), property source resolution (Attribute, DataSource,
Selection, CaptionAttribute, Association, generic), and child slot mapping.
…SP completion

Wire up WidgetRegistry and PluggableWidgetEngine into pageBuilder with
lazy initialization. COMBOBOX and GALLERY now route through the
declarative engine via registry lookup in buildWidgetV3() default case.
Remove 7 dead builder functions (~360 lines).

Add `mxcli widget extract --mpk` command to generate skeleton .def.json
from widget packages, and `mxcli widget list` to show registered widgets.
LSP completion now includes registered pluggable widget types.
- Remove 5 dead .def.json files (datagrid + 4 filters) that were loaded
  but never used at runtime due to hardcoded switch cases
- Change Modes from map[string]WidgetMode to []WidgetMode for
  deterministic evaluation order; add Name field to WidgetMode
- Make LoadUserDefinitions report errors for malformed JSON and missing
  required fields instead of silently ignoring them
- Add "selection" to OperationRegistry test coverage
- Add TestOpSelection unit test for Selection field updates
- Update combobox.def.json to array-based modes format
…entation

Root cause: Gallery template was from a different widget version (33 properties)
than the project's Gallery 1.12.0 (23 properties). Augmentation fixed property
count but couldn't build nested ObjectType for object-type properties (filterList,
sortList), causing CE0463 "widget definition changed" error.

Changes:
- Re-extract gallery.json template from Studio Pro reference (correct version)
- Add Gallery to extract-templates command widget list
- Add Children field to mpk.PropertyDef for nested object properties
- Parse nested <properties> in mpk walkPropertyGroup
- Build nested ObjectType in augmentation for object-type properties
Covers 4 test scenarios:
- GALLERY basic with DATABASE datasource + TEMPLATE (PASS)
- GALLERY with FILTER/TEXTFILTER (PASS)
- COMBOBOX enum mode (known CE1613 engine bug)
- COMBOBOX association mode (known CE1613 engine bug)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove dead DefaultSelection field from WidgetDefinition struct
- Add warning logs for silent errors in initPluggableEngine and LoadUserDefinitions
- Refactor TestOpSelection to test real opSelection function, add TestOpSelectionEmptyValue
- Use return instead of break in buildWidgetV3 default case for clearer control flow
- Extract association attribute correctly in ComboBox DESCRIBE (extractCustomWidgetPropertyAssociation)
- Update MDL examples to reflect CE1613 fix status
…otes

- Add engine design plan document
- Add custom-widgets skill for Mendix projects
- Add pluggable widget describe/parse tests
- Add gallery user definition example
- Add roundtrip issues tracking document
- Use atomic.Uint32 for placeholderCounter to prevent data races
- Only ignore os.IsNotExist in loadDefinitionsFromDir, warn on other errors
- Propagate registry init failure reason in unsupported widget type errors
- Document LSP completion cache sync.Once limitation
Copy link
Collaborator

@ako ako left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: feat: pluggable widget engine with data-driven definitions

25 files, +9385/-2662, 10 commits. Replaces hardcoded pluggable widget builders with a data-driven PluggableWidgetEngine using .def.json definition files.


Proposal Alignment

There are 5 relevant proposals in the repo. Here's how the PR maps to each:

Proposal PR #28 Alignment Verdict
MPK Widget Augmentation (docs/11-proposals/PROPOSAL_mpk_widget_augmentation.md) Directly implements — sdk/widgets/mpk/ parses .mpk files, augments templates at runtime Aligned
BSON Schema Registry (docs/11-proposals/BSON_SCHEMA_REGISTRY_PROPOSAL.md) 3-tier WidgetRegistry (embedded → global → project) matches the proposal's Tier 1/2/3 widget schema resolution Aligned
Show/Describe Pluggable Widgets (docs/03-development/proposals/show-describe-pluggable-widgets.md) Adds DESCRIBE-side association extraction for ComboBox, but the proposal's phased widget coverage (Image → Container → Value → Chart) is not addressed Partially aligned
Bulk Widget Property Updates (docs/11-proposals/PROPOSAL_bulk_widget_property_updates.md) Not addressed N/A (separate scope)
Update Built-in Widget Properties (docs/11-proposals/PROPOSAL_update_builtin_widget_properties.md) Not addressed N/A (separate scope)

Overall: Well-aligned with the MPK augmentation and BSON schema registry proposals. The data-driven .def.json approach with OperationRegistry is a clean implementation of the declarative widget construction concept.


Critical Issues

1. Association mapping runs before DataSource — wrong entity context (widget_engine.go)

In combobox.def.json, the association mode lists attributeAssociation (type: Association) before optionsSourceAssociationDataSource (type: DataSource). The engine processes mappings in definition order, so entityContext is stale when the association mapping runs — it uses whatever entity was set by the parent DataView, not the ComboBox's own datasource. The old hardcoded buildComboBoxV3 explicitly built the datasource first. This will serialize the wrong entity reference for ComboBox association mode.

2. Inconsistent gallery.def.json files

The embedded sdk/widgets/definitions/gallery.def.json has 2 child slots (TEMPLATE, FILTER) while .mxcli/widgets/gallery.def.json has 3 slots (TEMPLATE, EMPTYPLACEHOLDER, FILTERSPLACEHOLDER). Container name also differs: FILTER vs FILTERSPLACEHOLDER. Different behavior depending on which definition is loaded.


Moderate Issues

3. initPluggableEngine retries on every widget after failure (cmd_pages_builder.go)

If NewWidgetRegistry() fails, pluggableEngine stays nil but the guard only checks pluggableEngine != nil. Every subsequent pluggable widget re-attempts initialization, printing the warning repeatedly. Add a second guard: if pb.pluggableEngineErr != nil { return }.

4. LoadUserDefinitions error silently swallowed (cmd_widget.go)

_ = registry.LoadUserDefinitions(projectPath) discards errors. Users with malformed .def.json files get no feedback.

5. "Last fallback wins" instead of documented "first match wins" (widget_engine.go)

The selectMappings loop overwrites fallback on each no-condition mode instead of keeping the first one. Works by accident today (definitions only have one fallback), but violates the documented semantic.


Minor Issues

6. No validation of operation names at .def.json load time (widget_registry.go)

Invalid operation fields only fail at build time. Load-time validation would give better DX.

7. Test uses invalid condition string (widget_engine_test.go)

TestWidgetDefinitionJSONRoundTrip uses "DataSource != nil" which evaluateCondition doesn't recognize. Test passes because it only tests serialization, but embeds a wrong assumption about condition syntax.

8. Magic string "TEMPLATE" for default slot (widget_engine.go)

Hardcoded — should be a constant.

9. No zip-bomb protection in MPK extract (mpk.go)

Low severity for a CLI tool, but ParseMPK has no file size limits.


What Looks Good

  • Architecture: OperationRegistry + WidgetDefinition + 3-tier WidgetRegistry is clean and extensible, matching the proposals well
  • 361 lines of hardcoded builders replaced with 7 declarative .def.json files — significant maintenance win
  • mxcli widget extract CLI for generating skeleton definitions from .mpk files enables community contribution
  • Gallery CE0463 fix with proper nested ObjectType augmentation for object-type properties
  • 40+ tests covering engine, registry, and DESCRIBE paths
  • 3 self-review commits addressing dead fields, atomic counters, error propagation — good discipline
  • atomic.Uint32 for placeholderCounter — correctly fixes a real race condition

Recommendation

Request changes — fix the critical mapping order dependency (#1) and resolve the gallery definition inconsistency (#2). The association/datasource ordering issue is a real correctness bug that will produce wrong BSON for ComboBox association mode. The rest can be follow-ups.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants