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

@@ -67,6 +67,11 @@ func compileExtension(pluginID string, cc *CompiledController, fsys fs.FS) error
return fmt.Errorf("cabana: admin controller %s/%s: %w", pluginID, id, err)
}
cc.Actions = actions
bulkActions, err := compileBulkActions(cc.Controller)
if err != nil {
return fmt.Errorf("cabana: admin controller %s/%s: %w", pluginID, id, err)
}
cc.BulkActions = bulkActions
if err := compileClientAssets(pluginID, cc, fsys); err != nil {
return err
}
@@ -276,3 +281,30 @@ func compileActions(ctl pact.AdminController) (map[string]pact.AdminAction, erro
}
return out, nil
}
// compileBulkActions collects a controller's registered bulk actions. They
// have their own namespace next to the toolbar and widget actions: a name is
// unique among the bulk actions, and create and delete stay reserved.
func compileBulkActions(ctl pact.AdminController) (map[string]pact.AdminBulkAction, error) {
out := map[string]pact.AdminBulkAction{}
src, ok := ctl.(pact.HasAdminBulkActions)
if !ok || src == nil {
return out, nil
}
for _, action := range src.AdminBulkActions() {
if !identifier(action.Name) {
return nil, fmt.Errorf("bulk action name %q is not an identifier", action.Name)
}
if builtinToolbarActions[action.Name] {
return nil, fmt.Errorf("bulk action %s uses a reserved built-in name (create, delete)", action.Name)
}
if _, dup := out[action.Name]; dup {
return nil, fmt.Errorf("duplicate bulk action %s", action.Name)
}
if action.Run == nil {
return nil, fmt.Errorf("bulk action %s has no Run function", action.Name)
}
out[action.Name] = action
}
return out, nil
}