Skip to content
laravel-nova-examples.com

Build an action with input fields, confirmation and authorization

A complete Nova 5 action that asks for a tracking number, confirms before running, only appears for authorized users and only runs on eligible orders.

Nova
5.x
Published
Reading time
3 min

Marking orders as shipped is a typical admin task: the operator selects one or more paid orders, enters a tracking number and confirms. Users without shipping permission should not see the action at all, and orders that are not paid yet must be skipped.

Generate the action

php artisan nova:action MarkAsShipped

The action class

<?php

namespace App\Nova\Actions;

use Illuminate\Support\Collection;
use Laravel\Nova\Actions\Action;
use Laravel\Nova\Actions\ActionResponse;
use Laravel\Nova\Fields\ActionFields;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Http\Requests\NovaRequest;

class MarkAsShipped extends Action
{
    public function handle(ActionFields $fields, Collection $models): ActionResponse
    {
        foreach ($models as $order) {
            $order->forceFill([
                'status' => 'shipped',
                'tracking_number' => $fields->tracking_number,
                'shipped_at' => now(),
            ])->save();

            if ($fields->notify_customer) {
                $order->customer->notify(new \App\Notifications\OrderShipped($order));
            }
        }

        return ActionResponse::message(
            trans_choice(':count order marked as shipped.|:count orders marked as shipped.', $models->count())
        );
    }

    public function fields(NovaRequest $request): array
    {
        return [
            Text::make('Tracking Number')->rules('required', 'max:64'),
            Boolean::make('Notify Customer')->default(true),
        ];
    }
}

Field values are available on ActionFields by their attribute name. Text::make('Tracking Number') gets the attribute tracking_number, so you read it as $fields->tracking_number.

Register it with confirmation and authorization

In the resource, canSee decides whether the action is shown at all, and canRun is checked for every selected model:

use App\Nova\Actions\MarkAsShipped;
use Laravel\Nova\Http\Requests\NovaRequest;

public function actions(NovaRequest $request): array
{
    return [
        MarkAsShipped::make()
            ->confirmText('Mark the selected orders as shipped?')
            ->confirmButtonText('Mark as shipped')
            ->cancelButtonText('Cancel')
            ->canSee(fn ($request) => $request->user()->can('ship', \App\Models\Order::class))
            ->canRun(fn ($request, $order) => $order->status === 'paid'),
    ];
}

Orders for which canRun returns false never reach handle(). That keeps the eligibility rule in one place instead of re-checking the status inside the loop.

Showing an error instead

Return a danger response when the action cannot complete, for example when an external carrier API rejects the request:

return ActionResponse::danger('The carrier rejected the tracking number.');

Messages are rendered as HTML and are not escaped, so wrap any user-provided value in e() before putting it into a message.

Running it in the background

If notifying customers takes time, queue the action by implementing ShouldQueue:

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class MarkAsShipped extends Action implements ShouldQueue
{
    use InteractsWithQueue;
    use Queueable;

    // ...
}

Queued actions run in a worker after the request has finished, so a returned message cannot be shown to the user. If the model uses Nova's Actionable trait, the status of each run appears in the resource's action log.

Reference