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,11 @@ func (s *service) mount(r pact.Router) {
constrainController(g)
g.Post("/{vendor}/{plugin}/{controller}/bulk-delete", requireAjax(s.bulkDelete))
constrainController(g)
// Declared bulk actions (D-09): ids are resolved through the list
// scope before plugin code runs.
g.Post("/{vendor}/{plugin}/{controller}/bulk/{action}", requireAjax(s.bulkAction))
constrainController(g)
g.Where("action", "[A-Za-z_][A-Za-z0-9_]*")
// Runtime extension actions (Phase 10.1): cabana owns these routes, so
// CSRF, auth and record scoping never depend on plugin code.
g.Post("/{vendor}/{plugin}/{controller}/widgets/{field}", requireAjax(s.widgetAction))
@@ -719,6 +724,20 @@ func (s *service) listSchema(w http.ResponseWriter, r *http.Request) {
}
}
view.ToolbarActions = allowed
// The built-in delete stays; a declared bulk action is offered only
// to an admin who may run it. A new slice: the cached schema is
// never mutated.
bulk := make([]BulkAction, 0, len(view.BulkActions))
for _, entry := range view.BulkActions {
if builtinToolbarActions[entry.Name] {
bulk = append(bulk, entry)
continue
}
if registered, ok := cc.BulkActions[entry.Name]; ok && Allows(principal, registered.Permissions) {
bulk = append(bulk, entry)
}
}
view.BulkActions = bulk
view.Assets = s.controllerAssets(cc)
meta := map[string]any{}
if view.Meta != nil {