Skip to content

Commit e6bd656

Browse files
akoclaude
andcommitted
feat: make datagrid columns addressable in ALTER PAGE via dotted refs
Add widgetRef grammar rule supporting dotted notation (grid.column) in ALTER PAGE operations. Columns inside DataGrid2 are now targetable for SET, INSERT, DROP, and REPLACE using their derived name (attribute short name or caption). Grammar: widgetRef = identifier DOT identifier | identifier AST: WidgetRef type replaces plain string widget references Finder: findBsonColumn navigates Object.Properties["columns"].Objects[] and derives column names matching DESCRIBE output Example: ALTER PAGE Mod.List { DROP WIDGET dgProducts.OldCol }; ALTER PAGE Mod.List { SET Caption = 'New' ON dgProducts.Name }; Closes #113 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 13f28bb commit e6bd656

File tree

10 files changed

+9523
-9011
lines changed

10 files changed

+9523
-9011
lines changed

mdl/ast/ast_alter_page.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,55 @@ type AlterPageOperation interface {
2020
isAlterPageOperation()
2121
}
2222

23-
// SetPropertyOp represents: SET prop = value ON widgetName
24-
// or SET prop = value (page-level, WidgetName empty)
23+
// WidgetRef represents a widget reference, optionally with a sub-element path.
24+
// Plain: "btnSave" (Widget="btnSave", Column="")
25+
// Dotted: "dgProducts.Name" (Widget="dgProducts", Column="Name")
26+
type WidgetRef struct {
27+
Widget string // widget name (always set)
28+
Column string // column name within widget (empty for plain widget refs)
29+
}
30+
31+
// Name returns the full reference string for error messages.
32+
func (r WidgetRef) Name() string {
33+
if r.Column != "" {
34+
return r.Widget + "." + r.Column
35+
}
36+
return r.Widget
37+
}
38+
39+
// IsColumn returns true if this is a column reference (dotted path).
40+
func (r WidgetRef) IsColumn() bool {
41+
return r.Column != ""
42+
}
43+
44+
// SetPropertyOp represents: SET prop = value ON widgetRef
45+
// or SET prop = value (page-level, Target.Widget empty)
2546
type SetPropertyOp struct {
26-
WidgetName string // empty for page-level SET
47+
Target WidgetRef // empty Widget for page-level SET
2748
Properties map[string]interface{} // property name -> value
2849
}
2950

3051
func (s *SetPropertyOp) isAlterPageOperation() {}
3152

32-
// InsertWidgetOp represents: INSERT AFTER/BEFORE widgetName { widgets }
53+
// InsertWidgetOp represents: INSERT AFTER/BEFORE widgetRef { widgets }
3354
type InsertWidgetOp struct {
34-
Position string // "AFTER" or "BEFORE"
35-
TargetName string // widget to insert relative to
36-
Widgets []*WidgetV3
55+
Position string // "AFTER" or "BEFORE"
56+
Target WidgetRef // widget/column to insert relative to
57+
Widgets []*WidgetV3
3758
}
3859

3960
func (s *InsertWidgetOp) isAlterPageOperation() {}
4061

41-
// DropWidgetOp represents: DROP WIDGET name1, name2, ...
62+
// DropWidgetOp represents: DROP WIDGET ref1, ref2, ...
4263
type DropWidgetOp struct {
43-
WidgetNames []string
64+
Targets []WidgetRef
4465
}
4566

4667
func (s *DropWidgetOp) isAlterPageOperation() {}
4768

48-
// ReplaceWidgetOp represents: REPLACE widgetName WITH { widgets }
69+
// ReplaceWidgetOp represents: REPLACE widgetRef WITH { widgets }
4970
type ReplaceWidgetOp struct {
50-
WidgetName string
71+
Target WidgetRef
5172
NewWidgets []*WidgetV3
5273
}
5374

mdl/executor/alter_page_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func TestApplyDropWidget_Single(t *testing.T) {
9595
w3 := makeWidget("txtPhone", "Pages$TextBox")
9696
rawData := makeRawPage(w1, w2, w3)
9797

98-
op := &ast.DropWidgetOp{WidgetNames: []string{"txtEmail"}}
98+
op := &ast.DropWidgetOp{Targets: []ast.WidgetRef{{Widget: "txtEmail"}}}
9999
if err := applyDropWidget(rawData, op); err != nil {
100100
t.Fatalf("applyDropWidget failed: %v", err)
101101
}
@@ -126,7 +126,7 @@ func TestApplyDropWidget_Multiple(t *testing.T) {
126126
w3 := makeWidget("c", "Pages$TextBox")
127127
rawData := makeRawPage(w1, w2, w3)
128128

129-
op := &ast.DropWidgetOp{WidgetNames: []string{"a", "c"}}
129+
op := &ast.DropWidgetOp{Targets: []ast.WidgetRef{{Widget: "a"}, {Widget: "c"}}}
130130
if err := applyDropWidget(rawData, op); err != nil {
131131
t.Fatalf("applyDropWidget failed: %v", err)
132132
}
@@ -150,7 +150,7 @@ func TestApplyDropWidget_NotFound(t *testing.T) {
150150
w1 := makeWidget("txtName", "Pages$TextBox")
151151
rawData := makeRawPage(w1)
152152

153-
op := &ast.DropWidgetOp{WidgetNames: []string{"nonexistent"}}
153+
op := &ast.DropWidgetOp{Targets: []ast.WidgetRef{{Widget: "nonexistent"}}}
154154
err := applyDropWidget(rawData, op)
155155
if err == nil {
156156
t.Fatal("Expected error for nonexistent widget")
@@ -163,7 +163,7 @@ func TestApplyDropWidget_Nested(t *testing.T) {
163163
container := makeContainerWidget("ctn1", inner1, inner2)
164164
rawData := makeRawPage(container)
165165

166-
op := &ast.DropWidgetOp{WidgetNames: []string{"txtInner1"}}
166+
op := &ast.DropWidgetOp{Targets: []ast.WidgetRef{{Widget: "txtInner1"}}}
167167
if err := applyDropWidget(rawData, op); err != nil {
168168
t.Fatalf("applyDropWidget failed: %v", err)
169169
}
@@ -186,7 +186,7 @@ func TestApplySetProperty_Name(t *testing.T) {
186186
rawData := makeRawPage(w1)
187187

188188
op := &ast.SetPropertyOp{
189-
WidgetName: "txtOld",
189+
Target: ast.WidgetRef{Widget: "txtOld"},
190190
Properties: map[string]interface{}{
191191
"Name": "txtNew",
192192
},
@@ -211,7 +211,7 @@ func TestApplySetProperty_ButtonStyle(t *testing.T) {
211211
rawData := makeRawPage(w1)
212212

213213
op := &ast.SetPropertyOp{
214-
WidgetName: "btnSave",
214+
Target: ast.WidgetRef{Widget: "btnSave"},
215215
Properties: map[string]interface{}{
216216
"ButtonStyle": "Success",
217217
},
@@ -234,7 +234,7 @@ func TestApplySetProperty_WidgetNotFound(t *testing.T) {
234234
rawData := makeRawPage(w1)
235235

236236
op := &ast.SetPropertyOp{
237-
WidgetName: "nonexistent",
237+
Target: ast.WidgetRef{Widget: "nonexistent"},
238238
Properties: map[string]interface{}{
239239
"Name": "new",
240240
},
@@ -279,7 +279,7 @@ func TestApplySetProperty_PluggableWidget(t *testing.T) {
279279
rawData := makeRawPage(w1)
280280

281281
op := &ast.SetPropertyOp{
282-
WidgetName: "cb1",
282+
Target: ast.WidgetRef{Widget: "cb1"},
283283
Properties: map[string]interface{}{
284284
"showLabel": false,
285285
},
@@ -448,7 +448,7 @@ func TestApplyDropWidget_Snippet(t *testing.T) {
448448
w2 := makeWidget("txtEmail", "Pages$TextBox")
449449
rawData := makeRawSnippet(w1, w2)
450450

451-
op := &ast.DropWidgetOp{WidgetNames: []string{"txtEmail"}}
451+
op := &ast.DropWidgetOp{Targets: []ast.WidgetRef{{Widget: "txtEmail"}}}
452452
if err := applyDropWidgetWith(rawData, op, findBsonWidgetInSnippet); err != nil {
453453
t.Fatalf("applyDropWidgetWith failed: %v", err)
454454
}
@@ -473,7 +473,7 @@ func TestApplySetProperty_Snippet(t *testing.T) {
473473
rawData := makeRawSnippet(w1)
474474

475475
op := &ast.SetPropertyOp{
476-
WidgetName: "btnAction",
476+
Target: ast.WidgetRef{Widget: "btnAction"},
477477
Properties: map[string]interface{}{
478478
"ButtonStyle": "Danger",
479479
},

0 commit comments

Comments
 (0)