Search by

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

Transparency log

Statistics

Installs: 1 983

Dependents: 0

Suggesters: 0

Stars: 10

Open Issues: 2

v1.1.1 2026-08-30 20:07 UTC

This package is auto-updated.

Last update: 2026-08-30 20:10:22 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 mapped field is an auto-incrementing primary key:
    • Numeric CSV values → find or create by ID
  2. Otherwise, lookup uses the mapped field (e.g. code, sku, slug) when it is fillable or the model is unguarded
  3. Legacy fallback: look up by owner key when it is not the auto-increment primary key
  4. 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');

Running the Queue

Non-sync queue connections (database, redis, etc.) require a running worker. Without one, jobs sit unprocessed and the import progress UI never updates.

php artisan queue:work --queue=default

If you set a custom queue_name (for example imports), pass that name:

php artisan queue:work --queue=imports

When using the database queue driver, create and run the jobs table first:

php artisan queue:table
php artisan migrate

For local testing without a worker, set the queue connection to sync so jobs run inline:

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

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
  • Queue worker: A running php artisan queue:work process is required when the queue connection is not sync

📄 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