Class ActionSpec
EntityView.actions(ActionSpec).
Each action is a labelled, icon'd button placed on the list (toolbar or per-row) or the record detail. It does one of two things when clicked:
- a server handler —
.handler(ctx -> ...)runs arbitrary backend logic and returns anActionResult(typed feedback and/or refresh); or - a navigation —
.navigate("onno://...")just routes the client (a{id}placeholder is filled with the row/record id).
public void actions(ActionSpec a) {
a.action("archive").label("Archive").icon("archive").scope(ActionScope.ROW)
.handler(ctx -> { repo.archive(ctx.id()); return ActionResult.refresh(ActionToast.success("Archived")); });
a.action("report").label("Open report").icon("file-text").scope(ActionScope.TOOLBAR)
.navigate("onno://reports/occupancy");
}
A row action's icon, label, visibility and enabled state may be a function of the row
instead of fixed, so one control adapts to each record (a pause "Suspend" on a running
row, a play "Resume" on a stopped one; a button shown only where it applies). Pass a
ActionRow-taking function/predicate; it's evaluated per row as the list renders:
a.action("suspend").scope(ActionScope.ROW)
.label("Change running state")
.label(row -> row.enumValue("status", Status.class) == Status.STOPPED ? "Resume" : "Suspend")
.icon(row -> row.enumValue("status", Status.class) == Status.STOPPED ? "play" : "pause")
.visibleWhen(row -> row.enumValue("status", Status.class) != Status.ARCHIVED)
.handler(ctx -> { service.toggle(ctx.id()); return ActionResult.reload(); });
The per-record functions apply to ActionScope.ROW actions (evaluated against each row
as the list renders) and to ActionScope.DETAIL actions (evaluated against the loaded
record as the detail surface renders) — so one detail-header button can hide, relabel or disable
itself by the record's state, mirroring the row button. On ActionScope.TOOLBAR they're
ignored in favour of the fixed icon/label (a toolbar has no record context).
A server ActionScope.ROW action is also available to batch selection. Because the
batch menu and its progress messages have no single row context, they use the fixed
label; when a dynamic label is declared, provide a
human-facing fixed label as well or those surfaces fall back to the action key. If applying one
action to a mixed-state selection would be ambiguous, declare separate deterministic actions
(for example, "Suspend" and "Resume") instead of a state-toggling batch action.
A server action may also declare a form — the click then opens a modal dialog that
collects the declared fields before the handler runs; the values arrive as
ActionContext.input(String). The classic case: a "Cancel" action that asks for a
reason:
a.action("cancel").label("Cancel order").icon("ban").scope(ActionScope.DETAIL)
.form(f -> f.input("reason").label("Reason").type(InputType.TEXTAREA)
.placeholder("Why is this order cancelled?").required())
.handler(ctx -> { service.cancel(ctx.id(), ctx.input("reason"));
return ActionResult.refresh(ActionToast.success("Cancelled")); });
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic final recordA resolved action button.static final classFluent builder for one action; setters may be called in any order.static final recordValues an action form opens with, computed server-side at open time by theformDefaultshook:valuesseed the scalar inputs (keyed by input key),rowsseed the row groups (keyed by group key, each row acolumn → valuemap — the same wire shape the form submits back). -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionStart declaring an action with the given unique key.actions()Static declarations only.dynamic(Consumer<ActionSpec> provider) Declare actions whose keys and presentation come from live business data.booleanWhether this spec contains at least one late-bound action provider.Resolve static and late-bound declarations in authored order.
-
Constructor Details
-
ActionSpec
public ActionSpec()
-
-
Method Details
-
action
Start declaring an action with the given unique key. -
dynamic
Declare actions whose keys and presentation come from live business data. The provider is retained at startup and evaluated when a dynamic entity menu opens and again when an action key executes; it is not a cache or an application-startup callback.Use this for direct menu choices backed by an editable catalog (statuses, assignees, queues, and similar lists). Each evaluation receives a fresh
ActionSpec, so declare ordinary actions with the same builder API, including forms, roles, per-row state, batch handlers, colors, logos, and navigation:actions.dynamic(live -> { for (Status status : statuses.all()) { live.action("setStatus_" + status.id()) .scope(ActionScope.ROW).menu("Change status") .label(status.label()).color(status.color()) .handler(ctx -> setStatus(ctx.id(), status.id())); } });The provider should be read-only and fast enough for a menu-open request. Declaration order is preserved, and the first declaration of a duplicate key still wins when static and dynamic actions from all views are merged.
-
actions
Static declarations only. Dynamic providers are deliberately not evaluated here. -
hasDynamicActions
public boolean hasDynamicActions()Whether this spec contains at least one late-bound action provider. -
resolveActions
Resolve static and late-bound declarations in authored order. Framework runtime code calls this per menu-open/action execution only whenhasDynamicActions()is true.
-