feat(10.1-01): run registered widget actions through a cabana-owned route

- pact: AdminClientAssets, AdminAction, AdminActionInput, AdminActionResult,
  HasAdminActions and AdminPartialData contracts
- fields.yaml type: widget with widget, action and fill keys; boot checks the
  plugin tag prefix, the registered action and writable scalar fill fields
- POST .../widgets/{field} behind requireAjax, controller and action
  permissions, scoped non-locking record read and a server-side fill filter
- typed OpenAPI operation, inventories and an acme conformance case
This commit is contained in:
Jakub Zych
2026-09-28 23:35:00 +02:00
parent 9b98d8409f
commit f9281949a6
19 changed files with 876 additions and 10 deletions

View File

@@ -387,6 +387,44 @@ func AdminCreate() {}
// @Router /{vendor}/{plugin}/{controller}/bulk-delete [post]
func AdminBulkDelete() {}
// AdminActionRequest is the body of a widget or toolbar action. record_id is
// the record a widget on the update form belongs to (absent on create and
// always absent for a toolbar action); values is the widget's snapshot of its
// fill fields.
type AdminActionRequest struct {
RecordID *uint64 `json:"record_id,omitempty"`
Values map[string]any `json:"values,omitempty"`
}
// AdminActionResult is an action's answer: a localized message for the toast
// and the widget write-back, holding only the field's declared fill keys with
// scalar values. fill is always an object.
type AdminActionResult struct {
Message string `json:"message"`
Fill map[string]any `json:"fill"`
}
// AdminWidgetAction documents the widget action route.
//
// @Summary Run a widget action
// @Description Runs the controller action a `type: widget` field declares. The record is loaded through the controller's form scope (404 when out of scope); only the field's fill keys with scalar values reach the action and the response.
// @Tags admin
// @Accept json
// @Produce json
// @Security BackendBearer
// @Param vendor path string true "Vendor"
// @Param plugin path string true "Plugin"
// @Param controller path string true "Controller"
// @Param field path string true "Widget field name"
// @Param body body AdminActionRequest true "Record id and fill snapshot"
// @Success 200 {object} Envelope[AdminActionResult]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 422 {object} ErrorEnvelope
// @Router /{vendor}/{plugin}/{controller}/widgets/{field} [post]
func AdminWidgetAction() {}
// AdminShow documents the record show route.
//
// @Summary Show an admin record