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

@@ -299,6 +299,50 @@ type HasAdminBulkActions interface {
AdminBulkActions() []AdminBulkAction
}
// AdminRecordAction is one controller action an administrator runs on a
// single record (config_form.yaml recordActions). The admin framework owns the
// HTTP route, the CSRF check, authentication, the record lookup and the
// transaction; Run only carries the business logic and reads the write
// transaction with cabana.TxFromContext. Name is an identifier unique among
// the controller's record actions; create and delete are reserved, and a bulk
// action may use the same name. Label and Confirm are phrase keys or literal
// text; an empty Confirm means the framework's default confirm text.
// Permissions are checked in addition to the controller's
// RequiredPermissions. Applies reports whether the action applies to the
// record in its current state: a nil Applies means the action always applies.
// Applies must be a pure read, because it also runs when a record is shown, to
// decide which actions the admin is offered.
type AdminRecordAction struct {
Name string
Label string
Confirm string
Permissions []string
Applies func(ctx context.Context, record any) (bool, error) `json:"-"`
Run func(ctx context.Context, in AdminRecordActionInput) (AdminRecordActionResult, error) `json:"-"`
}
// AdminRecordActionInput is what the framework hands an AdminRecordAction.
// Record is the record the framework loaded through the controller's
// FormExtendQuery scope with a row lock, inside the action's transaction;
// RecordID is its primary key.
type AdminRecordActionInput struct {
RecordID uint64
Record any
}
// AdminRecordActionResult is a record action's answer. Message is a phrase key
// or text, localized by the framework and shown as a toast; when it is empty
// the admin shows its default text.
type AdminRecordActionResult struct {
Message string
}
// HasAdminRecordActions is implemented by an admin controller that registers
// named record actions for its config_form.yaml recordActions list.
type HasAdminRecordActions interface {
AdminRecordActions() []AdminRecordAction
}
// AdminPartialData supplies the view model a controller partial template
// renders (config_list.yaml headerPartial, fields.yaml `type: partial`). name
// is the partial name; record is the scoped record for a form partial on an