feat(10.1-01): run registered widget actions through a cabana-owned route
- pact: AdminClientAssets, AdminAction, AdminActionInput, AdminActionResult,
HasAdminActions and AdminPartialData contracts
- fields.yaml type: widget with widget, action and fill keys; boot checks the
plugin tag prefix, the registered action and writable scalar fill fields
- POST .../widgets/{field} behind requireAjax, controller and action
permissions, scoped non-locking record read and a server-side fill filter
- typed OpenAPI operation, inventories and an acme conformance case
This commit is contained in:
@@ -2,6 +2,7 @@ package cabana_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
@@ -103,6 +104,19 @@ func TestPhase10OpenAPIConformance(t *testing.T) {
|
||||
e.gadgetID = dataID(t, rec.Body.Bytes())
|
||||
return rec
|
||||
}, into[cabana.RecordEnvelope]()},
|
||||
{"POST /{vendor}/{plugin}/{controller}/widgets/{field}", 200, "cabana.Envelope-cabana_AdminActionResult", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
|
||||
rec := e.send(t, http.MethodPost, "/acme/conform/gadgets/widgets/lookup", map[string]any{"record_id": e.gadgetID, "values": map[string]any{"name": "x", "active": false}}, true)
|
||||
// The fixture action also returns active, which is outside the
|
||||
// field's fill: the server filter must drop it.
|
||||
var body cabana.Envelope[cabana.AdminActionResult]
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("widget action body: %v\n%s", err, rec.Body.String())
|
||||
}
|
||||
if len(body.Data.Fill) != 1 || body.Data.Fill["name"] != "lookup-"+e.stamp {
|
||||
t.Fatalf("widget fill = %#v, want only name", body.Data.Fill)
|
||||
}
|
||||
return rec
|
||||
}, into[cabana.Envelope[cabana.AdminActionResult]]()},
|
||||
{"GET /{vendor}/{plugin}/{controller}", 200, "cabana.ListEnvelope-array_cabana_AdminRecord", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
|
||||
return e.send(t, http.MethodGet, "/acme/conform/gadgets?search="+e.stamp, nil, true)
|
||||
}, into[cabana.ListEnvelope[[]cabana.AdminRecord]]()},
|
||||
@@ -299,7 +313,7 @@ func newConformEnv(t *testing.T) *conformEnv {
|
||||
if err := lagoon.Publish(app, adminSQL, gdb); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plugins := []party.Plugin{conformPlugin{}}
|
||||
plugins := []party.Plugin{conformPlugin{stamp: stamp}}
|
||||
if err := phrasebook.Activate(app, plugins); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -362,14 +376,14 @@ func (conformSettings) TableName() string { return "cabana_conform_settin
|
||||
func (conformSettings) Fillable() []string { return []string{"enabled"} }
|
||||
func (conformSettings) Rules() map[string]string { return map[string]string{} }
|
||||
|
||||
type conformPlugin struct{}
|
||||
type conformPlugin struct{ stamp string }
|
||||
|
||||
func (conformPlugin) ID() string { return "acme.conform" }
|
||||
func (conformPlugin) Requires() []string { return nil }
|
||||
func (conformPlugin) Register(*backpack.App) error { return nil }
|
||||
func (conformPlugin) Boot(*backpack.App) error { return nil }
|
||||
func (conformPlugin) AdminControllers() []pact.AdminController {
|
||||
return []pact.AdminController{conformController{}}
|
||||
func (p conformPlugin) AdminControllers() []pact.AdminController {
|
||||
return []pact.AdminController{conformController{stamp: p.stamp}}
|
||||
}
|
||||
func (conformPlugin) Permissions() []pact.Permission {
|
||||
return []pact.Permission{{Code: "acme.conform.access", Roles: []string{"developer"}}}
|
||||
@@ -386,7 +400,7 @@ func (conformPlugin) Settings() []pact.SettingsItem {
|
||||
}
|
||||
func (conformPlugin) AdminFS() fs.FS { return conformFS() }
|
||||
|
||||
type conformController struct{}
|
||||
type conformController struct{ stamp string }
|
||||
|
||||
func (conformController) ID() string { return "acme.conform.gadgets" }
|
||||
func (conformController) ModelName() string { return "Gadget" }
|
||||
@@ -403,6 +417,15 @@ func (conformController) AdminFieldRelations() []cabana.FieldRelationContract {
|
||||
return []cabana.FieldRelationContract{{Field: "group", Kind: "belongsTo", NewRelated: func() any { return &conformGroup{} }, ForeignKey: "group_id"}}
|
||||
}
|
||||
|
||||
func (c conformController) AdminActions() []pact.AdminAction {
|
||||
return []pact.AdminAction{{
|
||||
Name: "lookup", Label: "Look up", Permissions: []string{"acme.conform.access"},
|
||||
Run: func(context.Context, pact.AdminActionInput) (pact.AdminActionResult, error) {
|
||||
return pact.AdminActionResult{Message: "Looked up", Fill: map[string]any{"name": "lookup-" + c.stamp, "active": true}}, nil
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func conformFS() fs.FS {
|
||||
file := func(s string) *fstest.MapFile { return &fstest.MapFile{Data: []byte(s)} }
|
||||
return fstest.MapFS{
|
||||
@@ -483,6 +506,12 @@ update:
|
||||
type: relation-manager
|
||||
relation: members
|
||||
context: [update]
|
||||
lookup:
|
||||
label: Lookup
|
||||
type: widget
|
||||
widget: acme-conform-lookup
|
||||
action: lookup
|
||||
fill: [name]
|
||||
`),
|
||||
"models/settings/fields.yaml": file(`fields:
|
||||
enabled:
|
||||
|
||||
Reference in New Issue
Block a user