Skip to main content

Row Actions and Bulk Mutation

A view table can offer per-row actions (a row menu), row selection, and, as an independent opt-in, bulk mutation. All of the types below live in org.apache.juneau.console.views.

RowAction​

A RowAction is one row-menu item. On activation juneau-views.js submits the declared endpoint with the declared method as a JSON request carrying the process's CSRF token.

FieldMeaning
idRequired. Stable action id.
label, iconMenu text and an optional glyph name resolved by the icon registry.
endpointThe URL to submit to. May carry {property} tokens filled in from the row.
methodPOST, PUT, PATCH or DELETE.
confirmOptional confirmation prompt.
formOptional form-source URL supplying input fields; may carry {property} tokens. Blank means a confirm-only action.
presentpage, dialog or inline (RowAction.Present).
onSuccessredraw, mergeRow or navigate (RowAction.OnSuccess).
enabledWhenRow-state rules that must all match for the action to render enabled.
visibleWhenRow-state rules that must all match for the action to be offered at all.
RowAction ack = RowAction.create("ack")
.label("Acknowledge")
.endpoint("servlet:/incidents/{id}/ack")
.method(RowAction.Method.POST)
.confirm("Acknowledge this incident?")
.present(RowAction.Present.DIALOG)
.onSuccess(RowAction.OnSuccess.MERGE_ROW)
.enabledWhen("state", Op.NE, "resolved", "Already resolved");

Points worth knowing:

  • RowAction.Method contains only the four non-safe methods, so a mutating action bound to GET cannot be expressed. On the server, mutating operations are checked for the same rule at startup.
  • A {property} token that cannot be resolved from the row refuses the submission instead of sending a malformed request. A value with no token is used exactly as written.
  • enabledWhen is presentation only. A gated action is shown disabled with the given reason. The first matching rule wins, and a field the row does not carry fails closed. Op is EQ, NE, PRESENT or ABSENT.
  • Prefer disabled, and hide only when the action is irrelevant to the row. visibleWhen removes the action from that row's menu and pills when its rules do not all match; it is presentation only, and a field the row does not carry fails closed (the action is hidden).

Action results​

An action endpoint answers with an ActionResult (contractVersion "1"), which the runtime renders into the row's own outcome banner.

outcomeMeaning
successThe write happened.
failureThe write failed.
refusalThe server declined, with a refusalCode.
unknownThe outcome could not be determined.
cancelledA cancelled operation that had no effect.
cancelled-after-effectCancelled, but some effect had already happened.

The result can also carry replay (the duplicate of an earlier submit), a message, the updated row for mergeRow, and a resultForm hint.

IdempotencyKey​

IdempotencyKey is a server-minted key for a row-action submit, bound at mint time to the (action, targetId) pair it was issued for. The modal mints it when it opens, so a double-click, an impatient re-submit and a browser retry all carry the same key and collapse to one effect.

IdempotencyKey key = IdempotencyKey.mint("ack", incidentId);
// At submit: a key bound to a different (action, target) is a refusal, never a replayed success.
if (! recorded.matches(submittedAction, submittedTargetId))
return ActionResult.refusal("write-guard:idempotency-binding-mismatch");

The value is 32 random bytes from SecureRandom, hex-encoded. IdempotencyKey.mintSelfTargeted(action) covers a row-less dialog opened from a ribbon, where the key's target is its own value.

SelectionDef and BulkMutateDef​

Selection and bulk mutation are two separate opt-ins.

SelectionDef adds a checkbox column, and optionally a select-all header, to a table. It does nothing else: it cannot surface a bulk-mutate control, so a table can be "selectable for export" while categorically excluding bulk writes. Selection is keyed by a stable row id (SelectionDef.create(rowIdField)), never a DOM index, so a sort, page change or poll refresh cannot retarget a selection.

BulkMutateDef is the bulk-mutate opt-in. It has no zero-argument form.

SelectionDef selection = SelectionDef.create("id").selectAll(true);
WritePermit permit = WritePermit.forCapability("incident:bulk-ack"); // mint only after YOUR write gate passes
BulkMutateDef bulk = BulkMutateDef.create(permit, selection)
.actions(RowAction.create("ack")
.endpoint("servlet:/incidents/{id}/ack")
.method(RowAction.Method.POST));
  • A WritePermit is a proof-of-authorization token, not an enforcement seam. Your own write gate must have approved the capability before you mint one; omitting it is a compile error.
  • A bulk action runs in one of two modes, chosen per action with RowAction.bulkMode(RowAction.BulkMode). The default, BulkMode.PER_ROW (wire token perRow), runs N independent per-row submissions of the same single-action path, at most four in flight at a time, each with its own CSRF POST, in-flight marker and ActionResult. BulkMode.AGGREGATE (wire token aggregate) sends a single request carrying every selected id. Either way the run ends in one summary toast, ids that succeeded or were not found leave the selection, and ids that failed stay selected so the user can retry.
  • An aggregate action POSTs a BulkRequest as JSON to the action's endpoint: ids (the stable row ids) and idempotencyKey, a client-generated token unique to that submit. The endpoint answers with a BulkResult: a contractVersion ("1", its own contract, independent of the view, ActionResult and BulkMutateDef versions) and three lists, succeeded (ids), notFound (ids that no longer exist) and failed (an id plus a message each). The outcome is reported per id, so a partial failure is never hidden behind an overall success. A transport error, a non-2xx status, or a body that is not a BulkResult of the expected version is reported as a whole-run failure and leaves the selection intact.
  • The server is responsible for making an aggregate endpoint idempotent on idempotencyKey (a retried submit with the same key should return the same BulkResult rather than apply twice); the framework only generates and sends the key. Source: BulkRequest, BulkResult and RowAction.BulkMode in juneau-console-views; the petstore's ChangesPageBuilders and ChangesRest pair an aggregate Discard with a perRow Apply (see Pending Changes).
  • The bulk action list is wire data with its own BulkMutateDef.CONTRACT_VERSION ("1"), independent of the view metadata version.

How the beans reach the page​

RowAction, SelectionDef and BulkMutateDef are the wire-contract and server-side types. SelectionDef is stamped by ViewTable as plain HTML attributes on the emitted <table> (ViewTable.SELECT_ATTR, ROW_ID_FIELD_ATTR, SELECT_ALL_ATTR), not as JSON.

The <@card type="datatables"> catalog lift documented on Catalog and Columns currently copies a fixed list of keys and does not include rowActions, selection or bulk. A page that needs these features today builds its table with the ViewDef / ViewTable API rather than the FreeMarker catalog. This may change; confirm against the release you are on.

See also Security Model for the CSRF and loopback rules every action submit passes through.