waad/filament-import-wizard

A powerful, queue-powered CSV and Excel import wizard for Filament with smart column mapping, relationship linking, and background processing for 100K+ rows.

Maintainers

Package info

github.com/waadmawlood/filament-import-wizard

pkg:composer/waad/filament-import-wizard

Statistics

Installs: 457

Dependents: 0

Suggesters: 0

Stars: 5

Open Issues: 1

v1.1.0 2026-05-20 23:50 UTC

This package is auto-updated.

Last update: 2026-05-20 23:56:21 UTC


README

A powerful, queue-powered CSV and Excel import wizard for Filament with smart column mapping, relationship linking, Spatie Translatable support, locale merge, dynamic schema validation, and background processing for 100K+ rows.

Dark Mode Support PHP 8.2+ Laravel 10+ Filament 4.x/5.x License: MIT

2026-05-21.02.35.19.mp4

✨ Features

  • 4-Step Import Wizard — Upload, map columns, review data, and import inside a Filament modal
  • Reverse Mapping UI — Model fields shown first with CSV column dropdowns, not the other way around
  • Smart Auto-Mapping — Automatically maps CSV headers to model fields using snake_case matching (e.g., Titletitle, CategoryIdcategory_id)
  • Relationship Linking — Link or create related records (BelongsTo) with intelligent auto-increment ID handling
  • Spatie Translatable Support — Detect HasTranslations traits and JSON columns; supports whereJsonContains() for cross-DB translated lookups
  • Locale Merge — Combine multiple language columns (e.g., title_en, title_ar) into a single JSON translatable field with checkbox toggle
  • Locale-Aware Relation Lookup — Find or create related models using translated column values
  • Upsert Support — Update existing records instead of creating duplicates via configurable keys
  • Queue-Powered Processing — Chunked batch execution with live progress tracking for large datasets
  • Eloquent Event Firing — Inserts fire model boot(), observers, timestamps, and lifecycle hooks via individual save()
  • Dynamic Schema Validation — Auto-generate validation rules from database column types (required, numeric, boolean, date, email, unique)
  • Error Review — Inline validation and error download before import
  • Multi-Tenancy Ready — Built-in team and tenant scoping across queue boundaries
  • Standalone Mode — Use as a Livewire component outside Filament panels
  • Dark Mode — Full dark theme support out of the box
  • Custom Queue Configuration — Configurable queue connection and queue name per import
  • Excel Empty Column Trimming — Automatically strips trailing empty Excel columns (no more 256-column UI)

📦 Installation

composer require waad/filament-import-wizard

Publish Configuration & Migrations

# Publish config file (optional)
php artisan vendor:publish --tag="filament-import-wizard-config"

# Publish and run migrations
php artisan vendor:publish --tag="filament-import-wizard-migrations"
php artisan migrate

⚠️ If there are errors for CSS, try rebuilding Filament assets: php artisan filament:assets

🚀 Usage

Basic Usage in Filament Resource

Add the import action to your Filament resource's table. Example (app/Filament/Resources/Posts/Pages/ListPosts.php):

use Waad\FilamentImportWizard\Actions\ImportWizardAction;

protected function getHeaderActions(): array
{
    return [
        ImportWizardAction::make(),
    ];
}

Advanced Configuration

ImportWizardAction::make()
    ->forModel(\App\Models\Product::class)
    ->chunkSize(500)               // Process 500 rows per job
    ->enableUpsert(true)           // Update existing records
    ->upsertKeys(['sku'])          // Match by SKU field
    ->queueConnection('redis')     // Custom queue connection
    ->queueName('imports');        // Custom queue name

Standalone Usage

Use the wizard outside of Filament panels via Livewire:

@livewire('filament-import-wizard', ['modelClass' => \App\Models\Product::class])

⚙️ Configuration

// config/filament-import-wizard.php

return [
    'modal_width' => Width::Full,        // Modal width (Filament Width enum)
    'chunk_size' => 1000,                // Rows per queue job
    'default_csv_delimiter' => ',',      // CSV delimiter (comma, semicolon, tab)
    'queue_connection' => null,          // Queue connection (null = default)
    'queue_name' => null,                // Queue name (null = default)
];

Configuration Options

Option Default Description
modal_width Width::Full Width of the import wizard modal
chunk_size 1000 Number of rows processed per queue job
default_csv_delimiter , Default CSV delimiter for parsing
queue_connection null Queue connection to use (null = Laravel default)
queue_name null Specific queue name (null = default queue)

📋 Import Steps

Step 1: Upload

Upload your CSV or Excel file. Supported formats:

  • CSV (.csv) — with UTF-8 BOM auto-detection
  • Excel (.xlsx, .xls) — trailing empty columns are automatically trimmed

Step 2: Map Columns

The mapping step presents model fields first, with a clean table-based layout:

  • Model Fields Section — Each model field is displayed with its label, code name, and a dropdown to select which CSV column maps to it
  • Relations Section — Each BelongsTo relation is shown with its FK/PK badges and a searchable field dropdown
  • Unmapped Columns Section — CSV columns that are not mapped appear at the bottom

Locale Merge (Translatable Fields)

For translatable/JSON columns, toggle the Merge Translation switch to split a field into multiple locale→column mappings:

Title (translatable)  →  Merge Translation ON
  [en] → title_en
  [ar] → title_ar
  [fr] → title_fr

The locale can be auto-detected from header names (title_en, titleAr, etc.) or entered manually. This stores data as JSON: {"en": "Hello", "ar": "مرحبا"}.

Relation Mapping

For BelongsTo relations:

Category → [Select CSV column...]
           FK: category_id  PK: id
           [Search fields...]  [name ▼]

Relations support auto-increment ID detection (if CSV has numeric IDs) and intelligent fallback to string field matching.

Step 3: Review & Validate

Preview your data before import:

  • View first 100 rows with mapped columns
  • See validation errors per row with error badges
  • Dynamic rules auto-generated from database schema (column types, nullable, unique)
  • Relation foreign keys bypass type constraints in validation (labels are resolved later)
  • Configure upsert settings (enable/disable, keys)

Step 4: Import

Start the import process:

  • Background queue processing with live progress polling every 5 seconds
  • Sync queue runs jobs immediately for testing
  • Eloquent mode (non-upsert): Records are saved individually via save(), firing boot(), events, and observers
  • Upsert mode: Bulk upsert() with fallback to chunked inserts on failure
  • Error tracking with downloadable CSV error logs
  • Final summary with animated success/error counts and completed_with_errors status

🔗 Relationship Linking

Link related records during import:

// Example: Import products and link to categories
CSV Column: "Category Name" → Relation: category → Field: name

Supported relationship types:

  • BelongsTo — with automatic FK/PK resolution

How Relation Resolution Works

  1. If the owner key is an auto-incrementing primary key:
    • Numeric CSV values → find or create by ID
    • String CSV values → find or create by name field, or auto-detect a suitable string column
  2. If the owner key is a non-ID column → direct lookup by that column
  3. Translatable/locale-aware resolution via whereJsonContains() for JSON fields

🔃 Upsert (Match & Merge)

Update existing records instead of creating duplicates:

ImportWizardAction::make()
    ->forModel(\App\Models\User::class)
    ->enableUpsert(true)
    ->upsertKeys(['email']);  // Match users by email

The wizard will:

  1. Find existing records by the specified keys
  2. Update matching records via upsert() (with timestamp injection)
  3. Create new records only when no match is found
  4. Fall back to chunked inserts on upsert failure

🌍 Spatie Translatable & Locale Merge

Automatic Detection

The wizard automatically detects:

  • Models using Spatie\Translatable\HasTranslations trait
  • JSON/JSONB columns from the database schema

Locale Merge Mode

Check Merge Translation on any translatable field to split it into locale-specific mappings:

CSV Header Maps To
title_en title with locale en
title_ar title with locale ar
title_fr title with locale fr

The result is stored as a single JSON column: {"en": "value", "ar": "قيمة", "fr": "valeur"}

Custom Import Field Labels

Add a getImportFieldLabel() method to your model for custom display names:

class Product extends Model
{
    public function getImportFieldLabel(string $field): string
    {
        return match($field) {
            'sku' => 'SKU (Stock Keeping Unit)',
            'price_in_cents' => 'Price (cents)',
            default => Str::title(str_replace(['_', '.'], ' ', $field)),
        };
    }
}

🧠 Smart Defaults & Guarded Model Support

The wizard handles models with $guarded = [] (no fillable defined) gracefully:

  • Dynamic fillable resolution: Falls back to Schema::getColumnListing() when getFillable() is empty
  • All columns allowed: When fillable is empty, any CSV column can map to any database column
  • Foreign key resolution: Bypasses fillable checks when model is fully unguarded

🛠️ Customization

Custom Modal Width

use Filament\Support\Enums\Width;

ImportWizardAction::make()
    ->forModel(\App\Models\Product::class)
    ->setModalWidth(Width::ExtraLarge);

Queue Configuration

Set queue connection and name globally via config:

// config/filament-import-wizard.php
return [
    'queue_connection' => 'redis',
    'queue_name' => 'imports',
];

Or per import action:

ImportWizardAction::make()
    ->forModel(\App\Models\Product::class)
    ->queueConnection('redis')
    ->queueName('imports');

Custom Validation Rules

Add a getImportRules() method to your model for custom import validation:

class Product extends Model
{
    public function getImportRules(): array
    {
        return [
            'sku' => ['required', 'string', 'max:50'],
            'price' => ['required', 'numeric', 'min:0'],
            'stock' => ['nullable', 'integer', 'min:0'],
        ];
    }
}

If no getImportRules() method exists, rules are auto-generated from the database schema.

📝 Requirements

  • PHP: 8.2+
  • Laravel: 10+
  • Filament: 4.x or 5.x

📄 License

The MIT License (MIT). Please see License File for more information.

🤝 Contributing

Contributions are welcome! Please open an issue or submit a pull request.

📧 Support

If you discover any bugs or have feature requests, please open an issue on GitHub.

Screenshots

Screenshot Screenshot Screenshot Screenshot