Actions
Your team needs to publish 40 products at once, or restock one. Actions are async functions for that.
- A row action runs on one record, from the list’s row menu or the detail page.
- A bulk action runs on the rows you select.
actions: [ action .bulk('publish') .confirm('Publish the selected products?') .ability('publish') .handle(async (products, { trx }) => { await Product.query({ client: trx }) .whereIn( 'id', products.map((p) => p.id) ) .update({ status: 'published' })
return { message: `${products.length} products published` } }), // A dialog field, validated by its own VineJS validator action .row('restock') .ability('restock') .fields( [field.number('quantity').section('Stock adjustment').help('Added to the current stock')], restockValidator ) .handle(async (product, { input }) => { product.stock += input.quantity await product.save() return { message: `${product.name} now has ${product.stock} in stock` } }), // A reusable action from azu:action feature,],Permissions
Section titled “Permissions”.ability('publish') checks the publish method on your policy, for each record. Without .ability(), an action checks update.
In a bulk action, records the user can’t touch are skipped and reported. They don’t fail the whole batch.
Options
Section titled “Options”.confirm('…')asks before running..fields([...], validator)asks for input in the action’s dialog. The input goes through the validator you pass, like a form’s. The handler gets the validator’s output asinput..destructive()styles the action like Delete..label('…')names it. By default, the name in sentence case.
Return { message } for a toast, or { redirect } to go somewhere else.
Transactions and errors
Section titled “Transactions and errors”The handler runs in a transaction, with its records bound to it. Run your other queries through trx too. Then, if the handler throws, all its changes roll back and the user sees an error toast.
What the toast says depends on the error:
- An AdonisJS
Exceptionwith a status below 500 shows its message. - Any other error shows a generic message and goes to your log.
The demo’s order resource cancels orders, and refuses to cancel one that has shipped:
if (order.status === 'shipped') { throw new Exception(`Order ${order.number} has shipped, so it can't be cancelled.`, { status: 422, })}Cancelling a shipped order fails the whole batch, including orders the handler already cancelled.
All matching rows
Section titled “All matching rows”Once every row on the page is selected, the bulk toolbar offers Select all N matching. The action then runs on every row the list’s search and filters match, up to 10,000.
- In chunks: 500 records at a time, each chunk in its own transaction. Each record gets the same policy check. Your handler is called once per chunk, so it can’t assume it sees every record at once.
- Partial success: if a chunk fails, it rolls back and the run stops. The chunks before it stay done. The confirm dialog says so before a run of more than 500. The toast reports how many were done, skipped, and not changed. Azu writes that toast, so a handler’s
{ message }isn’t shown.{ redirect }applies only when the whole run succeeds. - Running it again: finished chunks stay done, so running the action again repeats it on those rows. Write handlers that are safe to run twice, or filter so done rows drop out. Filter by status “draft” before publishing, say.
- Which rows: the ones that matched when the run started. Rows added meanwhile are left out. A row that no longer matches when its chunk runs is skipped. If more rows match than the count the user confirmed, nothing runs.
- The limit: with more than 10,000 matching rows, the action is refused before anything changes. Narrow the filters and run it on each part.
Share an action between resources
Section titled “Share an action between resources”Generate a reusable action:
node ace azu:action Feature --bulkImport it from #admin/actions/feature and add it to each resource’s actions. Every resource that uses it needs the ability on its policy, and tsc checks that. This is the demo’s, after giving it its records’ type and its change:
import { action } from '@mezielabs/azu'import type Product from '#models/product'
/** * Feature: a bulk action. Add it to a resource's `actions`; * tsc checks that the resource's policy has a `feature` method, * which Azu calls with the user and each record. * * Generated by `node ace azu:action Feature --bulk`, then given * its records' type and its change. */export default action .bulk('feature') .confirm('Feature the selected products on the storefront?') .ability('feature') .handle(async (products: Product[], { trx }) => { for (const product of products) { // The handler runs in a transaction: run every query through `trx` // (`Model.query({ client: trx })`), so a thrown error rolls back // everything the action did. product.isFeatured = true await product.useTransaction(trx).save() } return { message: `Featured ${products.length} ${products.length === 1 ? 'product' : 'products'}`, } })Delete
Section titled “Delete”When the policy has a delete method, the resource gets a Delete row action and bulk action. Both confirm first. It runs the resource’s beforeDelete hook for each record. It deletes their stored files after the transaction commits.
The confirmation names the record, or the number of selected or matching records, and says it’s permanent.