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

@@ -1,6 +1,34 @@
{
"components": {
"schemas": {
"cabana.AdminActionRequest": {
"properties": {
"record_id": {
"type": "integer"
},
"values": {
"additionalProperties": {},
"type": "object"
}
},
"type": "object"
},
"cabana.AdminActionResult": {
"properties": {
"fill": {
"additionalProperties": {},
"type": "object"
},
"message": {
"type": "string"
}
},
"required": [
"fill",
"message"
],
"type": "object"
},
"cabana.AdminIDsRequest": {
"properties": {
"ids": {
@@ -196,6 +224,21 @@
],
"type": "object"
},
"cabana.Envelope-cabana_AdminActionResult": {
"properties": {
"data": {
"$ref": "#/components/schemas/cabana.AdminActionResult"
},
"meta": {
"$ref": "#/components/schemas/cabana.SuccessMeta"
}
},
"required": [
"data",
"meta"
],
"type": "object"
},
"cabana.Envelope-cabana_AdminLoginData": {
"properties": {
"data": {
@@ -394,6 +437,14 @@
},
"cabana.FormField": {
"properties": {
"action": {
"description": "Action names the controller action the widget runs (pact.HasAdminActions).",
"type": "string"
},
"actionLabel": {
"description": "ActionLabel is the action's Label, localized per request.",
"type": "string"
},
"attributes": {
"additionalProperties": {
"$ref": "#/components/schemas/cabana.jsonScalar"
@@ -412,6 +463,13 @@
"emptyOption": {
"type": "string"
},
"fill": {
"description": "Fill lists the fields of the same form the action writes back (D-07).",
"items": {
"type": "string"
},
"type": "array"
},
"label": {
"type": "string"
},
@@ -450,6 +508,10 @@
},
"type": {
"type": "string"
},
"widget": {
"description": "Widget is the custom-element tag of a `type: widget` field (D-06).",
"type": "string"
}
},
"required": [
@@ -2668,6 +2730,121 @@
]
}
},
"/{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.",
"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": "Widget field name",
"in": "path",
"name": "field",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.AdminActionRequest"
}
}
},
"description": "Record id and fill snapshot",
"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 widget action",
"tags": [
"admin"
]
}
},
"/{vendor}/{plugin}/{controller}/{id}": {
"delete": {
"parameters": [

View File

@@ -1237,6 +1237,95 @@ export interface paths {
patch?: never;
trace?: never;
};
"/{vendor}/{plugin}/{controller}/widgets/{field}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* 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.
*/
post: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Vendor */
vendor: string;
/** @description Plugin */
plugin: string;
/** @description Controller */
controller: string;
/** @description Widget field name */
field: string;
};
cookie?: never;
};
/** @description Record id and fill snapshot */
requestBody: {
content: {
"application/json": components["schemas"]["cabana.AdminActionRequest"];
};
};
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.Envelope-cabana_AdminActionResult"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Unprocessable Entity */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/{vendor}/{plugin}/{controller}/{id}": {
parameters: {
query?: never;
@@ -1812,6 +1901,18 @@ export interface paths {
export type webhooks = Record<string, never>;
export interface components {
schemas: {
"cabana.AdminActionRequest": {
record_id?: number;
values?: {
[key: string]: unknown;
};
};
"cabana.AdminActionResult": {
fill: {
[key: string]: unknown;
};
message: string;
};
"cabana.AdminIDsRequest": {
ids: number[];
};
@@ -1864,6 +1965,10 @@ export interface components {
data: components["schemas"]["cabana.SettingsEntry"][];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_AdminActionResult": {
data: components["schemas"]["cabana.AdminActionResult"];
meta: components["schemas"]["cabana.SuccessMeta"];
};
"cabana.Envelope-cabana_AdminLoginData": {
data: components["schemas"]["cabana.AdminLoginData"];
meta: components["schemas"]["cabana.SuccessMeta"];
@@ -1919,6 +2024,10 @@ export interface components {
value: string;
};
"cabana.FormField": {
/** @description Action names the controller action the widget runs (pact.HasAdminActions). */
action?: string;
/** @description ActionLabel is the action's Label, localized per request. */
actionLabel?: string;
attributes?: {
[key: string]: components["schemas"]["cabana.jsonScalar"];
};
@@ -1926,6 +2035,8 @@ export interface components {
context?: components["schemas"]["cabana.fieldContext"];
default?: components["schemas"]["cabana.jsonScalar"];
emptyOption?: string;
/** @description Fill lists the fields of the same form the action writes back (D-07). */
fill?: string[];
label?: string;
multiple?: boolean;
name: string;
@@ -1938,6 +2049,8 @@ export interface components {
span?: string;
tab?: string;
type: string;
/** @description Widget is the custom-element tag of a `type: widget` field (D-06). */
widget?: string;
};
"cabana.FormMessages": {
create: components["schemas"]["cabana.MessageForms"];