Package su.onno.ui

Class ActionSpec

java.lang.Object
su.onno.ui.ActionSpec

public final class ActionSpec extends Object
Declares custom action buttons for an entity, from 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 an ActionResult (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")); });
 
  • Constructor Details

    • ActionSpec

      public ActionSpec()
  • Method Details

    • action

      public ActionSpec.ActionBuilder action(String key)
      Start declaring an action with the given unique key.
    • dynamic

      public ActionSpec dynamic(Consumer<ActionSpec> provider)
      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

      public List<ActionSpec.Action> 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

      public List<ActionSpec.Action> resolveActions()
      Resolve static and late-bound declarations in authored order. Framework runtime code calls this per menu-open/action execution only when hasDynamicActions() is true.