Files
summercms/modules/cabana/example_form_extension_test.go
Jakub Zych fa1af69376 docs(32-01): form extensions in admin-controllers, cabana and pact READMEs
- admin-controllers: placement, values, Load/Save, permissions,
  widgets, partials and boot errors, with the ExtrasPlugin snippet
- Example_formExtension compiles an owner form with the extension
- coming-from-wintercms maps extendFormFields, addFields, addTabFields
  and removeField to the form extension
- cabana README feature bullet and Activate row
2026-10-26 22:05:22 +01:00

148 lines
4.9 KiB
Go

package cabana
import (
"context"
"fmt"
"io/fs"
"strconv"
"strings"
"testing/fstest"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/golem15/summercms/modules/party"
)
// ExtraValue is a row of the acme.extras plugin's own table: the subtitle
// and the featured flag it keeps for a post of the acme.blog plugin.
type ExtraValue struct {
PostID uint `gorm:"column:post_id;primaryKey;autoIncrement:false"`
Subtitle string `gorm:"column:subtitle"`
Featured bool `gorm:"column:featured"`
}
func (ExtraValue) TableName() string { return "acme_extras_values" }
// extrasFields is the extension's fields document: fields.yaml keys plus
// before:/after: anchors, and a tabs: list for the new tab.
const extrasFields = `tabs:
- name: acme.extras::lang.tab.publishing
after: acme.blog::lang.tab.content
fields:
subtitle:
label: acme.extras::lang.subtitle
type: text
span: left
after: title
featured:
label: acme.extras::lang.featured
type: switch
tab: acme.extras::lang.tab.publishing
`
// ExtrasPlugin adds a subtitle after the title and a Publishing tab to the
// acme.blog posts form, which it does not own, and hides the owner's summary
// field. It stores its values in its own table.
type ExtrasPlugin struct{}
func (ExtrasPlugin) ID() string { return "acme.extras" }
func (ExtrasPlugin) Requires() []string { return []string{"acme.blog"} }
func (ExtrasPlugin) Register(*backpack.App) error { return nil }
func (ExtrasPlugin) Boot(*backpack.App) error { return nil }
func (ExtrasPlugin) AdminFS() fs.FS {
return fstest.MapFS{"extensions/posts.yaml": &fstest.MapFile{Data: []byte(extrasFields)}}
}
var _ pact.HasAdminActionExtensions = ExtrasPlugin{}
// AdminActionExtensions adds fields and a tab to the acme.blog posts form.
func (ExtrasPlugin) AdminActionExtensions() []pact.AdminActionExtension {
return []pact.AdminActionExtension{{
Controller: "acme.blog.posts",
Form: &pact.AdminFormExtension{
Fields: "extensions/posts.yaml", // in this plugin's AdminFS
Hide: []string{"summary"},
Permissions: []string{"acme.extras.edit"},
Load: func(ctx context.Context, in pact.AdminFormLoad) (map[string]any, error) {
var row ExtraValue
err := in.DB.Where("post_id = ?", in.ID).Limit(1).Find(&row).Error
return map[string]any{"subtitle": row.Subtitle, "featured": row.Featured}, err
},
Save: func(ctx context.Context, in pact.AdminFormSave) (pact.AdminFormSaveResult, error) {
subtitle, _ := in.Values["subtitle"].(string)
if len(subtitle) > 120 {
return pact.AdminFormSaveResult{}, pact.AdminFormErrors{"subtitle": {"acme.extras::lang.subtitle_too_long"}}
}
id, err := strconv.ParseUint(in.ID, 10, 64)
if err != nil {
return pact.AdminFormSaveResult{}, err
}
featured, _ := in.Values["featured"].(bool)
row := ExtraValue{PostID: uint(id), Subtitle: subtitle, Featured: featured}
// in.DB is the record's save transaction: an error rolls back
// the owner's row too.
return pact.AdminFormSaveResult{}, in.DB.Save(&row).Error
},
},
}}
}
// exampleBlog is the owner: acme.blog with a posts form of its own.
type exampleBlog struct{}
func (exampleBlog) ID() string { return "acme.blog" }
func (exampleBlog) Requires() []string { return nil }
func (exampleBlog) Register(*backpack.App) error { return nil }
func (exampleBlog) Boot(*backpack.App) error { return nil }
func (exampleBlog) AdminControllers() []pact.AdminController {
return []pact.AdminController{feCtl{id: "acme.blog.posts"}}
}
func (exampleBlog) AdminFS() fs.FS {
return feOwnerPlugin{id: "acme.blog", fields: `fields:
title:
label: acme.blog::lang.title
type: text
span: left
summary:
label: acme.blog::lang.summary
type: text
body:
label: acme.blog::lang.body
type: text
tab: acme.blog::lang.tab.content
meta:
label: acme.blog::lang.meta
type: text
tab: acme.blog::lang.tab.meta
`}.AdminFS()
}
// Example_formExtension compiles the owner's form with the extension, as
// the admin does at boot, and prints each tab with its fields.
func Example_formExtension() {
plugins := []party.Plugin{exampleBlog{}, ExtrasPlugin{}}
reg, err := feCompile(plugins...)
if err != nil {
fmt.Println(err)
return
}
cc, _ := reg.Get("acme.blog.posts")
var tabs []string
fields := map[string][]string{}
for _, field := range cc.Form.Fields {
tab := displayTab(field.Tab)
if _, seen := fields[tab]; !seen {
tabs = append(tabs, tab)
}
fields[tab] = append(fields[tab], field.Name)
}
for _, tab := range tabs {
fmt.Printf("%s: %s\n", tab, strings.Join(fields[tab], ", "))
}
// Output:
// default: title, subtitle
// acme.blog::lang.tab.content: body
// acme.extras::lang.tab.publishing: featured
// acme.blog::lang.tab.meta: meta
}