DEV3 Technologies home

Filament approve and reject actions with reason

Reusable Filament 4 table actions that approve or reject pending records, require a reason on rejection and guard the state transition.

FilamentPermissionsfilamentworkflowapprovalactions

Code

<?php

namespace App\Filament\Actions;

use App\Models\Concerns\Approvable;
use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Model;

/** Use in recordActions([...]) of any table whose model uses the Approvable trait. */
class ApprovalActions
{
    public static function approve(): Action
    {
        return Action::make('approve')
            ->icon('heroicon-o-check-circle')
            ->color('success')
            ->requiresConfirmation()
            ->visible(fn (Model $record): bool => $record->isPending() && auth()->user()->can('approve', $record))
            ->action(function (Model $record): void {
                $record->approve(auth()->user());

                Notification::make()->title('Approved')->success()->send();
            });
    }

    public static function reject(): Action
    {
        return Action::make('reject')
            ->icon('heroicon-o-x-circle')
            ->color('danger')
            ->modalHeading('Reject this item')
            ->schema([
                // The reason is stored and shown to the author, so it is mandatory.
                Textarea::make('reason')->required()->minLength(10)->maxLength(500),
            ])
            ->visible(fn (Model $record): bool => $record->isPending() && auth()->user()->can('approve', $record))
            ->action(function (Model $record, array $data): void {
                $record->reject(auth()->user(), $data['reason']);

                Notification::make()->title('Rejected')->warning()->send();
            });
    }
}

Needs: filament/filament@^4.0

How it works

What it solves

Proposals, listings, documents and payouts often need a human sign-off. The interface part is simple, two buttons, but the details matter: a rejection must carry a reason, buttons must disappear when an item is no longer pending, and two reviewers must not both succeed on the same row.

How it works

ApprovalActions exposes two static builders that return Filament Action objects, so any resource can drop them into recordActions(). Both use visible() to show only for pending records and only when the policy ability approve passes. Approve asks for a simple confirmation. Reject opens a modal with a required, length-limited reason field, and the text arrives in $data.

The state change lives in the Approvable trait rather than in the action. transition() runs inside a transaction and uses a conditional update, where status = pending. The affected-row count tells whether this call won; zero means someone else already decided, and an exception is thrown, which Filament shows as an error. That makes the workflow safe without explicit locks.

After a successful change, the model is refreshed so the table and any following notification see the new status, reviewer and timestamps.

Adapting it

Add more states by extending transition(). Send mail or database notifications to the author after approve() or reject(). For two-step approval, add an approver_level check inside the policy.

Pitfalls

  • The visibility closure is not security. Always enforce the same rule in the policy that the action and any API endpoint use.
  • Throwing a bare RuntimeException shows a generic error; catch it in the action and send a friendly notification if you prefer.
  • Log the reason in an audit table if rejections must be traceable after later edits.

How to use it

  1. Add status (default pending), reviewed_by, reviewed_at and rejection_reason columns to your table.
  2. Add use Approvable; to the model and create an approve ability in its policy.
  3. Put the actions in recordActions([ApprovalActions::approve(), ApprovalActions::reject()]).
  4. Open the list as a reviewer: pending rows show both buttons.
  5. Notify the author after a decision by adding a notification call in the action closures.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets