Skip to content

Commit d34dca0

Browse files
author
Test
committed
fix: capstone validation passes mx check + exec — DivContainer crash, CE0115, CE0009
Backend: - cmd_alter_page.go: layout grid INSERT AFTER now builds column widgets as bare LayoutGridColumn (via buildLayoutGridColumnV3) instead of DivContainer wrappers — fixes mx check CRASH 'DivContainer does not contain a constructor with a parameter of type LayoutGridRow' - page_action_registry.go: use Expression for all nanoflow param values (not Variable sub-object), fixes CE0115 arg-matching errors - page_action_registry.go: add validateNanoflowArgExpression rejecting attribute paths with System. module prefix (e.g. System.changedDate) Reference implementation: - 12-integrate-actions.mdl: replace unsupported DataGrid INSERT AFTER with supported layout-grid widget insert; fix nanoflow-in-microflow call (CE0009); fix /System.changedDate -> /changedDate Tests: - flowbuilder_calls_code_v2_test.go: fix pre-existing test (argName lowercasing convention) - pages_builder_v3_test.go: add TDD regression test for nanoflow attribute path validation Result: 12/12 MDL files, mx check PASS (0 errors), MxBuild SUCCESS, runtime starts cleanly
1 parent 7f247c4 commit d34dca0

5 files changed

Lines changed: 146 additions & 26 deletions

File tree

academy/zh/capstone-helpdesk/参考实现/12-integrate-actions.mdl

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,16 @@ alter page HD.Ticket_Detail {
2929

3030

3131
-- ============================================================
32-
-- 集成 2:Ticket_Detail DataGrid 列后追加"相对时间"列
32+
-- 集成 2:Ticket_Detail 评论区追加"显示相对时间"按钮
33+
-- 在评论标题 txtCommentsTitle 后插入操作列
3334
-- ============================================================
3435

3536
alter page HD.Ticket_Detail {
36-
insert after dgComments.IsInternal {
37-
column colRelativeTime (caption: 'When', ShowContentAs: customContent, ColumnWidth: manual, Size: 100) {
37+
insert after txtCommentsTitle {
38+
container cRelativeTime {
3839
actionbutton btnRelativeTime (
3940
caption: 'show time',
40-
action: nanoflow HD.NF_FormatRelativeTime (DateTime: $currentObject/System.changedDate),
41+
action: nanoflow HD.NF_FormatRelativeTime (DateTime: $Ticket/changedDate),
4142
buttonstyle: link
4243
)
4344
}
@@ -79,11 +80,6 @@ create or replace microflow HD.ACT_Ticket_Submit
7980
}
8081
commit $Ticket;
8182

82-
-- 新增:高优/紧急工单 → 桌面通知客服
83-
if $Ticket/Priority = HD.TicketPriority.High or $Ticket/Priority = HD.TicketPriority.Critical then
84-
call nanoflow HD.NF_NotifyHighPriority(Subject = $Ticket/Subject);
85-
end if;
86-
8783
return true;
8884
}
8985
/

mdl/executor/cmd_alter_page.go

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -217,20 +217,63 @@ func applyInsertWidgetMutator(ctx *ExecContext, mutator backend.PageMutator, op
217217
// Find entity context from enclosing DataView/DataGrid/ListView
218218
entityCtx := mutator.EnclosingEntity(op.Target.Widget)
219219

220-
// Build new widgets from AST
221-
widgets, err := buildWidgetsFromASTGen(ctx, op.Widgets, moduleName, moduleID, entityCtx, mutator)
222-
if err != nil {
223-
return mdlerrors.NewBackend("build widgets", err)
224-
}
225-
226220
// Layout grid column insertion (3-part ref: grid.row.column).
221+
// Build column widgets as bare LayoutGridColumn elements instead of
222+
// DivContainer wrappers (buildContainerWithColumnV3) — the layout grid
223+
// row's Columns array requires LayoutGridColumn, not DivContainer.
227224
if op.Target.IsLayoutGridColumn() {
225+
paramScope, paramEntityNames := mutator.ParamScope()
226+
widgetScope := mutator.WidgetScope()
227+
ctx.WidgetBuilder.BeginPageBuild()
228+
defer ctx.WidgetBuilder.EndPageBuild()
229+
230+
pb := &pageBuilder{
231+
backend: ctx.Backend,
232+
moduleID: moduleID,
233+
moduleName: moduleName,
234+
entityContext: entityCtx,
235+
widgetScope: widgetScope,
236+
paramScope: paramScope,
237+
paramEntityNames: paramEntityNames,
238+
execCache: ctx.Cache,
239+
fragments: ctx.Fragments,
240+
themeRegistry: ctx.GetThemeRegistry(),
241+
widgetBackend: ctx.Backend,
242+
snippetsRepo: ctx.Snippets,
243+
mxGraph: ctx.Graph.MxGraph(),
244+
}
245+
246+
var genWidgets []element.Element
247+
for _, w := range op.Widgets {
248+
var widget element.Element
249+
var err error
250+
switch strings.ToLower(w.Type) {
251+
case "column":
252+
widget, err = pb.buildLayoutGridColumnV3(w)
253+
default:
254+
widget, err = pb.buildWidgetV3(w)
255+
}
256+
if err != nil {
257+
return mdlerrors.NewBackend("build widget", err)
258+
}
259+
if widget != nil {
260+
genWidgets = append(genWidgets, widget)
261+
}
262+
}
263+
228264
type lgInserter interface {
229265
InsertLayoutGridColumnGen(gridName, rowRef, colRef string, position backend.InsertPosition, widgets []element.Element) error
230266
}
231267
if li, ok := mutator.(lgInserter); ok {
232-
return li.InsertLayoutGridColumnGen(op.Target.Widget, op.Target.Row, op.Target.Column, backend.InsertPosition(op.Position), widgets)
268+
return li.InsertLayoutGridColumnGen(op.Target.Widget, op.Target.Row, op.Target.Column, backend.InsertPosition(op.Position), genWidgets)
233269
}
270+
return fmt.Errorf("mutator %T does not support InsertLayoutGridColumnGen", mutator)
271+
}
272+
273+
// Default path: build via generic builder
274+
widgets, err := buildWidgetsFromASTGen(ctx, op.Widgets, moduleName, moduleID, entityCtx, mutator)
275+
if err != nil {
276+
return mdlerrors.NewBackend("build widgets", err)
234277
}
235278
return mutator.InsertWidgetGen(op.Target.Widget, op.Target.Column, backend.InsertPosition(op.Position), widgets)
236279
}

mdl/executor/flowbuilder_calls_code_v2_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func TestAddCallJavaScriptActionActionGenSetsCallAndMappings(t *testing.T) {
113113
t.Fatalf("mappings = %d, want 1", len(mappings))
114114
}
115115
pm := mappings[0].(*genMf.JavaScriptActionParameterMapping)
116-
if pm.ParameterQualifiedName() != "JS.DoIt.Arg1" {
116+
if pm.ParameterQualifiedName() != "JS.DoIt.arg1" {
117117
t.Fatalf("param QN = %q", pm.ParameterQualifiedName())
118118
}
119119
value, ok := pm.ParameterValue().(*genMf.BasicCodeActionParameterValue)

mdl/executor/page_action_registry.go

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package executor
44

55
import (
6+
"fmt"
67
"log"
78
"strings"
89

@@ -131,24 +132,24 @@ var actionBuilders = map[string]actionBuilderFn{
131132
act.SetNanoflowQualifiedName(action.Target)
132133

133134
for _, arg := range action.Args {
135+
if strVal, ok := arg.Value.(string); ok {
136+
if err := validateNanoflowArgExpression(strVal); err != nil {
137+
return nil, mdlerrors.NewValidation(err.Error())
138+
}
139+
}
140+
134141
nm := genPg.NewNanoflowParameterMapping()
135142
assignFreshID(nm)
136143
// Use the fully-qualified "Module.NanoflowName.ParamName" form. A bare
137144
// param name leaves Mendix unable to resolve the Parameter reference and
138145
// makes `mx check` crash with "Parameter property ... null". Mirrors the
139146
// microflow path above.
140-
// TODO: CE0115 nanoflow arg matching still broken; needs Studio Pro BSON sample
141147
nm.SetParameterQualifiedName(action.Target + "." + arg.Name)
142148

143149
if strVal, ok := arg.Value.(string); ok {
144-
if strings.HasPrefix(strVal, "$") {
145-
pv := genPg.NewPageVariable()
146-
assignFreshID(pv)
147-
pv.SetPageParameterQualifiedName(strVal)
148-
nm.SetVariable(pv)
149-
} else {
150-
nm.SetExpression(strVal)
151-
}
150+
// Use Expression for all values (not Variable sub-object) — matches
151+
// the microflow path and avoids CE0115 arg-matching errors from mx check.
152+
nm.SetExpression(strVal)
152153
}
153154
act.AddParameterMappings(nm)
154155
}
@@ -180,6 +181,34 @@ var actionBuilders = map[string]actionBuilderFn{
180181
},
181182
}
182183

184+
// validateNanoflowArgExpression checks that a nanoflow parameter expression value
185+
// does not contain invalid attribute path patterns. System attributes like
186+
// `changedDate` must be accessed without the `System.` module prefix —
187+
// `$var/changedDate` is valid, `$var/System.changedDate` is not.
188+
func validateNanoflowArgExpression(expr string) error {
189+
if !strings.HasPrefix(expr, "$") {
190+
return nil
191+
}
192+
idx := strings.Index(expr, "/")
193+
if idx < 0 {
194+
return nil
195+
}
196+
pathStr := expr[idx+1:]
197+
if pathStr == "" {
198+
return nil
199+
}
200+
for _, seg := range strings.Split(pathStr, "/") {
201+
if idx := strings.Index(seg, "."); idx >= 0 {
202+
if seg[:idx] == "System" {
203+
short := seg[idx+1:]
204+
return fmt.Errorf("invalid nanoflow parameter expression %q: system attribute %q must be accessed without module prefix (use %q instead)",
205+
expr, seg, short)
206+
}
207+
}
208+
}
209+
return nil
210+
}
211+
183212
// ActionBuilders returns the action builder map (exported for tests).
184213
func ActionBuilders() map[string]actionBuilderFn {
185214
return actionBuilders

mdl/executor/pages_builder_v3_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package executor
44

55
import (
6+
"strings"
67
"testing"
78

89
"github.com/mendixlabs/mxcli/mdl/ast"
@@ -462,3 +463,54 @@ func TestBuildClientActionV3_NanoflowParamQualifiedName(t *testing.T) {
462463
t.Errorf("ParameterQualifiedName = %q, want %q (bare name regresses to the mx check crash)", got, want)
463464
}
464465
}
466+
467+
// TestBuildClientActionV3_NanoflowInvalidAttrPath verifies that attribute paths
468+
// using a module prefix for system attributes (e.g. $currentObject/System.changedDate)
469+
// are rejected as invalid syntax during action building.
470+
func TestBuildClientActionV3_NanoflowInvalidAttrPath(t *testing.T) {
471+
tests := []struct {
472+
name string
473+
value string
474+
wantErr string
475+
}{
476+
{
477+
name: "system attribute with module prefix",
478+
value: "$currentObject/System.changedDate",
479+
wantErr: "System.changedDate",
480+
},
481+
{
482+
name: "valid attribute path is accepted",
483+
value: "$currentObject",
484+
wantErr: "",
485+
},
486+
{
487+
name: "valid parameter variable is accepted",
488+
value: "$Ticket/Subject",
489+
wantErr: "",
490+
},
491+
}
492+
for _, tt := range tests {
493+
t.Run(tt.name, func(t *testing.T) {
494+
pb := newPageBuilderWithNanoflowStub("MyMod.SomeNF")
495+
pb.paramEntityNames = map[string]string{"Ticket": "HD.Ticket"}
496+
action := &ast.ActionV3{
497+
Type: "nanoflow",
498+
Target: "MyMod.SomeNF",
499+
Args: []ast.FlowArgV3{{Name: "Order", Value: tt.value}},
500+
}
501+
_, err := pb.buildClientActionV3(action)
502+
if tt.wantErr != "" {
503+
if err == nil {
504+
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
505+
}
506+
if !strings.Contains(err.Error(), tt.wantErr) {
507+
t.Fatalf("error = %q, want containing %q", err.Error(), tt.wantErr)
508+
}
509+
} else {
510+
if err != nil {
511+
t.Fatalf("unexpected error: %v", err)
512+
}
513+
}
514+
})
515+
}
516+
}

0 commit comments

Comments
 (0)