feat(12.1-01): declared bulk actions on admin lists

- pact.HasAdminBulkActions with AdminBulkAction, its input and result
- config_list.yaml bulkActions, compiled fail-loud, needs showCheckboxes
- POST .../{controller}/bulk/{action}: ids resolved and locked through the
  list scope in one transaction; partial selection is 409
- list schema offers declared actions per principal, with confirm text
- admin SPA bulk actions menu with confirm, busy state and failure toasts
- acme.roster fixture, tracer test, OpenAPI, TS types, dist, READMEs, docs
This commit is contained in:
Jakub Zych
2026-10-04 23:28:30 +02:00
parent ca9e9c0557
commit a879d6388c
45 changed files with 2007 additions and 38 deletions

View File

@@ -255,6 +255,50 @@ type HasAdminActions interface {
AdminActions() []AdminAction
}
// AdminBulkAction is one controller action an administrator runs on the rows
// selected in a list (config_list.yaml bulkActions). The admin framework owns
// the HTTP route, the CSRF check, authentication, the resolution of the posted
// ids 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 bulk actions; create and delete are reserved, and a
// record action may reuse a bulk action's name. Label and Confirm are phrase
// keys or literal text: Label is the menu item, Confirm the confirmation
// question, and an empty Confirm means the framework's default confirm text.
// Permissions are checked in addition to the controller's
// RequiredPermissions.
type AdminBulkAction struct {
Name string
Label string
Confirm string
Permissions []string
Run func(ctx context.Context, in AdminBulkActionInput) (AdminBulkActionResult, error) `json:"-"`
}
// AdminBulkActionInput is what the framework hands an AdminBulkAction.
// Records are pointers to the controller's model, loaded and row-locked
// through the controller's ListExtendQuery scope and ordered by primary key.
// A bulk action never receives ids, so an id list can never become an
// unscoped lookup.
type AdminBulkActionInput struct {
Records []any
}
// AdminBulkActionResult is a bulk 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 with Affected. Affected is the number of
// records the action changed, which may be lower than the selection when the
// action skips rows.
type AdminBulkActionResult struct {
Message string
Affected int
}
// HasAdminBulkActions is implemented by an admin controller that registers
// named bulk actions for its config_list.yaml bulkActions list.
type HasAdminBulkActions interface {
AdminBulkActions() []AdminBulkAction
}
// 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