DEV Community

Cover image for Improve Filament Import UX with Persistent Error CSV Downloads
yebor974 for Filament Mastery

Posted on • Originally published at filamentmastery.com on

Improve Filament Import UX with Persistent Error CSV Downloads

By default, FilamentPHP provides a convenient import action with built-in feedback once the process is complete.

After an import finishes, a notification is displayed showing the result, along with a link to download a CSV file containing failed rows.

While this works well for simple use cases, it quickly becomes limiting in real-world applications:

  • Only the user who initiated the import can access the error file
  • The download link disappears once the notification is dismissed
  • There is no built-in way to view past imports or retry analysis

๐Ÿ‘‰ In a team environment, this can become frustrating very quickly.

In this article, we'll implement a simple and clean solution to:

  • List all imports in a Filament resource
  • Allow authorized users to download error CSV files
  • Manage and delete past imports

Understanding Filament's Default Behavior

Filament already provides an internal model to handle imports:

Filament\Actions\Imports\Models\Import
Enter fullscreen mode Exit fullscreen mode

This model includes several useful attributes:

  • completed_at โ†’ timestamp
  • processed_rows โ†’ integer
  • total_rows โ†’ integer
  • successful_rows โ†’ integer

It also exposes computed values like:

$import->getFailedRowsCount()
Enter fullscreen mode Exit fullscreen mode

Filament also defines an internal route used to download failed rows:

Route::get('/imports/{import}/failed-rows/download', DownloadImportFailureCsv::class)
    ->name('imports.failed-rows.download');
Enter fullscreen mode Exit fullscreen mode

You don't need to override this route, we'll simply control access to it.

The Access Problem

By default, if no Policy is defined, Filament restricts access like this:

$importPolicy = Gate::getPolicyFor($import::class);

if (filled($importPolicy) && method_exists($importPolicy, 'view')) {
    Gate::forUser($user)->authorize('view', Arr::wrap($import));
} else {
    abort_unless($import->user()->is($user), 403);
}
Enter fullscreen mode Exit fullscreen mode

Extract of Filament\Actions\Imports\Http\Controllers\DownloadImportFailureCsv Controller

This means:

  • If no policy exists โ†’ only the owner can download the file
  • If a policy exists โ†’ Filament defers authorization to it

So the solution is simple: define a Policy.

Creating the Import Policy

Generate the policy:

php artisan make:policy ImportPolicy --model="Filament\Actions\Imports\Models\Import"
Enter fullscreen mode Exit fullscreen mode

Here is an example implementation:

namespace App\Policies;

use App\Enums\ImportImporterEnum;
use App\Enums\PermissionEnum;
use App\Models\User;
use App\Services\PermissionService;
use Filament\Actions\Imports\Models\Import;
use Illuminate\Auth\Access\HandlesAuthorization;

class ImportPolicy
{
    use HandlesAuthorization;

    public function __construct(protected PermissionService $permissionService) {}

    public function viewAny(User $user): bool
    {
        return $user->hasPermissionTo($this->permissionService->getPermissionName(PermissionEnum::VIEW_ANY, Import::class));
    }

    public function view(User $user, Import $import): bool
    {
        if (is_null($import->completed_at) || $import->getFailedRowsCount() == 0) {
            return false;
        }

        return $user->hasPermissionTo($this->permissionService->getPermissionName(PermissionEnum::VIEW, Import::class));
    }

    public function delete(User $user, Import $import): bool
    {
        if ($import->importer == ImportImporterEnum::PRODUCT_MAPPING->value) {
            return false;
        }

        return $user->hasPermissionTo($this->permissionService->getPermissionName(PermissionEnum::DELETE, Import::class));
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, permissions are handled using a role/permission system (e.g. Spatie Laravel Permission) with a own Permission Service and Enum. Adapt this logic to your own application.

Ideally, Filament would use a dedicated download method instead of view, as these represent different concerns. However, we'll stick with the default behavior for simplicity.

Registering the Policy

Don't forget to register the policy in your service provider:

use Filament\Actions\Imports\Models\Import;

public function boot(): void
{
    Gate::policy(Import::class, ImportPolicy::class);
    ...
}
Enter fullscreen mode Exit fullscreen mode

Creating the Filament Resource

Now letโ€™s expose imports in the admin panel:

php artisan make:filament-resource Import --model-namespace=Filament\\Actions\\Imports\\Models
Enter fullscreen mode Exit fullscreen mode

We donโ€™t need forms here, only a listing page. So you can delete Schemas format and some pages like CreateImport and EditImport

Improve Filament Import UX with Persistent Error CSV Downloads

Resource class

class ImportResource extends Resource
{
    protected static ?string $model = Import::class;

    public static function table(Table $table): Table
    {
        return ImportsTable::configure($table);
    }

    public static function getPages(): array
    {
        return [
            'index' => ListImports::route('/'),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Simplified Import Resource

Building the Imports Table

Here's a simplified version of the table:

class ImportsTable
{
    public static function configure(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('file_name')->searchable(),
                TextColumn::make('importer'),
                TextColumn::make('user.name'),
                TextColumn::make('total_rows')->numeric(),
                TextColumn::make('processed_rows')->numeric(),
                TextColumn::make('successful_rows')->numeric(),
                TextColumn::make('completed_at')->sortable(),
            ])
            ->recordActions([
                ActionGroup::make([
                    Action::make('downloadFailedRowsCsv')
                        ->label('Download errors')
                        ->color('warning')
                        ->icon(Heroicon::ArrowDownCircle)
                        ->url(fn (Import $import) => URL::signedRoute('filament.imports.failed-rows.download', ['authGuard' => filament()->getAuthGuard(), 'import' => $import], absolute: false), shouldOpenInNewTab: true)
                        ->authorize('view'),

                    DeleteAction::make(),
                ]),
            ])
            ->defaultSort('created_at', 'desc');
    }
}
Enter fullscreen mode Exit fullscreen mode

Simplified ImportsTable

Key Feature: Downloading Failed Rows

The most important part is this action:

Action::make('downloadFailedRowsCsv')
Enter fullscreen mode Exit fullscreen mode

It:

  • Uses Filament's internal signed route
  • Relies on the view policy for authorization

๐Ÿ‘‰ Combined with the policy, this ensures:

  • Only authorized users can access error files
  • Downloads remain secure
  • UX is significantly improved

Final Result

With just a Policy and a Resource, you now have:

  • A complete history of imports
  • Persistent access to error CSV files
  • Team-friendly access control
  • A cleaner and more professional admin experience

Improve Filament Import UX with Persistent Error CSV Downloads
Import's actions with failed rows

Improve Filament Import UX with Persistent Error CSV Downloads
Import's actions without failed rows

Conclusion

Filament provides powerful building blocks, but some features, like import history, require a bit of customization to fully shine.

With this approach, you enhance both usability and maintainability without adding unnecessary complexity.

From here, you could go further by adding:

  • Retry mechanisms
  • Import status filters
  • etc.

๐Ÿš€ Want to go further?

More articles on Filament Mastery

Iโ€™m sharing production-ready Laravel & Filament setups too (Docker, CI/CD, deploymentโ€ฆ).
Join the Early Supporters tier for full access

Top comments (0)