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:
@@ -279,6 +279,14 @@ func TestPhase10OpenAPIConformance(t *testing.T) {
|
||||
spare := e.send(t, http.MethodPost, "/acme/conform/gadgets", map[string]any{"name": "spare-" + e.stamp}, true)
|
||||
return e.send(t, http.MethodPost, "/acme/conform/gadgets/bulk-delete", map[string]any{"ids": []uint{dataID(t, spare.Body.Bytes())}}, true)
|
||||
}, into[cabana.Envelope[cabana.BulkResult]](), nil},
|
||||
{"POST /{vendor}/{plugin}/{controller}/bulk/{action}", 200, "cabana.Envelope-cabana_BulkActionResult", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
|
||||
rec := e.send(t, http.MethodPost, "/acme/conform/gadgets/bulk/touch", map[string]any{"ids": []uint{e.gadgetID}}, true)
|
||||
var body cabana.Envelope[cabana.BulkActionResult]
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil || body.Data.Affected != 1 || body.Data.Message != "Touched" {
|
||||
t.Fatalf("bulk action body = %s (%v)", rec.Body.String(), err)
|
||||
}
|
||||
return rec
|
||||
}, into[cabana.Envelope[cabana.BulkActionResult]](), nil},
|
||||
{"DELETE /{vendor}/{plugin}/{controller}/{id}", 200, "cabana.Envelope-cabana_BulkResult", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
|
||||
return e.send(t, http.MethodDelete, fmt.Sprintf("/acme/conform/gadgets/%d", e.gadgetID), nil, true)
|
||||
}, into[cabana.Envelope[cabana.BulkResult]](), nil},
|
||||
@@ -790,6 +798,16 @@ func (c conformController) AdminActions() []pact.AdminAction {
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// AdminBulkActions registers the bulk action the list's bulkActions declares.
|
||||
func (conformController) AdminBulkActions() []pact.AdminBulkAction {
|
||||
return []pact.AdminBulkAction{{
|
||||
Name: "touch", Label: "Touch", Permissions: []string{"acme.conform.access"},
|
||||
Run: func(_ context.Context, in pact.AdminBulkActionInput) (pact.AdminBulkActionResult, error) {
|
||||
return pact.AdminBulkActionResult{Message: "Touched", Affected: len(in.Records)}, nil
|
||||
},
|
||||
}}
|
||||
}
|
||||
func (conformController) AdminJS() []string { return []string{"assets/js/lookup.js"} }
|
||||
|
||||
// PartialData supplies curated view models, never the gadget model itself.
|
||||
@@ -824,6 +842,7 @@ toolbar:
|
||||
buttons: [create, delete, recount]
|
||||
search:
|
||||
prompt: backend::lang.list.search_prompt
|
||||
bulkActions: [touch]
|
||||
`),
|
||||
// A plain custom element: a light-DOM button that asks the admin SPA
|
||||
// to run the field's action. It makes no network call and never
|
||||
|
||||
Reference in New Issue
Block a user