From b026aec162ba9ddbce79cda55c4f49c425776c23 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 1 Oct 2026 21:10:09 +0200 Subject: [PATCH] fix(09): WR-07 keep relation column order from the YAML itself, not a fixed indentation --- modules/cabana/relation.go | 23 +---------- modules/cabana/relation_test.go | 71 +++++++++++++++++++++++++++++++++ modules/cabana/schema.go | 8 ++++ 3 files changed, 80 insertions(+), 22 deletions(-) diff --git a/modules/cabana/relation.go b/modules/cabana/relation.go index 7ebc1c6..b9084cc 100644 --- a/modules/cabana/relation.go +++ b/modules/cabana/relation.go @@ -7,7 +7,6 @@ import ( "fmt" "io/fs" "reflect" - "sort" "strings" "git.golem15.com/golem15/summercms/modules/lagoon" @@ -212,7 +211,7 @@ func compileRelations(pluginID string, ctl pact.AdminController, fsys fs.FS, for return nil, bootErr(pluginID, ctl.ID(), file, err) } var root yaml.MapSlice - if err := decodeStrict(raw, &root); err != nil { + if err := decodeStrictOrdered(raw, &root); err != nil { return nil, bootErr(pluginID, ctl.ID(), file, err) } out := map[string]*CompiledRelation{} @@ -247,8 +246,6 @@ func compileRelations(pluginID string, ctl pact.AdminController, fsys fs.FS, for if err != nil { return nil, bootErr(pluginID, ctl.ID(), file, fmt.Errorf("relation %s manage: %w", name, err)) } - orderRelationColumns(raw, &view) - orderRelationColumns(raw, &manage) schema := &RelationSchema{Name: name, Label: doc.Label, View: view, Manage: manage} if doc.Messages != nil { schema.messageKeys = *doc.Messages @@ -274,24 +271,6 @@ func compileRelations(pluginID string, ctl pact.AdminController, fsys fs.FS, for return out, nil } -func orderRelationColumns(raw []byte, panel *RelationPanel) { - if panel == nil || len(panel.List.Columns) < 2 { - return - } - source := string(raw) - sort.SliceStable(panel.List.Columns, func(i, j int) bool { - left := strings.Index(source, "\n "+panel.List.Columns[i].Key+":") - right := strings.Index(source, "\n "+panel.List.Columns[j].Key+":") - if left < 0 { - left = len(source) - } - if right < 0 { - right = len(source) - } - return left < right - }) -} - func compileRelationPanel(doc relationPanelDocument, contract RelationContract, view bool) (RelationPanel, error) { cols := make([]RelationColumn, 0, len(doc.List.Columns)) seen := map[string]struct{}{} diff --git a/modules/cabana/relation_test.go b/modules/cabana/relation_test.go index 36c07a5..ad8c03a 100644 --- a/modules/cabana/relation_test.go +++ b/modules/cabana/relation_test.go @@ -115,6 +115,77 @@ func TestRelationDefaultSort(t *testing.T) { } } +// TestRelationColumnOrderIsIndependentOfIndentation pins WR-07: column order +// is the file's order in each panel, whatever the indentation, and the view and +// manage panels do not share one order. +func TestRelationColumnOrderIsIndependentOfIndentation(t *testing.T) { + const twoSpace = `editors: + label: demo.editors + view: + list: + columns: + username: + label: demo.username + email: + label: demo.email + toolbarButtons: link|unlink + manage: + list: + columns: + username: + label: demo.username + email: + label: demo.email +` + for name, yamlText := range map[string]string{"two-space": twoSpace, "four-space": validRelationYAML} { + t.Run(name, func(t *testing.T) { + ctl := relationTestController{} + fsys := relationTestFS(yamlText) + form, err := CompileForm("acme.demo", ctl, fsys) + if err != nil { + t.Fatal(err) + } + relations, err := compileRelations("acme.demo", ctl, fsys, form) + if err != nil { + t.Fatal(err) + } + schema := relations["editors"].Schema + for panel, cols := range map[string][]RelationColumn{"view": schema.View.List.Columns, "manage": schema.Manage.List.Columns} { + if len(cols) != 2 || cols[0].Key != "username" || cols[1].Key != "email" { + t.Fatalf("%s columns = %+v, want username then email (file order, not alphabetical)", panel, cols) + } + } + }) + } + + const reversed = `editors: + view: + list: + columns: + username: {label: demo.username} + email: {label: demo.email} + manage: + list: + columns: + email: {label: demo.email} + username: {label: demo.username} +` + ctl := relationTestController{} + fsys := relationTestFS(reversed) + form, err := CompileForm("acme.demo", ctl, fsys) + if err != nil { + t.Fatal(err) + } + relations, err := compileRelations("acme.demo", ctl, fsys, form) + if err != nil { + t.Fatal(err) + } + schema := relations["editors"].Schema + if v, m := schema.View.List.Columns, schema.Manage.List.Columns; v[0].Key != "username" || m[0].Key != "email" { + t.Fatalf("view=%+v manage=%+v, want each panel in its own file order", v, m) + } +} + func TestRelationCandidateExclusions(t *testing.T) { contract := relationTestController{}.AdminRelationContracts()[0] ids, err := contract.ExcludedRelatedIDs(&relationTestOwner{OwnerID: 41}) diff --git a/modules/cabana/schema.go b/modules/cabana/schema.go index a1f52aa..65b694c 100644 --- a/modules/cabana/schema.go +++ b/modules/cabana/schema.go @@ -18,6 +18,14 @@ func decodeStrict(raw []byte, dest any) error { return nil } +// decodeStrictOrdered is decodeStrict with every nested mapping kept as an +// ordered yaml.MapSlice, so a file that is decoded, re-marshalled per entry and +// decoded again keeps its key order at any depth and any indentation. +func decodeStrictOrdered(raw []byte, dest any) error { + dec := yaml.NewDecoder(bytes.NewReader(raw), yaml.DisallowUnknownField(), yaml.UseOrderedMap()) + return dec.Decode(dest) +} + func readAsset(fsys fs.FS, name string) ([]byte, error) { name = path.Clean(name) if name == "." || strings.HasPrefix(name, "..") || strings.Contains(name, "..") {