Compare commits

...

2 Commits

Author SHA1 Message Date
Jakub Zych
7554241e9d docs(planning): log to-many list column fix 2026-10-07 18:43:18 +02:00
Jakub Zych
fa25b4e000 fix(cabana): show to-many relation list columns joined with commas 2026-10-07 18:43:17 +02:00
4 changed files with 76 additions and 5 deletions

View File

@@ -5,10 +5,10 @@ current_phase: 15
current_phase_name: Journal plugin
status: executing
stopped_at: Completed 15-04-PLAN.md
last_updated: "2026-10-06T21:58:51.425Z"
last_activity: 2026-10-06
last_updated: "2026-10-07T16:43:18.298Z"
last_activity: 2026-10-07
last_activity_desc: Completed quick task 261006-s0v (remove ML copy-from locale UI)
state_head: 4a94380b5fe947b657dadd250e1960865ca3285e
state_head: fa25b4e000fac864b949be6daa4f800a4511a458
progress:
total_phases: 26
completed_phases: 12
@@ -652,6 +652,7 @@ Recent decisions affecting current work:
| 15 | Commit accumulated planning documents properly | 2026-10-06 | c05ad09 | — |
| 16 | Default icon shouldn't be a square but a sun. | 2026-10-06 | 4a94380 | — |
| 261007-p4y | Switch toggle pixel sizing; settings entries linking to admin controllers; translate Locales moved to Settings | 2026-10-07 | 0b09acd | [261007-p4y-toggle-proportions-and-settings-controll](./quick/261007-p4y-toggle-proportions-and-settings-controll/) |
| 18 | cabana list: join to-many relation column values with ", " | 2026-10-07 | fa25b4e | — |
## Deferred Items

View File

@@ -236,4 +236,4 @@ A relation's `messages` block overrides the relation manager's copy, each key a
## Relations in lists
A list column can show a related value with `relation` and `select` in `columns.yaml`; see [Lists and filters](lists-and-filters.md). A controller that maps a relation column to a physical column itself implements `pact.ListRelationColumnMapper`.
A list column can show a related value with `relation` and `select` in `columns.yaml`; see [Lists and filters](lists-and-filters.md). On a to-many relation the column joins the selected value of every related record with `, `, as WinterCMS does, and stays empty when there are none. A controller that maps a relation column to a physical column itself implements `pact.ListRelationColumnMapper`.

View File

@@ -84,3 +84,41 @@ func TestFieldByColumnTagBeatsGoName(t *testing.T) {
t.Fatalf("a tagged field matched by its Go name: %v", got)
}
}
type listTag struct {
ID uint `gorm:"primarykey"`
Name *string `gorm:"column:name"`
}
type listTagged struct {
ID uint `gorm:"primarykey"`
Tags []listTag `gorm:"many2many:tagged_tags"`
Pointers []*listTag `gorm:"many2many:tagged_pointer_tags"`
}
// TestProjectRowJoinsToManyRelation pins a to-many relation column (relation:
// tags, select: name): the list shows every related record's selected value
// joined with ", " like Winter, and leaves the key unset when there are none.
func TestProjectRowJoinsToManyRelation(t *testing.T) {
news, guides := "News", "Guides"
row := &listTagged{
ID: 3,
Tags: []listTag{{ID: 1, Name: &news}, {ID: 2, Name: &guides}, {ID: 4}},
Pointers: []*listTag{nil, {ID: 2, Name: &guides}},
}
cols := []ListColumn{
{Key: "tags", Relation: "tags", Select: "name"},
{Key: "pointers", Relation: "pointers", Select: "name"},
}
projected := projectRow(row, nil, cols)
if projected["tags"] != "News, Guides" {
t.Fatalf("tags = %#v, want %q", projected["tags"], "News, Guides")
}
if projected["pointers"] != "Guides" {
t.Fatalf("pointers = %#v, want %q", projected["pointers"], "Guides")
}
empty := projectRow(&listTagged{ID: 5}, nil, cols)
if _, ok := empty["tags"]; ok {
t.Fatalf("empty relation projected %#v, want the key unset", empty)
}
}

View File

@@ -580,16 +580,48 @@ func relatedSelect(row reflect.Value, controller pact.AdminController, col ListC
}
field = field.Elem()
}
column := listRelationColumn(controller, col.Relation, col.Select)
if field.Kind() == reflect.Slice {
return relatedSelectMany(field, column)
}
if field.Kind() != reflect.Struct {
return nil, false
}
selected := fieldByColumn(field, listRelationColumn(controller, col.Relation, col.Select))
selected := fieldByColumn(field, column)
if !selected.IsValid() || !selected.CanInterface() {
return nil, false
}
return selected.Interface(), true
}
// relatedSelectMany joins the selected column of every related record with
// ", ", as Winter's list does for a to-many relation column. No related
// records leave the key unset, so the list shows its empty placeholder.
func relatedSelectMany(records reflect.Value, column string) (any, bool) {
values := make([]string, 0, records.Len())
for i := 0; i < records.Len(); i++ {
record := records.Index(i)
for record.Kind() == reflect.Pointer && !record.IsNil() {
record = record.Elem()
}
if record.Kind() != reflect.Struct {
continue
}
selected := fieldByColumn(record, column)
for selected.IsValid() && selected.Kind() == reflect.Pointer && !selected.IsNil() {
selected = selected.Elem()
}
if !selected.IsValid() || !selected.CanInterface() || selected.Kind() == reflect.Pointer {
continue
}
values = append(values, fmt.Sprint(selected.Interface()))
}
if len(values) == 0 {
return nil, false
}
return strings.Join(values, ", "), true
}
func listRelationColumn(controller pact.AdminController, relation, column string) string {
mapper, ok := controller.(pact.ListRelationColumnMapper)
if !ok || mapper == nil {