Skip to content

Commit 6a0b41f

Browse files
akoclaude
andcommitted
feat: add VISIBLE IF, EDITABLE IF, TabletWidth, PhoneWidth (#32)
P0 functional property gaps from issue #32: 1. VISIBLE IF 'expression' — conditional visibility on any widget. Writes ConditionalVisibilitySettings to BSON via post-serialization patching in serializeWidget(), avoiding changes to 20+ serializers. 2. EDITABLE IF 'expression' — conditional editability on input widgets. Sets Editable to "Conditional" and writes ConditionalEditabilitySettings to BSON. 3. TabletWidth/PhoneWidth on COLUMN — numeric responsive weights (1-12) alongside DesktopWidth. Defaults to -1 (auto) when not specified. All three properties round-trip through DESCRIBE and are verified in BSON dumps against real 11.8.0 projects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3e0fef5 commit 6a0b41f

19 files changed

+8228
-7829
lines changed

cmd/mxcli/lsp_completions_gen.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mdl/executor/cmd_pages_builder_v3.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,9 +344,45 @@ func (pb *pageBuilder) buildWidgetV3(w *ast.WidgetV3) (pages.Widget, error) {
344344
// Apply Class/Style appearance properties to the widget
345345
applyWidgetAppearance(widget, w, pb.themeRegistry)
346346

347+
// Apply conditional visibility/editability
348+
applyConditionalSettings(widget, w)
349+
347350
return widget, nil
348351
}
349352

353+
// applyConditionalSettings sets ConditionalVisibility and ConditionalEditability
354+
// on a widget if VISIBLE IF or EDITABLE IF properties are specified in the AST.
355+
func applyConditionalSettings(widget pages.Widget, w *ast.WidgetV3) {
356+
type baseWidgetGetter interface {
357+
GetBaseWidget() *pages.BaseWidget
358+
}
359+
bwg, ok := widget.(baseWidgetGetter)
360+
if !ok {
361+
return
362+
}
363+
bw := bwg.GetBaseWidget()
364+
365+
if visibleIf := w.GetStringProp("VisibleIf"); visibleIf != "" {
366+
bw.ConditionalVisibility = &pages.ConditionalVisibilitySettings{
367+
BaseElement: model.BaseElement{
368+
ID: model.ID(mpr.GenerateID()),
369+
TypeName: "Forms$ConditionalVisibilitySettings",
370+
},
371+
Expression: visibleIf,
372+
}
373+
}
374+
375+
if editableIf := w.GetStringProp("EditableIf"); editableIf != "" {
376+
bw.ConditionalEditability = &pages.ConditionalEditabilitySettings{
377+
BaseElement: model.BaseElement{
378+
ID: model.ID(mpr.GenerateID()),
379+
TypeName: "Forms$ConditionalEditabilitySettings",
380+
},
381+
Expression: editableIf,
382+
}
383+
}
384+
}
385+
350386
// applyWidgetAppearance sets Class, Style, and DesignProperties on a widget if specified in the AST.
351387
// The theme registry (if non-nil) is used to determine the correct BSON type for each design property.
352388
func applyWidgetAppearance(widget pages.Widget, w *ast.WidgetV3, theme *ThemeRegistry) {

mdl/executor/cmd_pages_builder_v3_layout.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,30 @@ func (pb *pageBuilder) buildLayoutGridColumnV3(w *ast.WidgetV3) (*pages.LayoutGr
8080
}
8181
}
8282

83+
// Handle TabletWidth
84+
if tw := w.Properties["TabletWidth"]; tw != nil {
85+
switch v := tw.(type) {
86+
case int:
87+
col.TabletWeight = v
88+
case string:
89+
if strings.ToUpper(v) == "AUTOFILL" {
90+
col.TabletWeight = -1
91+
}
92+
}
93+
}
94+
95+
// Handle PhoneWidth
96+
if pw := w.Properties["PhoneWidth"]; pw != nil {
97+
switch v := pw.(type) {
98+
case int:
99+
col.PhoneWeight = v
100+
case string:
101+
if strings.ToUpper(v) == "AUTOFILL" {
102+
col.PhoneWeight = -1
103+
}
104+
}
105+
}
106+
83107
// Build child widgets
84108
for _, child := range w.Children {
85109
widget, err := pb.buildWidgetV3(child)

mdl/executor/cmd_pages_describe.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,9 @@ type rawWidget struct {
526526
// GroupBox properties
527527
Collapsible string // "No", "YesInitiallyExpanded", "YesInitiallyCollapsed"
528528
HeaderMode string // "Div", "H1"-"H6"
529+
// Conditional visibility/editability
530+
VisibleIf string // Expression from ConditionalVisibilitySettings
531+
EditableIf string // Expression from ConditionalEditabilitySettings
529532
// Design properties from Appearance
530533
DesignProperties []rawDesignProp
531534
}
@@ -542,8 +545,10 @@ type rawWidgetRow struct {
542545
}
543546

544547
type rawWidgetColumn struct {
545-
Width int
546-
Widgets []rawWidget
548+
Width int
549+
TabletWidth int
550+
PhoneWidth int
551+
Widgets []rawWidget
547552
}
548553

549554
// toBsonArray converts various BSON array types to []interface{}.

mdl/executor/cmd_pages_describe_output.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,18 @@ func appendDataGridPagingProps(props []string, w rawWidget) []string {
3636
return props
3737
}
3838

39-
// appendAppearanceProps appends Class, Style, and DesignProperties if present.
39+
// appendConditionalProps appends VISIBLE IF and EDITABLE IF if present.
40+
func appendConditionalProps(props []string, w rawWidget) []string {
41+
if w.VisibleIf != "" {
42+
props = append(props, fmt.Sprintf("VISIBLE IF %s", mdlQuote(w.VisibleIf)))
43+
}
44+
if w.EditableIf != "" {
45+
props = append(props, fmt.Sprintf("EDITABLE IF %s", mdlQuote(w.EditableIf)))
46+
}
47+
return props
48+
}
49+
50+
// appendAppearanceProps appends Class, Style, DesignProperties, and conditional settings if present.
4051
func appendAppearanceProps(props []string, w rawWidget) []string {
4152
if w.Class != "" {
4253
props = append(props, fmt.Sprintf("Class: %s", mdlQuote(w.Class)))
@@ -47,6 +58,12 @@ func appendAppearanceProps(props []string, w rawWidget) []string {
4758
if len(w.DesignProperties) > 0 {
4859
props = append(props, formatDesignPropertiesMDL(w.DesignProperties))
4960
}
61+
if w.VisibleIf != "" {
62+
props = append(props, fmt.Sprintf("VISIBLE IF %s", mdlQuote(w.VisibleIf)))
63+
}
64+
if w.EditableIf != "" {
65+
props = append(props, fmt.Sprintf("EDITABLE IF %s", mdlQuote(w.EditableIf)))
66+
}
5067
return props
5168
}
5269

@@ -151,11 +168,19 @@ func (e *Executor) outputWidgetMDLV3(w rawWidget, indent int) {
151168
for rowIdx, row := range w.Rows {
152169
fmt.Fprintf(e.output, "%s ROW row%d {\n", prefix, rowIdx+1)
153170
for colIdx, col := range row.Columns {
171+
var colProps []string
154172
widthStr := "AutoFill"
155173
if col.Width > 0 && col.Width <= 12 {
156174
widthStr = fmt.Sprintf("%d", col.Width)
157175
}
158-
fmt.Fprintf(e.output, "%s COLUMN col%d (DesktopWidth: %s) {\n", prefix, colIdx+1, widthStr)
176+
colProps = append(colProps, "DesktopWidth: "+widthStr)
177+
if col.TabletWidth > 0 && col.TabletWidth <= 12 {
178+
colProps = append(colProps, fmt.Sprintf("TabletWidth: %d", col.TabletWidth))
179+
}
180+
if col.PhoneWidth > 0 && col.PhoneWidth <= 12 {
181+
colProps = append(colProps, fmt.Sprintf("PhoneWidth: %d", col.PhoneWidth))
182+
}
183+
fmt.Fprintf(e.output, "%s COLUMN col%d (%s) {\n", prefix, colIdx+1, strings.Join(colProps, ", "))
159184
for _, cw := range col.Widgets {
160185
e.outputWidgetMDLV3(cw, indent+3)
161186
}

mdl/executor/cmd_pages_describe_parse.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ import (
99
)
1010

1111
// parseRawWidget parses a raw widget map into rawWidget structs.
12+
// extractConditionalSettings extracts ConditionalVisibility/Editability from raw BSON.
13+
func extractConditionalSettings(widget *rawWidget, w map[string]any) {
14+
if cvs, ok := w["ConditionalVisibilitySettings"].(map[string]any); ok && cvs != nil {
15+
if expr, ok := cvs["Expression"].(string); ok && expr != "" {
16+
widget.VisibleIf = expr
17+
}
18+
}
19+
if ces, ok := w["ConditionalEditabilitySettings"].(map[string]any); ok && ces != nil {
20+
if expr, ok := ces["Expression"].(string); ok && expr != "" {
21+
widget.EditableIf = expr
22+
}
23+
}
24+
}
25+
1226
func (e *Executor) parseRawWidget(w map[string]any) []rawWidget {
1327
typeName, _ := w["$Type"].(string)
1428
name, _ := w["Name"].(string)
@@ -46,6 +60,7 @@ func (e *Executor) parseRawWidget(w map[string]any) []rawWidget {
4660
widget.HeaderMode = headerMode
4761
}
4862
}
63+
extractConditionalSettings(&widget, w)
4964
children := getBsonArrayElements(w["Widgets"])
5065
if children != nil {
5166
for _, c := range children {
@@ -61,6 +76,7 @@ func (e *Executor) parseRawWidget(w map[string]any) []rawWidget {
6176
Type: typeName,
6277
Name: name,
6378
}
79+
extractConditionalSettings(&widget, w)
6480

6581
// Extract CSS class, style, and design properties from Appearance
6682
if appearance, ok := w["Appearance"].(map[string]any); ok {
@@ -235,6 +251,12 @@ func (e *Executor) parseLayoutGridRows(w map[string]any) []rawWidgetRow {
235251
} else if weight, ok := cMap["DesktopWeight"].(int32); ok {
236252
col.Width = int(weight)
237253
}
254+
if tw, ok := cMap["TabletWeight"].(int32); ok {
255+
col.TabletWidth = int(tw)
256+
}
257+
if pw, ok := cMap["PhoneWeight"].(int32); ok {
258+
col.PhoneWidth = int(pw)
259+
}
238260
// Get widgets
239261
colWidgets := getBsonArrayElements(cMap["Widgets"])
240262
for _, cw := range colWidgets {

mdl/grammar/MDLLexer.g4

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,8 @@ CAPTIONPARAMS: C A P T I O N P A R A M S;
290290
PARAMS: P A R A M S;
291291
VARIABLES_KW: V A R I A B L E S;
292292
DESKTOPWIDTH: D E S K T O P W I D T H;
293+
TABLETWIDTH: T A B L E T W I D T H;
294+
PHONEWIDTH: P H O N E W I D T H;
293295
CLASS: C L A S S;
294296
STYLE: S T Y L E;
295297
BUTTONSTYLE: B U T T O N S T Y L E;

mdl/grammar/MDLParser.g4

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1815,6 +1815,8 @@ widgetPropertyV3
18151815
| CLASS COLON STRING_LITERAL // Class: 'my-class'
18161816
| STYLE COLON STRING_LITERAL // Style: 'color: red'
18171817
| DESKTOPWIDTH COLON desktopWidthV3 // DesktopWidth: 6 | AutoFill
1818+
| TABLETWIDTH COLON desktopWidthV3 // TabletWidth: 6 | AutoFill
1819+
| PHONEWIDTH COLON desktopWidthV3 // PhoneWidth: 12 | AutoFill
18181820
// Where: and OrderBy: removed — use inline WHERE/SORT BY in DataSource: expression
18191821
| SELECTION COLON selectionModeV3 // Selection: Single | Multiple
18201822
| SNIPPET COLON qualifiedName // Snippet: Module.SnippetName
@@ -1823,7 +1825,9 @@ widgetPropertyV3
18231825
| DESIGNPROPERTIES COLON designPropertyListV3 // DesignProperties: [...]
18241826
| WIDTH COLON NUMBER_LITERAL // Width: 200
18251827
| HEIGHT COLON NUMBER_LITERAL // Height: 100
1828+
| VISIBLE IF STRING_LITERAL // VISIBLE IF '$currentObject/IsActive = true'
18261829
| VISIBLE COLON propertyValueV3 // Visible: expression
1830+
| EDITABLE IF STRING_LITERAL // EDITABLE IF '$currentObject/Status = Draft'
18271831
| TOOLTIP COLON propertyValueV3 // Tooltip: 'text'
18281832
| IDENTIFIER COLON propertyValueV3 // Generic: any other property
18291833
;
@@ -3156,6 +3160,7 @@ commonNameKeyword
31563160
| FORMAT | RANGE | SOURCE_KW | CHECK // Validation/data keywords
31573161
| FOLDER | NAVIGATION | HOME | VERSION | PRODUCTION // Structure/config keywords
31583162
| SELECTION | EDITABLE | VISIBLE | DATASOURCE // Widget property keywords
3163+
| TABLETWIDTH | PHONEWIDTH // Responsive width keywords
31593164
| WIDTH | HEIGHT | STYLE | CLASS // Styling keywords
31603165
| BOTH | SINGLE | MULTIPLE | NONE // Cardinality keywords
31613166
| PROTOTYPE | OFF // Security level keywords

mdl/grammar/parser/MDLLexer.interp

Lines changed: 7 additions & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)