From 8515ba51622006228cdc3db467ed3e988915c2c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 22:40:22 +0000 Subject: [PATCH] =?UTF-8?q?feat(alter-page):=20INSERT=20INTO=20container?= =?UTF-8?q?=20=E2=80=94=20append=20widgets=20as=20a=20container's=20childr?= =?UTF-8?q?en?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ALTER PAGE could only INSERT BEFORE/AFTER a sibling widget, so there was no way to fill an EMPTY container or add a widget to a container/dataview without an existing sibling to anchor to. Add: alter page Mod.Page { insert into containerName { } } which appends the widgets as the last children of the named container. Full-stack: grammar (INSERT INTO), visitor (Position "INTO"), executor (children take the target's own entity context, e.g. a dataview's entity), and both mutators (pagemutator for MPR files, MCP for live Studio Pro). An empty container omits its Widgets list, so it is created with the Mendix widget-list marker and written back into the parent (the bson.D grows). Simple containers are supported (DivContainer, Container, DataView, GroupBox, ScrollContainerRegion, Section); LayoutGrid and TabContainer have no single child list and give a clear error pointing to INSERT BEFORE/AFTER inside the target column/tab. Verified on 11.12.1: insert into an empty container, append to a non-empty one, and insert a data-bound widget into a dataview — mx check 0 errors. Unit test for the mutator INTO path + error case; doctype example; skill / quick-reference / syntax help updated. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4 --- .claude/skills/mendix/alter-page.md | 12 +++ cmd/mxcli/syntax/features_page.go | 4 +- docs-site/src/appendixes/quick-reference.md | 1 + docs-site/src/examples/alter-page.md | 12 +++ docs-site/src/language/alter-page.md | 9 ++ docs-site/src/language/snippets.md | 3 + docs-site/src/reference/page/alter-page.md | 9 ++ docs/01-project/MDL_QUICK_REFERENCE.md | 1 + .../01-language-reference.md | 1 + .../doctype-tests/33-alter-page-examples.mdl | 13 +++ .../doctype-tests/alter-page-insert-into.mdl | 50 +++++++++++ mdl/ast/ast_alter_page.go | 6 +- mdl/backend/mcp/page_mutator.go | 12 ++- mdl/backend/mcp/page_mutator_test.go | 16 ++++ mdl/backend/pagemutator/mutator.go | 87 +++++++++++++++++++ mdl/executor/cmd_alter_page.go | 8 +- mdl/grammar/MDLParser.g4 | 1 + mdl/visitor/visitor_alter_page.go | 2 + 18 files changed, 240 insertions(+), 7 deletions(-) create mode 100644 mdl-examples/doctype-tests/alter-page-insert-into.mdl diff --git a/.claude/skills/mendix/alter-page.md b/.claude/skills/mendix/alter-page.md index 2f691c8ab..4865f46c5 100644 --- a/.claude/skills/mendix/alter-page.md +++ b/.claude/skills/mendix/alter-page.md @@ -134,10 +134,22 @@ insert after txtName { insert before btnSave { actionbutton btnPreview (caption: 'Preview', action: microflow Module.ACT_Preview) } + +-- Insert INTO a container — append as its last child (works on an EMPTY container) +insert into ctnToolbar { + actionbutton btnNew (caption: 'New', action: nothing, buttonstyle: primary) +} ``` Inserted widgets use the same syntax as `create page`. Multiple widgets can be inserted in a single block. +`insert into ` appends as the last child of the named container — the +only way to fill an **empty** container, and handy for adding to a container/dataview +without needing a sibling to anchor to. Widgets inserted into a dataview take that +dataview's entity as their context. Supported on simple containers (container, +dataview, groupbox, scroll-container region); for a layout grid or tab container, +insert relative to a widget inside the target column/tab instead. + ### DROP - Remove Widgets ```sql diff --git a/cmd/mxcli/syntax/features_page.go b/cmd/mxcli/syntax/features_page.go index 5bab9f14e..5a4762a0d 100644 --- a/cmd/mxcli/syntax/features_page.go +++ b/cmd/mxcli/syntax/features_page.go @@ -89,7 +89,7 @@ func init() { "set property", "insert widget", "drop widget", "replace widget", "popup width", "popup height", "popup resizable", }, - Syntax: "ALTER PAGE Module.Name {\n SET property = value ON widgetName;\n SET (prop1 = val1, prop2 = val2) ON widgetName;\n SET Title = 'New Title'; -- page-level (case-sensitive)\n SET Class = 'css-class'; -- page-level CSS class / style\n SET Style = 'css: rule';\n SET PopupWidth = 800; -- page-level pop-up dimensions\n SET PopupHeight = 480;\n SET PopupResizable = true;\n INSERT AFTER widgetName { };\n INSERT BEFORE widgetName { };\n DROP WIDGET name1, name2;\n REPLACE widgetName WITH { };\n};", + Syntax: "ALTER PAGE Module.Name {\n SET property = value ON widgetName;\n SET (prop1 = val1, prop2 = val2) ON widgetName;\n SET Title = 'New Title'; -- page-level (case-sensitive)\n SET Class = 'css-class'; -- page-level CSS class / style\n SET Style = 'css: rule';\n SET PopupWidth = 800; -- page-level pop-up dimensions\n SET PopupHeight = 480;\n SET PopupResizable = true;\n INSERT AFTER widgetName { };\n INSERT BEFORE widgetName { };\n INSERT INTO containerName { };\n DROP WIDGET name1, name2;\n REPLACE widgetName WITH { };\n};", Example: "ALTER PAGE Module.EditPage {\n SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave;\n INSERT AFTER txtName {\n TEXTBOX txtMiddleName (Label: 'Middle Name', Binds: MiddleName)\n };\n DROP WIDGET txtUnused;\n};", SeeAlso: []string{"page.create", "page.show", "snippet.alter"}, }) @@ -152,7 +152,7 @@ func init() { Keywords: []string{ "alter snippet", "modify snippet", "update snippet", }, - Syntax: "ALTER SNIPPET Module.Name {\n SET property = value ON widgetName;\n INSERT AFTER widgetName { };\n INSERT BEFORE widgetName { };\n DROP WIDGET name1, name2;\n REPLACE widgetName WITH { };\n};", + Syntax: "ALTER SNIPPET Module.Name {\n SET property = value ON widgetName;\n INSERT AFTER widgetName { };\n INSERT BEFORE widgetName { };\n INSERT INTO containerName { };\n DROP WIDGET name1, name2;\n REPLACE widgetName WITH { };\n};", Example: "ALTER SNIPPET Module.NavSnippet {\n REPLACE navItem1 WITH {\n ACTIONBUTTON btnHome (Caption: 'Home', Action: SHOW_PAGE Module.HomePage)\n };\n DROP WIDGET txtOldField;\n INSERT AFTER txtName {\n TEXTBOX txtNewField (Label: 'New Field', Binds: NewAttr)\n };\n};", SeeAlso: []string{"snippet", "page.alter"}, }) diff --git a/docs-site/src/appendixes/quick-reference.md b/docs-site/src/appendixes/quick-reference.md index 2fdbd5bb5..d8c108a3e 100644 --- a/docs-site/src/appendixes/quick-reference.md +++ b/docs-site/src/appendixes/quick-reference.md @@ -421,6 +421,7 @@ Modify an existing page or snippet's widget tree in-place without full `CREATE O | Page-level set | `SET Title = 'New Title'` | No ON clause for page properties | | Insert after | `INSERT AFTER widgetName { widgets }` | Add widgets after target | | Insert before | `INSERT BEFORE widgetName { widgets }` | Add widgets before target | +| Insert into | `INSERT INTO containerName { widgets }` | Append as the container's last child (fills an empty container) | | Drop widgets | `DROP WIDGET name1, name2` | Remove widgets by name | | Replace widget | `REPLACE widgetName WITH { widgets }` | Replace widget subtree | | Pluggable prop | `SET 'showLabel' = false ON cbStatus` | Quoted name for pluggable widgets | diff --git a/docs-site/src/examples/alter-page.md b/docs-site/src/examples/alter-page.md index 5fbee57ab..af4461984 100644 --- a/docs-site/src/examples/alter-page.md +++ b/docs-site/src/examples/alter-page.md @@ -20,6 +20,18 @@ ALTER PAGE CRM.Customer_Edit { }; ``` +## Add a Widget Into a Container + +Use `INSERT INTO` to append a widget as a container's last child — the only way to fill an empty container. + +```sql +ALTER PAGE CRM.Customer_Overview { + INSERT INTO ctnToolbar { + ACTIONBUTTON btnNew (Caption: 'New Customer', Action: NOTHING, ButtonStyle: Primary) + } +}; +``` + ## Remove Widgets ```sql diff --git a/docs-site/src/language/alter-page.md b/docs-site/src/language/alter-page.md index 6252d19d1..677b34261 100644 --- a/docs-site/src/language/alter-page.md +++ b/docs-site/src/language/alter-page.md @@ -110,8 +110,17 @@ ALTER PAGE Module.EditPage { ACTIONBUTTON btnPreview (Caption: 'Preview', Action: MICROFLOW Module.ACT_Preview) } }; + +-- Insert INTO a container — append as its last child (works on an empty container) +ALTER PAGE Module.EditPage { + INSERT INTO ctnToolbar { + ACTIONBUTTON btnNew (Caption: 'New', Action: NOTHING, ButtonStyle: Primary) + } +}; ``` +`INSERT INTO ` appends widgets as the container's last children — the only way to fill an **empty** container, and handy for adding to a container or data view without a sibling to anchor to. Widgets inserted into a data view take that data view's entity. Supported on simple containers; for a layout grid or tab container, insert relative to a widget inside the target column/tab instead. + The inserted widgets use the same syntax as in `CREATE PAGE`. Multiple widgets can be inserted in a single block. ### DROP WIDGET -- Remove Widgets diff --git a/docs-site/src/language/snippets.md b/docs-site/src/language/snippets.md index 31407a0c6..04fb1c533 100644 --- a/docs-site/src/language/snippets.md +++ b/docs-site/src/language/snippets.md @@ -97,6 +97,9 @@ ALTER SNIPPET MyModule.CustomerCard { SET Caption = 'View Details' ON btnEdit; INSERT AFTER txtEmail { DYNAMICTEXT txtPhone (Content: '{1}', Attribute: Phone) + }; + INSERT INTO ctnActions { -- append as the container's last child + ACTIONBUTTON btnMore (Caption: 'More', Action: NOTHING) } }; ``` diff --git a/docs-site/src/reference/page/alter-page.md b/docs-site/src/reference/page/alter-page.md index 274be7f9d..a7dcae7a0 100644 --- a/docs-site/src/reference/page/alter-page.md +++ b/docs-site/src/reference/page/alter-page.md @@ -29,6 +29,9 @@ SET 'propertyName' = value ON widgetName; INSERT BEFORE widgetName { widget_definitions }; INSERT AFTER widgetName { widget_definitions }; +-- Insert widgets as the last children of a container +INSERT INTO containerName { widget_definitions }; + -- Remove widgets DROP WIDGET widgetName1, widgetName2; @@ -68,6 +71,12 @@ For pluggable widget properties, use quoted property names (e.g., `'showLabel'`) Inserts new widgets immediately before or after a named widget within its parent container. The new widgets use the same syntax as in `CREATE PAGE`. +### INSERT INTO + +Appends new widgets as the **last children** of a named container. This is the only way to fill an **empty** container, and the natural way to add a widget to a container or data view without needing a sibling to anchor to. Widgets inserted into a data view take that data view's entity as their context. + +Supported on simple containers (container/DivContainer, data view, group box, scroll-container region). A layout grid (rows/columns) and tab container have no single child list — insert relative to a widget inside the target column or tab instead. + ### DROP WIDGET Removes one or more widgets by name. The widget and all its children are removed from the tree. diff --git a/docs/01-project/MDL_QUICK_REFERENCE.md b/docs/01-project/MDL_QUICK_REFERENCE.md index 5fa90a8cd..0f09a5190 100644 --- a/docs/01-project/MDL_QUICK_REFERENCE.md +++ b/docs/01-project/MDL_QUICK_REFERENCE.md @@ -982,6 +982,7 @@ Modify an existing page or snippet's widget tree in-place without full `create o | Widget dynamic classes | `set DynamicClasses = 'expr' on widgetName` | Runtime-computed classes on a widget — the surgical alternative to a bulk `update widgets` | | Insert after | `insert after widgetName { widgets }` | Add widgets after target | | Insert before | `insert before widgetName { widgets }` | Add widgets before target | +| Insert into | `insert into containerName { widgets }` | Append as the container's last child (fills an empty container; dataview children take its entity) | | Drop widgets | `drop widget name1, name2` | Remove widgets by name | | Replace widget | `replace widgetName with { widgets }` | Replace widget subtree | | Pluggable prop | `set 'showLabel' = false on cbStatus` | Quoted name for pluggable widgets | diff --git a/docs/05-mdl-specification/01-language-reference.md b/docs/05-mdl-specification/01-language-reference.md index 70023ae83..4f94a3ded 100644 --- a/docs/05-mdl-specification/01-language-reference.md +++ b/docs/05-mdl-specification/01-language-reference.md @@ -838,6 +838,7 @@ set title = 'New Title'; -- Insert widgets insert after widgetName { }; insert before widgetName { }; +insert into containerName { }; -- append as the container's last child -- Remove widgets drop widget name1, name2; diff --git a/mdl-examples/doctype-tests/33-alter-page-examples.mdl b/mdl-examples/doctype-tests/33-alter-page-examples.mdl index 48b895573..1a4c00d0f 100644 --- a/mdl-examples/doctype-tests/33-alter-page-examples.mdl +++ b/mdl-examples/doctype-tests/33-alter-page-examples.mdl @@ -131,6 +131,19 @@ alter page AlterPg.Product_Overview { } }; +-- MARK: AP02b — INSERT INTO a container (append as last child) + +-- ============================================================================ +-- AP02b: INSERT INTO — append widgets as a container's children. This is the +-- only way to fill an EMPTY container (here, the `divider` just created above). +-- ============================================================================ + +alter page AlterPg.Product_Overview { + insert into divider { + dynamictext dividerNote (Content: 'Catalog below', RenderMode: paragraph) + } +}; + -- MARK: AP03 — INSERT a DataGrid2 with initial columns -- ============================================================================ diff --git a/mdl-examples/doctype-tests/alter-page-insert-into.mdl b/mdl-examples/doctype-tests/alter-page-insert-into.mdl new file mode 100644 index 000000000..72bdfc2e3 --- /dev/null +++ b/mdl-examples/doctype-tests/alter-page-insert-into.mdl @@ -0,0 +1,50 @@ +-- ============================================================================ +-- ALTER PAGE — INSERT INTO +-- ============================================================================ +-- INSERT INTO appends widgets as the LAST children of a named container, rather +-- than as a sibling before/after another widget. This is the only way to fill an +-- EMPTY container, and the natural way to add a widget to a container/dataview +-- without needing a sibling to anchor to. +-- +-- insert into { } +-- +-- Simple containers are supported (container/DivContainer, dataview, groupbox, +-- scroll-container region). For a layout grid or tab container, insert relative to +-- a widget inside the target column/tab instead. +-- ============================================================================ + +create module AlterInto; +/ +create entity AlterInto.Item ( Title: String, Status: String ); +/ +create or replace page AlterInto.Home ( layout: Atlas_Core.Atlas_Default ) { + container ctnEmpty (class: 'toolbar') { } + container ctnBody (class: 'body') { + dynamictext dtExisting (content: 'Existing content') + } +} +/ +create or replace page AlterInto.Detail ( params: { $Item: AlterInto.Item }, layout: Atlas_Core.Atlas_Default ) { + dataview dv (datasource: $Item) { } +} +/ +-- Fill an empty container. +alter page AlterInto.Home { + insert into ctnEmpty { + actionbutton btnNew (caption: 'New', action: nothing, buttonstyle: primary) + } +} +/ +-- Append as the last child of a container that already has widgets. +alter page AlterInto.Home { + insert into ctnBody { + pluggablewidget 'com.mendix.widget.custom.badge.Badge' bdgStatus (type: 'badge', value: 'Active') + } +} +/ +-- Insert a data-bound widget into a dataview — the children take the dataview's entity. +alter page AlterInto.Detail { + insert into dv { + dynamictext dtTitle (content: '{1}', contentparams: [{1} = Title], rendermode: H1) + } +} diff --git a/mdl/ast/ast_alter_page.go b/mdl/ast/ast_alter_page.go index 6f5a836d2..c857084b0 100644 --- a/mdl/ast/ast_alter_page.go +++ b/mdl/ast/ast_alter_page.go @@ -50,10 +50,10 @@ type SetPropertyOp struct { func (s *SetPropertyOp) isAlterPageOperation() {} -// InsertWidgetOp represents: INSERT AFTER/BEFORE widgetRef { widgets } +// InsertWidgetOp represents: INSERT AFTER/BEFORE/INTO widgetRef { widgets } type InsertWidgetOp struct { - Position string // "AFTER" or "BEFORE" - Target WidgetRef // widget/column to insert relative to + Position string // "AFTER", "BEFORE", or "INTO" (append as children of the container) + Target WidgetRef // widget to insert relative to, or container to insert into Widgets []*WidgetV3 } diff --git a/mdl/backend/mcp/page_mutator.go b/mdl/backend/mcp/page_mutator.go index acdc4f70b..7186b3814 100644 --- a/mdl/backend/mcp/page_mutator.go +++ b/mdl/backend/mcp/page_mutator.go @@ -260,7 +260,7 @@ func (m *mcpPageMutator) InsertWidget(widgetRef string, columnRef string, positi if columnRef != "" { return fmt.Errorf("inserting into a column (%s.%s) is not yet supported by the MCP backend", widgetRef, columnRef) } - parent, key, idx, _, ok := findWidget(m.content, widgetRef) + parent, key, idx, widget, ok := findWidget(m.content, widgetRef) if !ok { return fmt.Errorf("widget %q not found", widgetRef) } @@ -268,6 +268,16 @@ func (m *mcpPageMutator) InsertWidget(widgetRef string, columnRef string, positi if err != nil { return err } + // INSERT INTO: append as children of the target container itself. + if strings.EqualFold(string(position), "into") { + if _, hasChildren := widget["widgets"]; !hasChildren { + return fmt.Errorf("cannot INSERT INTO %q (%v): it has no direct child list — "+ + "use INSERT BEFORE/AFTER a widget inside the target column or tab instead", widgetRef, widget["$Type"]) + } + children, _ := widget["widgets"].([]any) + widget["widgets"] = append(append([]any{}, children...), mapped...) + return nil + } arr, _ := parent[key].([]any) at := idx // The executor passes the AST token ("AFTER"/"BEFORE"), so compare case-insensitively. diff --git a/mdl/backend/mcp/page_mutator_test.go b/mdl/backend/mcp/page_mutator_test.go index 46140dc2a..d81e2c7a9 100644 --- a/mdl/backend/mcp/page_mutator_test.go +++ b/mdl/backend/mcp/page_mutator_test.go @@ -67,6 +67,22 @@ func TestPageMutator_InsertAfterBefore(t *testing.T) { } } +func TestPageMutator_InsertInto(t *testing.T) { + m := newTestMutator() + // INSERT INTO appends as the last child of the container itself (dv), not a sibling. + if err := m.InsertWidget("dv", "", backend.InsertPosition("INTO"), []pages.Widget{dynText("t2")}); err != nil { + t.Fatal(err) + } + if got := dvChildNames(m); len(got) != 2 || got[0] != "t1" || got[1] != "t2" { + t.Fatalf("after INTO insert: %v (want [t1 t2])", got) + } + // Inserting into a non-container (the DynamicText t1) is a clear error. + err := m.InsertWidget("t1", "", backend.InsertPosition("INTO"), []pages.Widget{dynText("x")}) + if err == nil || !strings.Contains(err.Error(), "INSERT INTO") { + t.Fatalf("expected INSERT INTO error for non-container, got %v", err) + } +} + func TestPageMutator_ReplaceAndDrop(t *testing.T) { m := newTestMutator() _ = m.InsertWidget("t1", "", backend.InsertPosition("AFTER"), []pages.Widget{dynText("t2")}) diff --git a/mdl/backend/pagemutator/mutator.go b/mdl/backend/pagemutator/mutator.go index 834a7e1bf..e5f533168 100644 --- a/mdl/backend/pagemutator/mutator.go +++ b/mdl/backend/pagemutator/mutator.go @@ -184,6 +184,21 @@ func (m *Mutator) InsertWidget(widgetRef string, columnRef string, position back return fmt.Errorf("serialize widgets: %w", err) } + // INSERT INTO: append the widgets as children of the target container itself + // (its `Widgets` array), rather than as siblings in the target's parent array. + // Enables inserting into an empty container or as a container's last child. + if strings.EqualFold(string(position), "into") { + newContainer, err := appendChildrenToContainer(result.widget, widgetRef, newBsonWidgets) + if err != nil { + return err + } + // Write the (possibly reallocated — an empty container gains a new Widgets + // field) container doc back into its parent slot. + result.parentArr[result.index] = newContainer + bsonnav.DSetArray(result.parentDoc, result.parentKey, result.parentArr) + return nil + } + insertIdx := result.index if strings.EqualFold(string(position), "after") { insertIdx = result.index + 1 @@ -198,6 +213,78 @@ func (m *Mutator) InsertWidget(widgetRef string, columnRef string, position back return nil } +// insertIntoContainer appends widgets as the last children of a container widget +// (its `Widgets` array). Simple containers (Pages$DivContainer, Forms$Container, +// DataView, GroupBox, ScrollContainer, …) keep their children in a single +// `Widgets` list. LayoutGrid (Rows/Columns) and TabContainer (TabPages) have no +// single child list, so INSERT INTO can't target them directly — the caller is +// told to insert relative to a widget inside the target column/tab instead. +// appendChildrenToContainer appends widgets to a container's `Widgets` list and +// returns the (possibly reallocated) container doc. An empty container omits the +// Widgets field entirely, so it is added — which grows the bson.D slice, hence the +// caller must store the returned doc back into its parent slot. +func appendChildrenToContainer(container bson.D, widgetRef string, newBsonWidgets []any) (bson.D, error) { + if !containerAcceptsWidgets(container) { + typeName := bsonnav.DGetString(container, "$Type") + return nil, fmt.Errorf("cannot INSERT INTO %q (%s): it is not a simple container — "+ + "use INSERT BEFORE/AFTER a widget inside the target column or tab instead", widgetRef, typeName) + } + // Append after any existing children, preserving the Mendix list marker (a + // leading int32). An empty container omits the Widgets field, so create it with + // the default marker (2) that Mendix uses for widget lists. + raw := bsonnav.ToBsonA(bsonnav.DGet(container, "Widgets")) + var out bson.A + switch { + case len(raw) > 0 && isListMarker(raw[0]): + out = append(out, raw...) // marker + existing children + out = append(out, newBsonWidgets...) + case len(raw) == 0: + out = append(out, int32(2)) // fresh (empty container): Mendix widget-list marker + out = append(out, newBsonWidgets...) + default: + out = append(out, raw...) // children without a marker (unusual): keep as-is + out = append(out, newBsonWidgets...) + } + // DSet updates an existing field in place; if Widgets is absent (empty + // container), append the field, growing the slice. + if bsonnav.DSet(container, "Widgets", out) { + return container, nil + } + return append(container, bson.E{Key: "Widgets", Value: out}), nil +} + +// isListMarker reports whether a value is a Mendix list-version marker (a leading int). +func isListMarker(v any) bool { + switch v.(type) { + case int32, int: + return true + } + return false +} + +// containerAcceptsWidgets reports whether a widget keeps its children in a single +// `Widgets` list that INSERT INTO can append to. A container that currently holds +// children always has the field; an empty one omits it, so recognise the common +// simple-container types by $Type. LayoutGrid (Rows/Columns) and TabContainer +// (TabPages) are intentionally excluded — they have no single child list. +func containerAcceptsWidgets(container bson.D) bool { + for _, e := range container { + if e.Key == "Widgets" { + return true + } + } + switch bsonnav.DGetString(container, "$Type") { + case "Forms$DivContainer", + "Forms$Container", + "Forms$DataView", + "Forms$GroupBox", + "Forms$ScrollContainerRegion", + "Forms$Section": + return true + } + return false +} + func (m *Mutator) DropWidget(refs []backend.WidgetRef) error { for _, ref := range refs { // Re-find widget each iteration because previous drops mutate the tree. diff --git a/mdl/executor/cmd_alter_page.go b/mdl/executor/cmd_alter_page.go index dfa4374d0..2dd42870b 100644 --- a/mdl/executor/cmd_alter_page.go +++ b/mdl/executor/cmd_alter_page.go @@ -190,8 +190,14 @@ func applyInsertWidgetMutator(ctx *ExecContext, mutator backend.PageMutator, op return mutator.InsertColumns(op.Target.Widget, op.Target.Column, backend.InsertPosition(op.Position), specs) } - // Find entity context from enclosing DataView/DataGrid/ListView for regular widget insert. + // Find entity context for the new widgets. For INSERT BEFORE/AFTER the target + // is a sibling, so use its enclosing container's context; for INSERT INTO the + // target IS the container, so the children take the target's own context (e.g. + // a dataview's entity). entityCtx := mutator.EnclosingEntity(op.Target.Widget) + if strings.EqualFold(op.Position, "INTO") { + entityCtx = mutator.EnclosingEntityForChildren(op.Target.Widget) + } // Build new widgets from AST widgets, err := buildWidgetsFromAST(ctx, op.Widgets, moduleName, moduleID, entityCtx, mutator) diff --git a/mdl/grammar/MDLParser.g4 b/mdl/grammar/MDLParser.g4 index b4dbb8c3b..d91cae854 100644 --- a/mdl/grammar/MDLParser.g4 +++ b/mdl/grammar/MDLParser.g4 @@ -255,6 +255,7 @@ alterPageAssignment alterPageInsert : INSERT AFTER widgetRef LBRACE pageBodyV3 RBRACE | INSERT BEFORE widgetRef LBRACE pageBodyV3 RBRACE + | INSERT INTO widgetRef LBRACE pageBodyV3 RBRACE // append as children of a container ; alterPageDrop diff --git a/mdl/visitor/visitor_alter_page.go b/mdl/visitor/visitor_alter_page.go index c3b9579d2..82c800034 100644 --- a/mdl/visitor/visitor_alter_page.go +++ b/mdl/visitor/visitor_alter_page.go @@ -145,6 +145,8 @@ func (b *Builder) buildAlterPageInsert(ctx *parser.AlterPageInsertContext) *ast. op.Position = "AFTER" } else if ctx.BEFORE() != nil { op.Position = "BEFORE" + } else if ctx.INTO() != nil { + op.Position = "INTO" } if wr := ctx.WidgetRef(); wr != nil {