ExportAction
PandaPanel\Actions\ExportAction mengubah record yang sedang ditampilkan tabel — atau record yang dicentang pengguna — menjadi spreadsheet yang dapat di-download. Ini merupakan factory, bukan class action terpisah: kedua method-nya mengembalikan PandaPanel\Actions\Action yang sudah dikonfigurasi, sehingga seluruh kemampuan action biasa untuk label, icon, modal, maupun authorization tetap dapat digunakan.
Gunakan saat kebutuhan berbunyi seperti "bisakah daftar ini saya download sebagai Excel?". Action menangani dialog, pemilihan record, dan download link; class Exporter yang Anda berikan menentukan kolom, nama file, serta lokasi penyimpanannya.
Contoh minimal yang berfungsi
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Users\Exports;
use PandaPanel\Actions\Exports\ExportColumn;
use PandaPanel\Actions\Exports\Exporter;
final class UserExporter extends Exporter
{
/**
* @return list<ExportColumn>
*/
public static function columns(): array
{
return [
ExportColumn::make('id')->label('ID'),
ExportColumn::make('name'),
ExportColumn::make('email'),
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Users\Tables;
use App\Panels\Admin\Resources\Users\Exports\UserExporter;
use App\Panels\Admin\Resources\Users\UserResource;
use PandaPanel\Actions\ExportAction;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\TableSchema;
final class UsersTable
{
public static function configure(TableSchema $table): TableSchema
{
return $table
->columns([
TextColumn::make('name')->searchable()->sortable(),
TextColumn::make('email')->searchable(),
])
->headerActions([
ExportAction::make(UserExporter::class, UserResource::class),
]);
}
}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
Tombol Export muncul di atas tabel. Tombol tersebut membuka dialog yang menawarkan tiga kolom dan dua format, menulis file, lalu mengembalikan toast dengan link Download.
Dua factory
use PandaPanel\Actions\Action;
use PandaPanel\Actions\Exports\Exporter;
use PandaPanel\Resources\Resource;
/** @param class-string<Exporter> $exporter @param class-string<Resource> $resource */
ExportAction::make(string $exporter, string $resource): Action; // the list, as currently filtered
ExportAction::bulk(string $exporter, string $resource): Action; // the ticked records only2
3
4
5
6
7
Keduanya menerima nama class, bukan instance atau closure. Export berukuran besar dapat dipindahkan ke queued job di proses lain, dan hanya nama class yang dapat dibawa melewati batas tersebut.
use App\Panels\Admin\Resources\Users\Exports\UserExporter;
use App\Panels\Admin\Resources\Users\UserResource;
use PandaPanel\Actions\ExportAction;
$table
->headerActions([
ExportAction::make(UserExporter::class, UserResource::class),
])
->bulkActions([
ExportAction::bulk(UserExporter::class, UserResource::class),
]);2
3
4
5
6
7
8
9
10
11
Keduanya menggunakan dialog yang sama dan hanya berbeda pada record yang dicakup. Pertanyaan "kolom mana yang ingin diexport" adalah pertanyaan tentang file, bukan tentang bagaimana record dipilih.
Konfigurasi yang dibuat factory
Kedua factory mengembalikan action yang sudah membawa konfigurasi berikut. Seluruhnya adalah setter Action biasa dan dapat dioverride dengan chaining setelah factory dipanggil.
| Pengaturan | Nilai | Ditetapkan oleh |
|---|---|---|
| name | export | Action::make('export') |
| label | Export | ->label() |
| icon | download | ->icon() |
| variant | ActionVariant::Outline | ->variant() |
| modal heading | Export records | ->modalHeading() |
| modal submit label | Export | ->modalSubmitLabel() |
| modal width | ModalWidth::Large | ->modalWidth() |
| success message | Your export is ready. | ->successMessage() |
| authorization | $resource::canViewAny() | ->authorize() |
| handler | ->tableAction() (make) / ->bulkAction() (bulk) | — |
| form | dialog kolom dan format | ->schema() |
Override dilakukan dengan chaining biasa:
use PandaPanel\Actions\Enums\ActionVariant;
use PandaPanel\Actions\ExportAction;
ExportAction::make(UserExporter::class, UserResource::class)
->label('Download users')
->icon('file-spreadsheet')
->variant(ActionVariant::Default)
->modalHeading('Download the user list')
->successMessage('Preparing your file.');2
3
4
5
6
7
8
9
Authorization default menggunakan Resource::canViewAny() karena export adalah salinan dari list — pertanyaan authorization-nya sama dengan list itu sendiri. Jika ingin diperketat, chain ->authorize() baru; konfigurasi baru akan menggantikan authorization yang diberikan factory:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Gate;
ExportAction::make(UserExporter::class, UserResource::class)
->authorize(static fn (?Model $record): bool => Gate::allows('export-users'));2
3
4
5
Dialog
Form dibangun berdasarkan exporter, bukan berdasarkan action, dan kedua field-nya merupakan form field biasa:
use PandaPanel\Forms\Components\CheckboxList;
use PandaPanel\Forms\Components\Radio;
CheckboxList::make('columns')
->label('Columns')
->options(/* name => label, from Exporter::columns() */)
->columns(2)
->bulkToggleable()
->required()
->default(/* the names whose enabledByDefault() is true */);
Radio::make('format')
->label('Format')
->options(/* value => label, from Exporter::formats() */)
->inline()
->required()
->default(/* the first format the exporter offers */);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Karena keduanya adalah field, nama kolom yang tidak dideklarasikan exporter akan ditolak oleh rule in: yang sama seperti choice field lainnya. Setelah validation, dua pemeriksaan tambahan dilakukan di server:
- Nama yang tidak dideklarasikan exporter dibuang; pilihan kolom yang kosong berarti tulis semua kolom, karena file yang hanya memiliki header dan tidak memiliki data kolom bukanlah yang dimaksud pengguna saat memilih "export".
- Format dibaca menggunakan
SpreadsheetFormat::tryFrom()dan fallback keSpreadsheetFormat::Csv.
Kolom mana yang masuk ke file dan urutan penulisannya ditentukan exporter — lihat Class exporter.
Record mana yang masuk ke file
| Factory | Query |
|---|---|
bulk() | Resource::query()->whereKey($selectedKeys) |
make() | Resource::query() yang dibatasi oleh PandaPanel\Tables\TableQuery menggunakan state tabel yang sedang ditampilkan |
Client mengirim query string list saat ini sebagai tableState bersama request, lalu PandaPanel\Support\TableState::fromRequest() membatasinya menjadi key string yang memuat scalar atau array satu level. Setiap nilai kemudian diproses kembali melalui schema tabel yang menjadi whitelist: filter yang tidak pernah dideklarasikan tabel diabaikan, sama seperti ketika filter tersebut datang dari URL. Payload buatan paling jauh hanya dapat mendeskripsikan list yang sebenarnya juga dapat dinavigasi pengguna melalui UI.
Inilah juga alasan file dan screen tidak dapat berbeda: keduanya dibangun oleh TableQuery yang sama.
Inline atau queued
Jumlah record dihitung sebelum penulisan apa pun dilakukan: count() pada constrained query untuk make(), dan count($keys) untuk bulk().
if ($exporter::queueAfter() >= 0 && $count > $exporter::queueAfter()) {
// PandaPanel\Jobs\RunPanelExport is dispatched and the request returns.
}2
3
queueAfter() | Behavior |
|---|---|
0 | selalu queued |
2000 (default) | queued jika lebih dari 2000 record |
| negatif | tidak pernah queued, berapa pun jumlah record |
Di bawah threshold, file ditulis di dalam request melalui PandaPanel\Actions\Exports\ExportRun::write() dan response membawa link download. Di atas threshold, job menulis file lalu link datang melalui notification. Keduanya menggunakan ExportRun yang sama sehingga file yang dihasilkan identik. Lihat Queued export.
Response yang dikirim
Untuk export yang selesai di dalam request, dua hal dikirim:
use PandaPanel\Notifications\Notification;
use PandaPanel\Notifications\NotificationAction;
Notification::make('export-ready')
->title($exporter::completedMessage($result['records']))
->success()
->icon('download')
->persistent()
->broadcast(false) // the response is right here; broadcasting would show it twice
->actions([
NotificationAction::make('download')->label('Download')->url($url),
])
->send($user);
Inertia::flash('toast', [
'type' => 'success',
'message' => $exporter::completedMessage($result['records']),
'url' => $url,
'urlLabel' => 'Download',
]);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
URL dibangun dengan route($panel->routeName('export-file'), ['file' => $file, 'exporter' => $exporter], absolute: false) — route export-file milik panel, menggunakan basename file dan class exporter sebagai query parameter. Lihat Notification import dan export.
Download
GET {panel}/exports/{file}?exporter=App\Panels\Admin\…\UserExporter
route name: panel.{panelId}.export-file2
PandaPanel\Http\Controllers\PanelExportController menangani request tersebut:
- request tanpa authenticated user mendapat 403;
fileyang kosong atau mengandung/,\, atau..mendapat 404 — caller hanya boleh menyebut nama file, tidak pernah path;exporteryang bukan subclassPandaPanel\Actions\Exports\Exportermendapat 404;- path dibangun sebagai
{$exporter::directory()}/{$user->getAuthIdentifier()}/{$file}— berdasarkan pengguna yang sedang meminta, sehingga pengguna tidak dapat menamai export milik orang lain melalui path; - file yang tidak ada mendapat 404, selain itu response menggunakan
Storage::disk($exporter::disk())->download($path, $file).
Tempat meletakkan action
| Placement | Factory | Dijalankan melalui |
|---|---|---|
headerActions() | ExportAction::make() | tableAction() — tanpa record |
toolbarActions() | ExportAction::make() | tableAction() |
bulkActions() | ExportAction::bulk() | bulkAction() — selection |
ExportAction::make() hanya mendeklarasikan table handler, sehingga meletakkannya di recordActions() menghasilkan action yang tidak dapat dieksekusi: record scope memanggil isExecutable() dan mendapat 400. Letakkan versi table di header atau toolbar, dan versi bulk di bulkActions().
Gotchas
- Success flash dan export toast adalah dua pesan berbeda. Action endpoint selalu melakukan redirect
back()->with('success', $action->getSuccessMessage()). Untuk inline export,Inertia::flash('toast', …)eksplisit yang membawa download link akan menang —PandaPanel\Http\Middleware\ShareFlashToasttidak pernah menimpa toast eksplisit. Untuk export queued, tidak ada toast eksplisit pada request awal, sehingga success message action-lah yang terlihat pengguna, dan defaultYour export is ready.muncul sebelum file benar-benar selesai. Override menjadi->successMessage('Preparing your export.')jika exporter dapat masuk queue. - Bulk selection dibatasi maksimal 500 key oleh rule endpoint form action
'records' => ['nullable', 'array', 'max:500']. Export lebih besar sebaiknya menggunakanExportAction::make()terhadap filtered list. - Exporter tidak boleh memiliki nama kolom duplikat.
ExportRunmelemparPanelSchemaException::duplicateExportColumns()— picker memakai nama sebagai key, sehingga dua kolom dengan nama yang sama tidak dapat dipilih secara terpisah. - Sorting adalah tanggung jawab exporter, bukan tabel. Table state menentukan record mana yang masuk;
Exporter::query()menentukan urutan penulisan file. Tanpareorder(), urutan file mengikuti sorting list ketika action dipanggil. - Tidak ada proses yang otomatis menghapus file export. File tetap berada di disk dalam directory milik owner sampai sesuatu menghapusnya — lihat Storage dan cleanup.
- Relation column tanpa eager load menghasilkan satu query per row.
with()seharusnya ditempatkan diExporter::query().
Lihat juga
- Class exporter — kolom, query, nama file, threshold
- Kolom dan mapping —
ExportColumnsecara lengkap - Queued export
- Storage dan cleanup
- Notification import dan export
- ImportAction
- Action import dan export
- Dasar Action, Form Action, Bulk action
- Filter tabel — isi yang dibawa
tableState