diff --git a/admin/openapi/admin.json b/admin/openapi/admin.json index c78bc16..7941d6c 100644 --- a/admin/openapi/admin.json +++ b/admin/openapi/admin.json @@ -176,6 +176,9 @@ }, "cabana.BulkAction": { "properties": { + "confirm": { + "type": "string" + }, "label": { "type": "string" }, @@ -188,6 +191,21 @@ ], "type": "object" }, + "cabana.BulkActionResult": { + "properties": { + "affected": { + "type": "integer" + }, + "message": { + "type": "string" + } + }, + "required": [ + "affected", + "message" + ], + "type": "object" + }, "cabana.BulkResult": { "properties": { "deleted": { @@ -367,6 +385,21 @@ ], "type": "object" }, + "cabana.Envelope-cabana_BulkActionResult": { + "properties": { + "data": { + "$ref": "#/components/schemas/cabana.BulkActionResult" + }, + "meta": { + "$ref": "#/components/schemas/cabana.SuccessMeta" + } + }, + "required": [ + "data", + "meta" + ], + "type": "object" + }, "cabana.Envelope-cabana_BulkResult": { "properties": { "data": { @@ -2727,6 +2760,131 @@ ] } }, + "/{vendor}/{plugin}/{controller}/bulk/{action}": { + "post": { + "description": "Runs a bulk action the controller registers and the list's bulkActions declares. The ids are resolved and row-locked through the controller's list scope in one transaction before the action runs: a selection that matches no scoped row answers affected 0 without running the action, and a partial selection is a 409 that changes nothing.", + "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": "Bulk action name", + "in": "path", + "name": "action", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/cabana.AdminIDsRequest" + } + } + }, + "description": "Record ids", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/cabana.Envelope-cabana_BulkActionResult" + } + } + }, + "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" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/cabana.ErrorEnvelope" + } + } + }, + "description": "Conflict" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/cabana.ErrorEnvelope" + } + } + }, + "description": "Unprocessable Entity" + } + }, + "security": [ + { + "BackendBearer": [] + } + ], + "summary": "Run a declared bulk action", + "tags": [ + "admin" + ] + } + }, "/{vendor}/{plugin}/{controller}/fields/{field}/options": { "get": { "description": "Choices for a writable `type: relation` field: value is the related id, label its nameFrom column. Read-only and non-relation fields answer 404.", diff --git a/admin/src/api/schema.d.ts b/admin/src/api/schema.d.ts index c27bbae..e886528 100644 --- a/admin/src/api/schema.d.ts +++ b/admin/src/api/schema.d.ts @@ -862,6 +862,104 @@ export interface paths { patch?: never; trace?: never; }; + "/{vendor}/{plugin}/{controller}/bulk/{action}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Run a declared bulk action + * @description Runs a bulk action the controller registers and the list's bulkActions declares. The ids are resolved and row-locked through the controller's list scope in one transaction before the action runs: a selection that matches no scoped row answers affected 0 without running the action, and a partial selection is a 409 that changes nothing. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Vendor */ + vendor: string; + /** @description Plugin */ + plugin: string; + /** @description Controller */ + controller: string; + /** @description Bulk action name */ + action: string; + }; + cookie?: never; + }; + /** @description Record ids */ + requestBody: { + content: { + "application/json": components["schemas"]["cabana.AdminIDsRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["cabana.Envelope-cabana_BulkActionResult"]; + }; + }; + /** @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 Conflict */ + 409: { + 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}/fields/{field}/options": { parameters: { query?: never; @@ -4046,9 +4144,14 @@ export interface components { name: string; }; "cabana.BulkAction": { + confirm?: string; label?: string; name: string; }; + "cabana.BulkActionResult": { + affected: number; + message: string; + }; "cabana.BulkResult": { deleted: number; }; @@ -4092,6 +4195,10 @@ export interface components { data: components["schemas"]["cabana.AdminRecord"]; meta: components["schemas"]["cabana.SuccessMeta"]; }; + "cabana.Envelope-cabana_BulkActionResult": { + data: components["schemas"]["cabana.BulkActionResult"]; + meta: components["schemas"]["cabana.SuccessMeta"]; + }; "cabana.Envelope-cabana_BulkResult": { data: components["schemas"]["cabana.BulkResult"]; meta: components["schemas"]["cabana.SuccessMeta"]; diff --git a/admin/src/api/types.ts b/admin/src/api/types.ts index fe1f7d7..718436c 100644 --- a/admin/src/api/types.ts +++ b/admin/src/api/types.ts @@ -34,6 +34,10 @@ export type FilterOption = Schemas['cabana.FilterOption'] export type RecordMeta = Schemas['cabana.RecordMeta'] export type RecordEnvelope = Schemas['cabana.RecordEnvelope'] export type BulkResult = Schemas['cabana.BulkResult'] +/** A checkbox action of a list: the built-in delete or a declared bulk action. */ +export type BulkAction = Schemas['cabana.BulkAction'] +/** A declared bulk action's answer: the toast message and the affected count. */ +export type BulkActionResult = Schemas['cabana.BulkActionResult'] /** Body of a widget action: the record id (absent on create) and fill values. */ export type AdminActionRequest = Schemas['cabana.AdminActionRequest'] /** A widget or toolbar action result: the toast message and fill values. */ diff --git a/admin/src/components/list/BulkActionsMenu.vue b/admin/src/components/list/BulkActionsMenu.vue new file mode 100644 index 0000000..07c9707 --- /dev/null +++ b/admin/src/components/list/BulkActionsMenu.vue @@ -0,0 +1,60 @@ + + + diff --git a/admin/src/components/list/ListToolbar.vue b/admin/src/components/list/ListToolbar.vue index 761124c..4f44275 100644 --- a/admin/src/components/list/ListToolbar.vue +++ b/admin/src/components/list/ListToolbar.vue @@ -1,9 +1,10 @@