feat(12.1-01): declared record actions with an applicability rule

- pact.HasAdminRecordActions with AdminRecordAction (Applies, Run)
- config_form.yaml recordActions, compiled fail-loud
- show response meta.actions lists the permitted actions that apply
- POST .../{controller}/{id}/actions/{action}: record loaded and locked
  through the form scope; 404 out of scope, 409 when it does not apply
- RecordActions.vue with confirm and request flow (mounted by plan 02)
- roster fixture, smoke tests, OpenAPI, TS types, READMEs, docs
This commit is contained in:
Jakub Zych
2026-10-04 23:37:30 +02:00
parent a879d6388c
commit e0ccced76a
34 changed files with 1350 additions and 24 deletions

View File

@@ -14,6 +14,7 @@ type Person struct {
Name string `gorm:"column:name"`
Email string `gorm:"column:email"`
Active bool `gorm:"column:active"`
Banned bool `gorm:"column:banned"`
}
func (Person) TableName() string { return "acme_roster_people" }
@@ -21,13 +22,14 @@ func (Person) TableName() string { return "acme_roster_people" }
// Fillable lists the columns the admin form may write.
func (Person) Fillable() []string { return []string{"name", "email"} }
// PeopleController is an admin controller whose list offers bulk actions.
// PeopleController is an admin controller with bulk and record actions.
type PeopleController struct{}
var (
_ pact.AdminController = PeopleController{}
_ pact.AdminRecordSource = PeopleController{}
_ pact.HasAdminBulkActions = PeopleController{}
_ pact.AdminController = PeopleController{}
_ pact.AdminRecordSource = PeopleController{}
_ pact.HasAdminBulkActions = PeopleController{}
_ pact.HasAdminRecordActions = PeopleController{}
)
func (PeopleController) ID() string { return "acme.roster.people" }
@@ -86,3 +88,44 @@ func (PeopleController) AdminBulkActions() []pact.AdminBulkAction {
},
}}
}
// AdminRecordActions registers the actions config_form.yaml offers under
// recordActions. Applies decides whether an action fits the record's current
// state; Run receives the record loaded and locked through the form scope.
func (PeopleController) AdminRecordActions() []pact.AdminRecordAction {
return []pact.AdminRecordAction{{
Name: "activate",
Label: "acme.roster::lang.people.activate",
Permissions: []string{"acme.roster.manage"},
Applies: func(_ context.Context, record any) (bool, error) {
return !record.(*Person).Active, nil
},
Run: func(ctx context.Context, in pact.AdminRecordActionInput) (pact.AdminRecordActionResult, error) {
tx, ok := cabana.TxFromContext(ctx)
if !ok {
return pact.AdminRecordActionResult{}, errors.New("no transaction")
}
if err := tx.Model(in.Record).Update("active", true).Error; err != nil {
return pact.AdminRecordActionResult{}, err
}
return pact.AdminRecordActionResult{Message: "acme.roster::lang.people.activated"}, nil
},
}, {
Name: "reinstate",
Label: "acme.roster::lang.people.reinstate",
Confirm: "acme.roster::lang.people.reinstate_confirm",
Applies: func(_ context.Context, record any) (bool, error) {
return record.(*Person).Banned, nil
},
Run: func(ctx context.Context, in pact.AdminRecordActionInput) (pact.AdminRecordActionResult, error) {
tx, ok := cabana.TxFromContext(ctx)
if !ok {
return pact.AdminRecordActionResult{}, errors.New("no transaction")
}
if err := tx.Model(in.Record).Update("banned", false).Error; err != nil {
return pact.AdminRecordActionResult{}, err
}
return pact.AdminRecordActionResult{}, nil
},
}}
}