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
6 changes: 3 additions & 3 deletions docs/11-proposals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ for display in this README):
## Active Proposals (84)


### In Progress (partial) (8)
### In Progress (partial) (9)

| Proposal | Status | Summary |
|----------|--------|---------|
Expand All @@ -41,8 +41,9 @@ for display in this README):
| [mxcli marketplace — Download & Manage Marketplace Modules](PROPOSAL_marketplace_modules.md) | Partial | Unblocking path #1 below has happened: the content API now returns a |
| [Navigation Support in MDL](navigation-support.md) | Partial | Every Mendix project has exactly one navigation$NavigationDocument — a project-level singleton containing navigation profiles (Responsive, P |
| [Podman Support as Docker Alternative](PROPOSAL_podman_support.md) | Partial | Docker Desktop requires a paid subscription for larger organizations. |
| [SHOW/DESCRIBE/USE Building Blocks](show-describe-building-blocks.md) | Partial | Document type: Pages$BuildingBlock (NOT Forms$BuildingBlock — the reader now |

### Proposed (36)
### Proposed (35)

| Proposal | Status | Summary |
|----------|--------|---------|
Expand All @@ -60,7 +61,6 @@ for display in this README):
| [Page Composition and Partial Updates](proposal_page_composition.md) | Proposed | Large MDL page scripts become unwieldy to write, read, and maintain. |
| [Prompt: Journey Architecture Diagram Generator for Mendix Projects](journey-architecture-viz.md) | Proposed | I'm building a CLI tool (part of mxcli) that generates a "Customer Journey Architecture" diagram from a Mendix project. |
| [Session Logging & Diagnostics for mxcli](PROPOSAL_session_logging.md) | Proposed | mxcli is being distributed to users for testing. |
| [SHOW/DESCRIBE Building Blocks](show-describe-building-blocks.md) | Proposed | Document type: Forms$BuildingBlock |
| [SHOW/DESCRIBE Custom Icon Collections](show-describe-custom-icon-collections.md) | Proposed | Document type: CustomIcons$CustomIconCollection |
| [SHOW/DESCRIBE Export Mappings](show-describe-export-mappings.md) | Proposed | Document type: ExportMappings$ExportMapping |
| [SHOW/DESCRIBE Image Collections](show-describe-image-collections.md) | Proposed | Document type: Images$ImageCollection |
Expand Down
132 changes: 127 additions & 5 deletions docs/11-proposals/show-describe-building-blocks.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
---
title: SHOW/DESCRIBE Building Blocks
status: proposed
title: SHOW/DESCRIBE/USE Building Blocks
status: partial
related:
- PR #16 (READ — SHOW/DESCRIBE + catalog)
- PR #17 (READ — modelsdk ListBuildingBlocks + exec-safe example)
---

# Proposal: SHOW/DESCRIBE Building Blocks
# Proposal: SHOW / DESCRIBE / USE Building Blocks

## Overview

**Document type:** `Forms$BuildingBlock`
**Prevalence:** 233 across test projects (83 Enquiries, 73 Evora, 77 Lato)
**Document type:** `Pages$BuildingBlock` (NOT `Forms$BuildingBlock` — the reader now
does a `Pages$`-first, `Forms$`-fallback lookup)
**Prevalence:** every project ships ~39 out-of-the-box Atlas blocks (`Atlas_Web_Content`)
**Priority:** High — present in every project, reusable UI components

Building Blocks are reusable widget compositions that can be dragged onto pages in Studio Pro. They are structurally similar to Snippets but serve as templates rather than runtime components.

The Building-Block capability has three layers — **READ → INSTANTIATE → AUTHOR**:

| Capability | What | Status |
|---|---|---|
| **READ** — `SHOW`/`DESCRIBE BUILDING BLOCK`, `CATALOG.building_blocks` | discover + inspect a block's widget tree | ✅ **shipped** (PR #16/#17); validated live against a real Atlas app (40 blocks) |
| **INSTANTIATE** — `USE BUILDING BLOCK` | deep-copy a block onto a page | 📐 **specced below** (v1 in progress) |
| **AUTHOR** — `CREATE BUILDING BLOCK` | contribute a block back to the toolbox | ⬜ future |

The rest of this document: the (now-shipped) READ design for reference, then the
**USE BUILDING BLOCK follow-up slice**.

## What Already Exists

| Layer | Status | Location |
Expand Down Expand Up @@ -131,3 +146,110 @@ func (e *Executor) GetBuildingBlockNames(moduleFilter string) []string

- Create `mdl-examples/doctype-tests/17-building-block-examples.mdl`
- Verify against all 3 test projects

---

# Follow-up slice: `USE BUILDING BLOCK` (Instantiate)

The READ capability lets you *discover and inspect* a project's out-of-the-box Atlas
blocks. `USE BUILDING BLOCK` closes the loop: **deep-copy** a block's widget tree onto
a page in one line, instead of hand-mirroring its `DESCRIBE` output. This is the single
biggest lever for the `atlas-design` skill — it turns "discover → inspect → **mirror**"
into "discover → **use**".

## Syntax — mirrors `use fragment`, sourced from a persisted block

```mdl
use building block Atlas_Web_Content.Card -- deep-copy the block's tree here
use building block Atlas_Web_Content.Card as cust_ -- prefix the copied widget names
```

It is a **page-body element** (valid anywhere `use fragment` is — page bodies, containers,
columns, placeholders), not a top-level statement. The only syntactic difference from
`use fragment X as p_` is the **qualified name** (`Module.Name`), because a building block
is a persisted `Pages$BuildingBlock` document rather than a script-scoped `define fragment`.

### Worked example

```mdl
create page Sales.CustomerOverview (title: 'Customers') {
layoutgrid g1 {
row r1 {
column c1 (desktopwidth: 6) { use building block Atlas_Web_Content.Card as cust_ }
column c2 (desktopwidth: 6) { use building block Atlas_Web_Content.Card as order_ }
}
}
}
```

Each `use` deep-copies the block's real tree (from `DESCRIBE Atlas_Web_Content.Card`):
`container (DesignProperties: ['Card style': on]) { dynamictext (Class: 'card-title', …) }`
→ `as cust_` yields `cust_container…` + `cust_text…`, `as order_` the parallel set.

## Semantics (match how Atlas blocks actually behave)

- **Deep copy, no live link** — identical to dragging a block onto a page in Studio Pro.
After insertion the widgets are the page's own, freely editable.
- **Name-collision handling** via the `as <prefix>` rename (same as fragments).
- **Read-only source** — the block document is never mutated; `USE` only reads it.

## Configuration: afterwards (v1), inline sugar (v1.1) — never magic

A building block has **no parameter interface** — it is a raw widget-tree template, so
there is nothing to pass arguments *to*. You configure by editing the copied widgets.

- **v1 — configure afterwards** with the already-shipped `alter page` commands. This is
the honest default (mirrors Studio Pro's drag-then-configure) and needs zero new
machinery beyond the copy:
```mdl
use building block Atlas_Web_Content.Card as cust_
alter page Sales.CustomerOverview set cust_text22 (content: 'Customers');
```
- **v1.1 — optional inline override block** (sugar over `use` + immediate `alter`), scoped
to the freshly-copied widgets, referencing the block's real internal names (from
`DESCRIBE`):
```mdl
use building block Atlas_Web_Content.Card as cust_ {
set text22 (content: 'Customers')
}
```
- **Maybe — a data-context shortcut** for the one config a block often can't work without
(a `Master_Detail`/`List_Cards` dropped with no entity is inert):
`use building block Atlas_Web_Content.List_Cards as prod_ over database Sales.Product`
— this just resolves to "set the copied datasource host's source", not a slot.
- **Never — implicit slots.** Do not have the engine guess which widget is "the title" or
"the datasource host". Atlas blocks don't declare roles; heuristic inference is fragile.
Keep configuration **explicit** (by widget name), inline or post-hoc.

## Implementation (v1) — reuse the fragment-expansion machinery

Confirmed feasible; v1 in progress on `feature/use-building-block`.

1. **Grammar** — add `useBuildingBlockRef : USE BUILDING BLOCK qualifiedName (AS
identifierOrKeyword)?` to `MDLPage.g4`, as an alternative everywhere `useFragmentRef`
appears in the page body. (`BUILDING`/`BLOCK` tokens already exist from READ.)
2. **AST/visitor** — mirror `buildUseFragmentRef`: emit a sentinel
`WidgetV3{Type: "USE_BUILDING_BLOCK", Name: "<Module.Name>", Properties: {Prefix}}`.
3. **Executor** — in `expandIfFragment` (the same hook that expands `USE_FRAGMENT`), add a
branch that resolves the block (`ListBuildingBlocks` + hierarchy), reads its widgets via
the READ path (`getBuildingBlockWidgetsFromRaw` → `GetRawUnit`), and converts them to
`[]*ast.WidgetV3`, then applies the prefix and returns them for the normal builder to
serialize.
4. **BSON → `[]*ast.WidgetV3` conversion** — rather than a fragile hand-written converter,
**round-trip through the DESCRIBE renderer**: render the block's raw widgets to MDL
(`outputWidgetMDLV3`), wrap in `define fragment __tmp as { … }`, re-parse with
`visitor.Build`, and take `.Widgets`. This reuses the entire existing widget
parser/visitor, so it handles every widget type DESCRIBE can emit and degrades to a
clear parse error otherwise. (Verified: real Card DESCRIBE output re-parses cleanly.)

## Testing

- Executor mock test: a block's raw widgets → expanded (prefixed) into a page's widget list.
- Real end-to-end: `use building block Atlas_Web_Content.Card as cust_` on a real Atlas
project, then `describe page` to confirm the prefixed card widgets landed, plus `mx check`.

## Later

- **v1.1** inline override block; the `over database …` data-context shortcut.
- **AUTHOR** (`CREATE BUILDING BLOCK`) so generated apps contribute blocks back to the
Studio-Pro toolbox.
2 changes: 2 additions & 0 deletions mdl/executor/cmd_alter_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ func buildColumnSpecsFromAST(ctx *ExecContext, widgets []*ast.WidgetV3, moduleNa
}

pb := &pageBuilder{
ctx: ctx,
backend: ctx.Backend,
moduleID: moduleID,
moduleName: moduleName,
Expand Down Expand Up @@ -323,6 +324,7 @@ func buildWidgetsFromAST(ctx *ExecContext, widgets []*ast.WidgetV3, moduleName s
}

pb := &pageBuilder{
ctx: ctx,
backend: ctx.Backend,
moduleID: moduleID,
moduleName: moduleName,
Expand Down
1 change: 1 addition & 0 deletions mdl/executor/cmd_pages_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

// pageBuilder constructs pages from AST.
type pageBuilder struct {
ctx *ExecContext // execution context (for building-block expansion, etc.)
backend backend.FullBackend
moduleID model.ID
moduleName string
Expand Down
89 changes: 86 additions & 3 deletions mdl/executor/cmd_pages_builder_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/mendixlabs/mxcli/mdl/ast"
mdlerrors "github.com/mendixlabs/mxcli/mdl/errors"
"github.com/mendixlabs/mxcli/mdl/types"
"github.com/mendixlabs/mxcli/mdl/visitor"
"github.com/mendixlabs/mxcli/model"
"github.com/mendixlabs/mxcli/sdk/domainmodel"
"github.com/mendixlabs/mxcli/sdk/microflows"
Expand Down Expand Up @@ -1732,13 +1733,23 @@ func (pb *pageBuilder) expandFragments(widgets []*ast.WidgetV3) ([]*ast.WidgetV3
return result, nil
}

// expandIfFragment returns the widget as-is if it's not a USE_FRAGMENT sentinel,
// or expands it into cloned fragment widgets with optional prefix.
// expandIfFragment returns the widget as-is if it's not a USE_FRAGMENT or
// USE_BUILDING_BLOCK sentinel, or expands it into cloned/copied widgets with an
// optional prefix.
func (pb *pageBuilder) expandIfFragment(w *ast.WidgetV3) ([]*ast.WidgetV3, error) {
if w.Type != "USE_FRAGMENT" {
switch w.Type {
case "USE_FRAGMENT":
return pb.expandFragmentRef(w)
case "USE_BUILDING_BLOCK":
return pb.expandBuildingBlockRef(w)
default:
return []*ast.WidgetV3{w}, nil
}
}

// expandFragmentRef expands a USE_FRAGMENT sentinel into cloned fragment widgets
// with an optional prefix rename.
func (pb *pageBuilder) expandFragmentRef(w *ast.WidgetV3) ([]*ast.WidgetV3, error) {
if pb.fragments == nil {
return nil, mdlerrors.NewNotFound("fragment", w.Name)
}
Expand All @@ -1754,6 +1765,78 @@ func (pb *pageBuilder) expandIfFragment(w *ast.WidgetV3) ([]*ast.WidgetV3, error
return widgets, nil
}

// expandBuildingBlockRef deep-copies a building block's widget tree into the
// page/container. The DESCRIBE renderer already emits faithful, re-parseable MDL
// for a block's widgets, so expansion = render the block's widgets to MDL text →
// re-parse them via a `define fragment` wrapper → apply the optional prefix. This
// reuses the whole existing widget parser instead of a hand-written BSON→AST
// converter.
func (pb *pageBuilder) expandBuildingBlockRef(w *ast.WidgetV3) ([]*ast.WidgetV3, error) {
if pb.ctx == nil {
return nil, mdlerrors.NewNotFound("building block", w.Name)
}
ctx := pb.ctx

// Resolve the block by module + name.
qn := parseQualifiedNameStr(w.Name)
h, err := getHierarchy(ctx)
if err != nil {
return nil, mdlerrors.NewBackend("build hierarchy", err)
}
blocks, err := ctx.Backend.ListBuildingBlocks()
if err != nil {
return nil, mdlerrors.NewBackend("list building blocks", err)
}
var found *pages.BuildingBlock
for _, bb := range blocks {
modID := h.FindModuleID(bb.ContainerID)
modName := h.GetModuleName(modID)
if bb.Name == qn.Name && (qn.Module == "" || modName == qn.Module) {
found = bb
break
}
}
if found == nil {
return nil, mdlerrors.NewNotFound("building block", w.Name)
}

// Read the block's widgets as raw BSON.
rawWidgets := getBuildingBlockWidgetsFromRaw(ctx, found.ID)
if len(rawWidgets) == 0 {
return nil, nil
}

// Render to MDL text. outputWidgetMDLV3 writes to ctx.Output; redirect it to a
// buffer via a shallow ExecContext copy so we can capture the rendered MDL.
var sb strings.Builder
renderCtx := *ctx
renderCtx.Output = &sb
for _, rw := range rawWidgets {
outputWidgetMDLV3(&renderCtx, rw, 1)
}

// Re-parse via a `define fragment` wrapper to obtain []*ast.WidgetV3.
src := "define fragment __bbtmp as {\n" + sb.String() + "\n};"
prog, errs := visitor.Build(src)
if len(errs) > 0 {
return nil, fmt.Errorf("use building block %s: could not expand widget tree: %v", w.Name, errs)
}
if len(prog.Statements) == 0 {
return nil, fmt.Errorf("use building block %s: could not expand widget tree", w.Name)
}
def, ok := prog.Statements[0].(*ast.DefineFragmentStmt)
if !ok {
return nil, fmt.Errorf("use building block %s: could not expand widget tree", w.Name)
}
widgets := def.Widgets

// Apply the optional prefix rename.
if prefix, ok := w.Properties["Prefix"].(string); ok && prefix != "" {
prefixWidgetNames(widgets, prefix)
}
return widgets, nil
}

// cloneWidgets deep-copies a widget tree to avoid mutating the fragment definition.
func cloneWidgets(widgets []*ast.WidgetV3) []*ast.WidgetV3 {
if widgets == nil {
Expand Down
2 changes: 2 additions & 0 deletions mdl/executor/cmd_pages_create_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func execCreatePageV3(ctx *ExecContext, s *ast.CreatePageStmtV3) error {

// Build the page BEFORE deleting the old one (atomic: if build fails, old page is preserved)
pb := &pageBuilder{
ctx: ctx,
backend: ctx.Backend,
moduleID: moduleID,
moduleName: s.Name.Module,
Expand Down Expand Up @@ -138,6 +139,7 @@ func execCreateSnippetV3(ctx *ExecContext, s *ast.CreateSnippetStmtV3) error {

// Build the snippet BEFORE deleting the old one (atomic: if build fails, old snippet is preserved)
pb := &pageBuilder{
ctx: ctx,
backend: ctx.Backend,
moduleID: moduleID,
moduleName: s.Name.Module,
Expand Down
Loading
Loading