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 }