ImportAction
PandaPanel\Actions\ImportAction memuat record ke dalam resource dari file CSV atau XLSX. Ini adalah factory yang mengembalikan PandaPanel\Actions\Action yang sudah dikonfigurasi, sehingga label, icon, modal, dan authorization tetap dapat Anda ubah seperti action biasa.
Gunakan saat pengguna memiliki spreadsheet dan ingin memasukkan datanya ke aplikasi: daftar user baru, update harga, atau file export dari sistem lain. Action menangani dialog — upload dan mapping kolom — sedangkan class Importer yang Anda berikan menentukan model, kolom, rules, dan arti setiap row.
Contoh minimal yang berfungsi
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Users\Imports;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Actions\Imports\ImportColumn;
use PandaPanel\Actions\Imports\Importer;
final class UserImporter extends Importer
{
/**
* @return class-string<Model>
*/
public static function model(): string
{
return User::class;
}
/**
* @return list<ImportColumn>
*/
public static function columns(): array
{
return [
ImportColumn::make('name')->required()->rules(['string', 'max:255']),
ImportColumn::make('email')
->guess(['e-mail', 'email address'])
->required()
->rules(['email', 'max:255']),
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
use App\Panels\Admin\Resources\Users\Imports\UserImporter;
use App\Panels\Admin\Resources\Users\UserResource;
use PandaPanel\Actions\ImportAction;
$table->headerActions([
ImportAction::make(UserImporter::class, UserResource::class),
]);2
3
4
5
6
7
Tombol Import muncul di atas tabel. Tombol tersebut membuka dialog dengan field file dan satu select untuk setiap kolom, membaca file ketika dialog disubmit, lalu melaporkan berapa row yang berhasil diterima.
Factory
use PandaPanel\Actions\Action;
use PandaPanel\Actions\Imports\Importer;
use PandaPanel\Resources\Resource;
/** @param class-string<Importer> $importer @param class-string<Resource> $resource */
ImportAction::make(string $importer, string $resource): Action;2
3
4
5
6
Factory menerima nama class, bukan closure, karena file besar dapat dibaca oleh queued job di proses lain dan hanya nama class yang dapat dibawa melewati batas proses tersebut.
| Pengaturan | Nilai |
|---|---|
| name | import |
| label | Import |
| icon | upload |
| variant | ActionVariant::Outline |
| modal heading | Import records |
| modal submit label | Import |
| modal width | ModalWidth::Large |
| modal description | Upload a CSV or Excel file, then say which column is which. |
| modal dismissal | closeByClickingAway(false) |
| authorization | $resource::canCreate() |
| handler | ->tableAction() |
Authorization menggunakan canCreate(), bukan canViewAny(): import menulis row baru atau mengubah row, sehingga kemampuan melihat list tidak sama dengan kemampuan menambahkan data. Seluruh konfigurasi di atas menggunakan setter biasa dan dapat dioverride:
use PandaPanel\Actions\ImportAction;
use PandaPanel\Actions\Support\Modal;
ImportAction::make(UserImporter::class, UserResource::class)
->label('Upload users')
->modalHeading('Upload a user list')
->modal(static function (Modal $modal): void {
$modal->description('Columns: Name, Email. Everything else is ignored.');
});2
3
4
5
6
7
8
9
Pengaturan agar modal tidak tertutup ketika klik di luar sebaiknya dipertahankan. Dialog panjang dengan file upload adalah tempat di mana satu klik tidak sengaja paling merugikan pengguna.
Dialog adalah dua langkah dalam satu alur
use PandaPanel\Actions\Enums\SpreadsheetFormat;
use PandaPanel\Forms\Components\FileUpload;
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\Layouts\Section;
FileUpload::make('file')
->label('File')
->disk($importer::disk())
->directory($importer::directory())
->acceptedTypes(array_merge(
SpreadsheetFormat::Csv->mimeTypes(), // text/csv, text/plain, application/csv
SpreadsheetFormat::Xlsx->mimeTypes(), // …spreadsheetml.sheet, application/zip
))
->maxSize(20480) // kilobytes — 20 MB
->required();
Section::make('Columns')
->description('Leave a column blank to skip it. Blank columns are guessed from the headings.')
->columns(2)
->schema(/* one Select per importer column */);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
File di-upload melalui field FileUpload biasa, yang menyimpannya sebelum form disubmit. Submit berikutnya menentukan kolom mana dari file yang mengisi kolom mana pada importer. Urutan ini diperlukan karena heading file baru dapat dibaca setelah file sudah tersedia.
Setiap kolom yang dideklarasikan mendapat select sendiri:
Select::make('map_'.$column->getName())
->label($column->getLabel())
->options(/* col0 => 'A', col1 => 'B', … col199 => 'GR' */)
->searchable()
->helperText($column->isRequired() ? 'Required' : null);2
3
4
5
Options adalah posisi ala spreadsheet, bukan heading file, karena form dibangun sebelum file di-upload. Select yang dibiarkan kosong akan diisi berdasarkan heading oleh ImportRun::guessMapping(); pilihan manual tidak pernah ditimpa oleh hasil guess. Seluruh detailnya dibahas di Kolom dan mapping.
Yang terjadi saat submit
ImportAction menjalankan pemeriksaan berikut secara berurutan dan berhenti pada kegagalan pertama:
| Langkah | Kegagalan |
|---|---|
| authenticated user dengan key yang dapat digunakan | 403 / 500 |
| panel berhasil di-resolve | 500 |
$data['file'] adalah string tidak kosong | 422 No file was uploaded. |
file masih ada pada $importer::disk() | 404 That file is no longer there. |
| setiap kolom required mendapatkan posisi | ValidationException pada file, dan upload dihapus |
| jumlah row menentukan inline atau queued | — |
Penghitungan row XLSX tetap harus membuka workbook sebelum memutuskan inline atau queued. Reader membatasi setiap XML part maksimal 64 MiB, sehingga workbook yang terlalu besar akan gagal sebagai file yang tidak dapat dibaca dengan aman, bukan dialihkan ke worker sebagai cara melewati batas tersebut.
Pemeriksaan required-column dilakukan sebelum satu row pun diproses. Tanpa ini, kolom required yang tidak memiliki heading akan gagal pada setiap row dengan pesan identik — misalnya "The name field is required" sepuluh ribu kali — padahal masalah sebenarnya adalah struktur file. Pesan error menyebut kolom yang hilang dan heading yang benar-benar tersedia:
This file has no column for [email], and it is required. Its headings are: Full Name, Address.
Rename the column in the file, or map it by hand before importing.2
Inline atau queued
$rows = ImportRun::countRows($local); // the file is read to count, never estimated
if ($importer::queueAfter() >= 0 && $rows > $importer::queueAfter()) {
// PandaPanel\Jobs\RunPanelImport is dispatched.
}2
3
4
5
queueAfter() | Behavior |
|---|---|
0 | selalu queued |
500 (default) | queued jika lebih dari 500 row |
| negatif | tidak pernah queued |
Queued import langsung menampilkan toast informatif lalu request selesai:
Inertia::flash('toast', [
'type' => 'info',
'message' => 'Your import has started. You will be notified when it finishes.',
]);2
3
4
Inline import membaca file, menghapus upload, lalu melaporkan hasil. Lihat Queued import untuk jalur lainnya.
Hasil dari inline import
$result = ImportRun::run($importer, $local, $mapping, $owner);
// ['imported' => 998, 'failed' => 2, 'report' => 'failed-rows-2026-08-15-114233.csv']2
- Toast bertipe
successjika tidak ada kegagalan danwarningjika ada row gagal. Pesannya menggunakan$importer::completedMessage($imported, $failed). - Notification persistent hanya dikirim ketika ada kegagalan, dengan link Download failed rows. Import bersih cukup diberi toast pada response — notification center yang penuh pesan "imported 40 rows" akan kehilangan nilainya.
- URL report dibangun dengan
route($panel->routeName('import-file'), ['file' => $report, 'importer' => $importer], absolute: false).
Lihat Failure report dan Notification import dan export.
Download row yang gagal
GET {panel}/imports/{file}?importer=App\Panels\Admin\…\UserImporter
route name: panel.{panelId}.import-file2
PandaPanel\Http\Controllers\PanelImportController menerapkan rule yang sama seperti download export: 403 tanpa authenticated user, 404 jika file mengandung /, \, atau .., 404 jika importer bukan subclass PandaPanel\Actions\Imports\Importer, dan path dibangun sebagai {$importer::directory()}/{$user->getAuthIdentifier()}/{$file} berdasarkan pengguna yang sedang meminta. Failure report adalah salinan data yang seseorang coba import, sehingga harus dilindungi sama ketatnya seperti file export.
Lokasi upload
Upload dikirim ke endpoint uploads milik panel, yang membaca disk dan directory dari deklarasi field itu sendiri — request tidak pernah menentukan keduanya. Karena field merupakan bagian dari form action, upload diauthorize sebagai action tersebut, bukan sebagai create form milik resource. Path tersimpan adalah {$importer::directory()}/{random}.{ext}, dan file dihapus:
- setelah inline import selesai;
- ketika pemeriksaan required-column menolak file;
- oleh
RunPanelImportsaat berhasil maupun gagal.
Satu-satunya kasus yang tidak otomatis dibersihkan adalah upload yang sudah dipilih lalu dialog ditutup tanpa submit — lihat Storage dan cleanup.
Gotchas
- Disk importer harus local. Reader menerima
Storage::disk($importer::disk())->path($stored)lalu membukanya denganfopen()/ZipArchive. Driver seperti S3 yang tidak menyediakan path filesystem yang dapat dibaca tidak dapat digunakan. Defaultlocalsudah tepat. - Batas upload adalah 20 MB, berasal dari
maxSize(20480). Batas diterapkan upload endpoint terhadap file nyata, bukan hanya berdasarkan klaim browser.ImportActiontidak menyediakan setter untuk menaikkan batas ini; file lebih besar memerlukan jalur import yang berbeda. ImportAction::make()adalah table action. Factory tidak mendeklarasikan record handler, sehingga tempatnya diheaderActions()atautoolbarActions(). Jika ditempatkan direcordActions(), action tidak dapat dieksekusi.- Kolom yang dibiarkan tanpa mapping tidak diimport. Perilakunya sama seperti file yang memang tidak memiliki kolom tersebut: cell dibaca sebagai
'', di-cast menjadinull, lalu diperiksa oleh rule kolom. - Mass assignment tidak digunakan sebagai batas. Row ditulis dengan
forceFill(); daftar kolom importer adalah whitelist. Kolom yang tidak dideklarasikan tidak dapat ditulis, sementara$fillabletidak menentukan izin untuk kolom yang memang dideklarasikan. - Partial import adalah hasil yang memang didesain. Satu tanggal buruk pada row ke-400 tidak seharusnya menggagalkan 999 row lainnya yang valid.
Lihat juga
- Class importer — model, kolom,
resolve(), threshold - Kolom dan mapping —
ImportColumndan proses mapping lengkap - Failure report
- Queued import
- Storage dan cleanup
- ExportAction
- Action import dan export
- File upload
- Form Action dan Modal Action