Skip to content

Bulk actions

Status: ✅ Implemented

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.

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() to false).
  • The frontend keeps the selected rows in the component state (crud_selected_items); the framework hydrates them typed into any List<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 Message toasts, a PageBanner shows a banner, void/null simply re-runs the search so the listing reflects the changes.

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:

AnnotationSays
@ListToolbarButtonwhere the button goes (a crud has two toolbars — this one and the detail view’s @ViewToolbarButton)
@Toolbarhow it looks — buttonStyle, buttonColor, buttonSize, order
@Actionhow 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 confirmationMessage keeps the framework’s title and Yes/No labels instead of blanking them.
  • The flags are merged, not overridden: @Action.rowsSelectedRequired defaults to false while @ListToolbarButton defaults it to true, so adding an @Action for its texts never disarms the selection guard.
  • @Action.id() is ignored here — the dispatch id belongs to the placement (action-on-row-<method>).
  • Without @Toolbar the button keeps the renderer’s default look, and without @Action it 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.

Every crud listing already ships a bulk Delete (selection + confirmation) wired to CrudStore.deleteAllById(List<IdType>) — no code needed.

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")
  • Workflow over screens — the operation happens where the user already is: the listing.
  • Recoverability — destructive bulk actions can require confirmation.