feat(cabana): widget action payload and data channel (quick-261006-eyj)

- pact.AdminActionInput.Payload (json.RawMessage) carries the widget's own
  JSON value untouched; pact.AdminActionResult.Data is passed through as data
- cabana decodes payload with a 64 KiB cap (422 on body), refuses it on the
  toolbar and record routes, and embeds Data once encoded with a 256 KiB cap
  (opaque 500 when larger or unencodable); fill stays filtered
- root .swaggo overrides json.RawMessage so swag keeps record_id and values;
  admin.json and schema.d.ts regenerated (payload?: unknown, data?: unknown)
- TestWidgetPayloadAndData covers pass-through, cap, refusal and data 500
- cabana and pact READMEs, partials-and-widgets and admin-spa docs updated
This commit is contained in:
Jakub Zych
2026-10-06 11:13:16 +02:00
parent 964145628a
commit 6af88f9df6
12 changed files with 257 additions and 32 deletions

View File

@@ -3,6 +3,9 @@
"schemas": {
"cabana.AdminActionRequest": {
"properties": {
"payload": {
"description": "Payload is the widget's own JSON value (the summer-action event's\ndetail.payload): any JSON, at most 64 KiB, handed to the action as-is.\nToolbar and record routes refuse it."
},
"record_id": {
"type": "integer"
},
@@ -15,6 +18,9 @@
},
"cabana.AdminActionResult": {
"properties": {
"data": {
"description": "Data is the action's structured answer for the widget: any JSON value,\nnot filtered by fill, absent when the action returned none."
},
"fill": {
"additionalProperties": {},
"type": "object"
@@ -3614,7 +3620,7 @@
},
"/{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.",
"description": "Runs a controller-registered action that the list's toolbar.buttons declares. The body must be {}: record_id, values and payload are all refused, and the answer's fill is always empty.",
"parameters": [
{
"description": "Vendor",
@@ -3729,7 +3735,7 @@
},
"/{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.",
"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. An optional payload (any JSON value, at most 64 KiB) is accepted and handed to the action as-is; the response may carry data, the action's own JSON answer, which is not subject to the fill filter.",
"parameters": [
{
"description": "Vendor",
@@ -3776,7 +3782,7 @@
}
}
},
"description": "Record id and fill snapshot",
"description": "Record id, fill snapshot and optional payload",
"required": true
},
"responses": {
@@ -4159,7 +4165,7 @@
},
"/{vendor}/{plugin}/{controller}/{id}/actions/{action}": {
"post": {
"description": "Runs a record action the controller registers and the form's recordActions declares. The record is loaded and row-locked through the controller's form scope in one transaction (404 when missing or out of scope); an action that does not apply to the record's current state answers 409. The body must be {} and the answer's fill is always empty.",
"description": "Runs a record action the controller registers and the form's recordActions declares. The record is loaded and row-locked through the controller's form scope in one transaction (404 when missing or out of scope); an action that does not apply to the record's current state answers 409. The body must be {}: record_id, values and payload are all refused, and the answer's fill is always empty.",
"parameters": [
{
"description": "Vendor",

View File

@@ -1433,7 +1433,7 @@ export interface paths {
put?: never;
/**
* Run a toolbar action
* @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.
* @description Runs a controller-registered action that the list's toolbar.buttons declares. The body must be {}: record_id, values and payload are all refused, and the answer's fill is always empty.
*/
post: {
parameters: {
@@ -1522,7 +1522,7 @@ export interface paths {
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.
* @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. An optional payload (any JSON value, at most 64 KiB) is accepted and handed to the action as-is; the response may carry data, the action's own JSON answer, which is not subject to the fill filter.
*/
post: {
parameters: {
@@ -1540,7 +1540,7 @@ export interface paths {
};
cookie?: never;
};
/** @description Record id and fill snapshot */
/** @description Record id, fill snapshot and optional payload */
requestBody: {
content: {
"application/json": components["schemas"]["cabana.AdminActionRequest"];
@@ -1824,7 +1824,7 @@ export interface paths {
put?: never;
/**
* Run a declared record action
* @description Runs a record action the controller registers and the form's recordActions declares. The record is loaded and row-locked through the controller's form scope in one transaction (404 when missing or out of scope); an action that does not apply to the record's current state answers 409. The body must be {} and the answer's fill is always empty.
* @description Runs a record action the controller registers and the form's recordActions declares. The record is loaded and row-locked through the controller's form scope in one transaction (404 when missing or out of scope); an action that does not apply to the record's current state answers 409. The body must be {}: record_id, values and payload are all refused, and the answer's fill is always empty.
*/
post: {
parameters: {
@@ -4198,12 +4198,23 @@ export type webhooks = Record<string, never>;
export interface components {
schemas: {
"cabana.AdminActionRequest": {
/**
* @description Payload is the widget's own JSON value (the summer-action event's
* detail.payload): any JSON, at most 64 KiB, handed to the action as-is.
* Toolbar and record routes refuse it.
*/
payload?: unknown;
record_id?: number;
values?: {
[key: string]: unknown;
};
};
"cabana.AdminActionResult": {
/**
* @description Data is the action's structured answer for the widget: any JSON value,
* not filtered by fill, absent when the action returned none.
*/
data?: unknown;
fill: {
[key: string]: unknown;
};