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.
| Field | Meaning |
|---|---|
id | Required. Stable action id. |
label, icon | Menu text and an optional glyph name resolved by the icon registry. |
endpoint | The URL to submit to. May carry {property} tokens filled in from the row. |
method | POST, PUT, PATCH or DELETE. |
confirm | Optional confirmation prompt. |
form | Optional form-source URL supplying input fields; may carry {property} tokens. Blank means a confirm-only action. |
present | page, dialog or inline (RowAction.Present). |
onSuccess | redraw, mergeRow or navigate (RowAction.OnSuccess). |
enabledWhen | Row-state rules that must all match for the action to render enabled. |
visibleWhen | Row-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.Methodcontains only the four non-safe methods, so a mutating action bound toGETcannot 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. enabledWhenis presentation only. A gated action is shown disabled with the givenreason. The first matching rule wins, and a field the row does not carry fails closed.OpisEQ,NE,PRESENTorABSENT.- Prefer disabled, and hide only when the action is irrelevant to the row.
visibleWhenremoves 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.
outcome | Meaning |
|---|---|
success | The write happened. |
failure | The write failed. |
refusal | The server declined, with a refusalCode. |
unknown | The outcome could not be determined. |
cancelled | A cancelled operation that had no effect. |
cancelled-after-effect | Cancelled, 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
WritePermitis 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 tokenperRow), 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 andActionResult.BulkMode.AGGREGATE(wire tokenaggregate) 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
aggregateaction POSTs aBulkRequestas JSON to the action's endpoint:ids(the stable row ids) andidempotencyKey, a client-generated token unique to that submit. The endpoint answers with aBulkResult: acontractVersion("1", its own contract, independent of the view,ActionResultandBulkMutateDefversions) and three lists,succeeded(ids),notFound(ids that no longer exist) andfailed(anidplus amessageeach). 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 aBulkResultof the expected version is reported as a whole-run failure and leaves the selection intact. - The server is responsible for making an
aggregateendpoint idempotent onidempotencyKey(a retried submit with the same key should return the sameBulkResultrather than apply twice); the framework only generates and sends the key. Source:BulkRequest,BulkResultandRowAction.BulkModeinjuneau-console-views; the petstore'sChangesPageBuildersandChangesRestpair anaggregateDiscard with aperRowApply (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.