Bulk actions
Status: ✅ Implemented
Intent
Section titled “Intent”Operate on many records at once from a listing — approve 50 invoices, deactivate a batch of products, assign a set of tickets — without visiting each detail view. The listing offers row selection (checkbox column with select-all) and toolbar buttons that act on the selection.
Solution
Section titled “Solution”Annotate a method of your AutoCrud (or any Crud) with @ListToolbarButton. It renders as a
toolbar button over the listing, and a List<Row> parameter receives the typed selection:
@UI("/invoices")public class InvoicesCrud extends AutoCrud<Invoice> {
@Override public CrudStore<Invoice> store() { return store; }
@ListToolbarButton @Label("Aprobar seleccionadas") public Message approve(List<Invoice> selection, HttpRequest httpRequest) { selection.forEach(invoice -> service.approve(invoice.id())); return new Message(selection.size() + " facturas aprobadas"); }}- The selection column is enabled by default on every crud listing (opt out by overriding
selectionEnabled()tofalse). - The frontend keeps the selected rows in the component state (
crud_selected_items); the framework hydrates them typed into anyList<Row>parameter of the method. - By default the button requires a selection: clicking it with nothing selected shows a
notice instead of calling the server. Opt out with
@ListToolbarButton(rowsSelectedRequired = false)for toolbar actions that don’t need rows. @ListToolbarButton(confirmationRequired = true)asks for confirmation first — combine both for destructive bulk operations. See Making it look and read dangerous for saying what is being confirmed.- The button label follows the usual rules:
@Label(translated) or the humanized method name. - Return value semantics are the standard action ones: a
Messagetoasts, aPageBannershows a banner,void/nullsimply re-runs the search so the listing reflects the changes.
Making it look and read dangerous
Section titled “Making it look and read dangerous”A bulk action runs over N records at once, so it is the one that most needs to look destructive and to say what the user is about to confirm. Three annotations on the same method, each with one job:
| Annotation | Says |
|---|---|
@ListToolbarButton | where the button goes (a crud has two toolbars — this one and the detail view’s @ViewToolbarButton) |
@Toolbar | how it looks — buttonStyle, buttonColor, buttonSize, order |
@Action | how it behaves — the confirmation texts, timeoutMillis, sse, background, idempotent… |
This is the same composition a detail-view method already uses, so nothing is duplicated:
@ListToolbarButton(confirmationRequired = true)@Toolbar(buttonStyle = ButtonStyle.secondary, buttonColor = ButtonColor.error, order = 10)@Action(confirmationTitle = "Cancel processes", confirmationMessage = "Cancelling stops every selected process. This cannot be undone.", confirmationText = "Cancel them", confirmationDenialText = "Keep running")@Label("Cancel")public Message cancel(List<ProcessRow> selection) { selection.forEach(process -> service.cancel(process.id())); return new Message(selection.size() + " cancelled");}Notes:
- Each confirmation text falls back on its own, so declaring only
confirmationMessagekeeps the framework’s title and Yes/No labels instead of blanking them. - The flags are merged, not overridden:
@Action.rowsSelectedRequireddefaults tofalsewhile@ListToolbarButtondefaults it totrue, so adding an@Actionfor its texts never disarms the selection guard. @Action.id()is ignored here — the dispatch id belongs to the placement (action-on-row-<method>).- Without
@Toolbarthe button keeps the renderer’s default look, and without@Actionit keeps the generic confirmation dialog: existing buttons are untouched. @Toolbar(order = N)fixes the button sequence, which reflection alone does not guarantee.
The same three annotations work on a capability Listing and on @ViewToolbarButton methods of the
detail view.
Built-in bulk delete
Section titled “Built-in bulk delete”Every crud listing already ships a bulk Delete (selection + confirmation) wired to
CrudStore.deleteAllById(List<IdType>) — no code needed.
Other servers
Section titled “Other servers”Same wire contract from .NET and Python — with the caveat that the appearance and confirmation-text
knobs above are Java only for now: the ports’ [Toolbar] / @toolbar style a page-toolbar
button only (a list-toolbar button keeps the renderer’s default look), and neither port carries
confirmation texts on the wire (no action of theirs can declare them, bulk or not).
public class InvoicesCrud : Crud<Invoice>{ // … Fetch / Get / Save as in any Crud …
[ListToolbarButton] [Label("Approve selected")] public Message Approve(List<Invoice> selection) => new Message($"{selection.Count} approved");}class InvoicesCrud(Crud[Invoice]): # … fetch / get / save as in any Crud …
@list_toolbar_button("Approve selected") def approve(self, selection: list[Invoice]): return Message(f"{len(selection)} approved")Principles served
Section titled “Principles served”- Workflow over screens — the operation happens where the user already is: the listing.
- Recoverability — destructive bulk actions can require confirmation.
Related
Section titled “Related”- Filters & Listing — finding the rows to act on
- Task queue — when each item needs individual attention instead