feat(10.1-01): serve controller JS/CSS and run registered toolbar actions

- boardwalk exports ContentType and SetSecurityHeaders
- pact.AdminClientAssets files are read and hashed at boot and served by exact
  key under {prefix}/assets/{vendor}/{plugin}/ with nosniff, CSP, CORP,
  no-cache and an ETag; a miss falls through to the SPA
- list and form schemas carry assets URLs with a ?v= hash
- toolbar.buttons resolves create, delete and registered actions after decode;
  toolbarActions is permission-filtered per admin
- POST .../toolbar/{action} behind requireAjax and action permissions
This commit is contained in:
Jakub Zych
2026-09-28 23:41:17 +02:00
parent f9281949a6
commit 8b1cb244de
23 changed files with 772 additions and 64 deletions

View File

@@ -170,6 +170,27 @@
],
"type": "object"
},
"cabana.ControllerAssets": {
"properties": {
"scripts": {
"items": {
"type": "string"
},
"type": "array"
},
"styles": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"scripts",
"styles"
],
"type": "object"
},
"cabana.Envelope-array_cabana_FilterOption": {
"properties": {
"data": {
@@ -609,6 +630,14 @@
},
"cabana.FormView": {
"properties": {
"assets": {
"allOf": [
{
"$ref": "#/components/schemas/cabana.ControllerAssets"
}
],
"description": "Assets are the controller's plugin JS and CSS URLs; a settings form\ncarries empty lists."
},
"fields": {
"items": {
"$ref": "#/components/schemas/cabana.FormField"
@@ -642,6 +671,7 @@
}
},
"required": [
"assets",
"fields",
"messages",
"meta",
@@ -838,6 +868,14 @@
},
"cabana.ListSchema": {
"properties": {
"assets": {
"allOf": [
{
"$ref": "#/components/schemas/cabana.ControllerAssets"
}
],
"description": "Assets are the controller's plugin JS and CSS URLs (pact.AdminClientAssets)."
},
"bulkActions": {
"items": {
"$ref": "#/components/schemas/cabana.BulkAction"
@@ -915,6 +953,13 @@
"title": {
"type": "string"
},
"toolbarActions": {
"description": "ToolbarActions are the controller-registered toolbar.buttons entries\nthe requesting admin may run, in declared order, with their labels.",
"items": {
"$ref": "#/components/schemas/cabana.ToolbarAction"
},
"type": "array"
},
"toolbarButtons": {
"items": {
"type": "string"
@@ -923,6 +968,7 @@
}
},
"required": [
"assets",
"bulkActions",
"columns",
"filters",
@@ -935,6 +981,7 @@
"showSearch",
"showSetup",
"showSorting",
"toolbarActions",
"toolbarButtons"
],
"type": "object"
@@ -1277,6 +1324,21 @@
},
"type": "object"
},
"cabana.ToolbarAction": {
"properties": {
"label": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"label",
"name"
],
"type": "object"
},
"cabana.fieldContext": {
"oneOf": [
{
@@ -2730,6 +2792,121 @@
]
}
},
"/{vendor}/{plugin}/{controller}/toolbar/{action}": {
"post": {
"description": "Runs a controller-registered action that the list's toolbar.buttons declares. The body must be {}: a toolbar action takes no record ids or values, and its fill is always empty.",
"parameters": [
{
"description": "Vendor",
"in": "path",
"name": "vendor",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Plugin",
"in": "path",
"name": "plugin",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Controller",
"in": "path",
"name": "controller",
"required": true,
"schema": {
"type": "string"
}
},
{
"description": "Action name",
"in": "path",
"name": "action",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.AdminActionRequest"
}
}
},
"description": "Empty object",
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.Envelope-cabana_AdminActionResult"
}
}
},
"description": "OK"
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unauthorized"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Forbidden"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Not Found"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unprocessable Entity"
}
},
"security": [
{
"BackendBearer": []
}
],
"summary": "Run a toolbar action",
"tags": [
"admin"
]
}
},
"/{vendor}/{plugin}/{controller}/widgets/{field}": {
"post": {
"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.",