Toolbar Actions
Beberapa aksi (actions) berkaitan dengan tabel secara keseluruhan alih-alih pada baris tertentu: mengekspor daftar, menyegarkan (refresh) cache, mengisi data demo (seed), atau membuka dialog pembuatan data (create dialog). Aksi-aksi ini dideklarasikan di salah satu dari tiga tempat — header halaman, toolbar di samping kotak pencarian, atau pada empty state (saat data kosong) — dan semuanya bekerja dengan cara yang sama: tanpa perlu spesifik record, handler dideklarasikan dengan tableAction(), dan otorisasinya diminta dengan nilai null.
Contoh minimal yang berfungsi
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Orders\Tables;
use Illuminate\Support\Facades\Cache;
use PandaPanel\Actions\Action;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\TableSchema;
final class OrdersTable
{
public static function configure(TableSchema $table): TableSchema
{
return $table
->columns([
TextColumn::make('reference')->searchable(),
])
->toolbarActions([
Action::make('refresh')
->label('Refresh totals')
->icon('refresh-cw')
->successMessage('Totals recalculated.')
->tableAction(static function (): void {
Cache::forget('orders.totals');
}),
]);
}
}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
Tombol tersebut akan muncul di toolbar. Ketika ditekan, tombol akan mengirimkan (post) nama aksi tersebut, kemudian server mencarinya di dalam schema tabel ini, melakukan otorisasi, lalu menjalankannya.
Tiga tempat peletakan
use PandaPanel\Actions\Action;
use PandaPanel\Tables\TableSchema;
TableSchema::headerActions(array $actions): self // array<array-key, Action>
TableSchema::toolbarActions(array $actions): self
TableSchema::emptyStateActions(array $actions): self
TableSchema::getHeaderActions(): array // list<Action>
TableSchema::getToolbarActions(): array
TableSchema::getEmptyStateActions(): array
TableSchema::getTableAction(string $name): ?Action // mencari di ketiganya2
3
4
5
6
7
8
9
10
11
| Set | Di-render di | Untuk |
|---|---|---|
headerActions() | header halaman, di samping tombol "New …" bawaan resource | hal yang menjadi tujuan halaman tersebut — create, import, export |
toolbarActions() | toolbar, di sebelah pencarian dan filter | tampilan (view) dari halaman tersebut — refresh, kalkulasi ulang, mengubah mode |
emptyStateActions() | di dalam empty state | saat tabel kosong adalah kondisi di mana aksi paling berguna dan paling sulit ditemukan |
Tiga tempat dan satu pencarian: getTableAction() mencari di header, toolbar, dan empty state secara berurutan, karena endpoint yang menjalankannya tidak mempedulikan di bar bagian mana aksi tersebut di-render — ia hanya peduli bahwa tabel mendeklarasikannya di suatu tempat.
use PandaPanel\Actions\Action;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\TableSchema;
$schema = TableSchema::make()
->columns([TextColumn::make('name')])
->headerActions([Action::make('export')->tableAction(static fn () => null)])
->emptyStateActions([Action::make('seed')->tableAction(static fn () => null)]);
$schema->getTableAction('export'); // Action
$schema->getTableAction('seed'); // Action
$schema->getTableAction('invented'); // null2
3
4
5
6
7
8
9
10
11
12
13
Setiap setter akan menolak penggunaan dua nama yang sama dalam set yang sama dan akan langsung menolak aksi yang tidak melakukan apa-apa pada saat baris kodenya dieksekusi. Nama aksi hanya unik di dalam sebuah set — jika Anda membuat aksi bernama export di header dan export di toolbar, keduanya akan me-resolve ke aksi header. Jadi, berikanlah nama yang berbeda pada keduanya.
Handler
use Closure;
use PandaPanel\Actions\Action;
Action::tableAction(Closure(array<string, mixed>): void $callback): static
Action::isTableExecutable(): bool
Action::executeWithoutRecord(array $data = []): void2
3
4
5
6
7
tableAction() adalah apa yang membuat sebuah aksi dapat dijalankan tanpa sebuah data record. Array yang diterimanya berisi data submit dari form milik aksi itu sendiri, dan array tersebut akan bernilai kosong jika aksinya tidak memiliki form. Handler ini dieksekusi di dalam transaksi database, persis seperti aksi record, serta menghormati pengaturan databaseTransaction() maupun nilai bawaan panelnya.
use App\Models\Order;
use PandaPanel\Actions\Action;
use PandaPanel\Actions\Enums\ActionVariant;
Action::make('purgeCancelled')
->label('Purge cancelled')
->icon('trash-2')
->variant(ActionVariant::Ghost)
->requiresConfirmation(
heading: 'Delete every cancelled order?',
description: 'This cannot be undone.',
button: 'Delete them',
)
->authorize(static fn (): bool => auth()->user()?->is_admin === true)
->successMessage('Cancelled orders removed.')
->tableAction(static function (): void {
Order::query()->where('status', 'cancelled')->delete();
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Semua metode lain pada Action juga bisa digunakan di sini — label(), icon(), variant(), requiresConfirmation(), successMessage(), successMessageUsing(), visible(), authorize(), modal*(), slideOver(), databaseTransaction(). Lihat Record actions untuk daftar selengkapnya.
Otorisasi tanpa record
Otorisasi pada table action di-resolve menggunakan nilai null, serupa dengan cara aksi massal (bulk action) meminta otorisasi sebelum ada record apa pun yang dipilih:
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Actions\Action;
Action::make('export')
->visible(static fn (?Model $record): bool => true)
->authorize(static fn (?Model $record): bool => auth()->user()?->can('export', Order::class) === true)
->tableAction(static fn () => null);2
3
4
5
6
7
8
Jika fungsi closure otorisasi menolak sebuah aksi, maka aksi tersebut akan absen (hilang) dari objek serialize ketimbang dirender lalu baru diblokir belakangan. Kedua fungsi closure tersebut (visible & authorize) juga akan menerima parameter null pada aksi record saat struktur schema di-serialize tanpa data record. Oleh karena itu, closure yang ditulis untuk salah satu fungsi harus dibuat cukup adaptif (tolerate) untuk fungsi lainnya.
Aksi dengan form
Sebuah table action dapat membuka dialog dengan form, melakukan pengiriman data (submit), dan menerima hasil validasinya:
use App\Models\Period;
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Actions\Action;
use PandaPanel\Forms\Components\DatePicker;
use PandaPanel\Forms\FormSchema;
Action::make('closePeriod')
->label('Close period')
->modalHeading('Close an accounting period')
->modalSubmitLabel('Close')
->schema(static fn (?Model $record): FormSchema => FormSchema::make()->schema([
DatePicker::make('until')->label('Close everything up to')->required(),
]))
->tableAction(static function (array $data): void {
Period::closeUpTo($data['until']);
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Schema ini baru di-fetch (diambil) ketika dialog terbuka, sehingga tidak turut ter-serialize membebani struktur awal halaman. Data hasil pengiriman akan divalidasi dan di-dehydrate oleh schema yang sama ini sebelum sampai ke handler — setiap parameter tak dikenali (key) yang di luar deklarasi form akan diabaikan/dibuang.
Aksi tabel bawaan (Built-in table actions)
use App\Panels\Admin\Resources\Users\Exports\UserExporter;
use App\Panels\Admin\Resources\Users\Imports\UserImporter;
use App\Panels\Admin\Resources\Users\UserResource;
use PandaPanel\Actions\CreateAction;
use PandaPanel\Actions\ExportAction;
use PandaPanel\Actions\ImportAction;
CreateAction::make(string $resource): Action // tautan (link) ke halaman create
CreateAction::modal(string $resource): Action // form yang sama namun dalam mode dialog
ExportAction::make(string $exporter, string $resource): Action
ImportAction::make(string $importer, string $resource): Action2
3
4
5
6
7
8
9
10
11
12
$table->headerActions([
CreateAction::modal(UserResource::class)->label('New user'),
ImportAction::make(UserImporter::class, UserResource::class),
ExportAction::make(UserExporter::class, UserResource::class),
]);2
3
4
5
6
CreateAction::modal() adalah sebuah table action: aksi ini membuka form bawaan resource dalam bentuk dialog dan menyimpannya melalui perantara tableAction(), sehingga dialog maupun halaman buat (create) tidak memiliki perbedaan validasi maupun mekanisme simpan (persist). ExportAction::make() akan mengekspor daftar data sesuai filter yang sedang aktif; sementara fungsi pilih ekspor untuk item spesifik yang diseleksi menggunakan fungsi ExportAction::bulk() dan berada di bulkActions(). Lihat Export action dan Import action.
Sedangkan, CreateAction::make() adalah tipe aksi berupa tautan (link), yang seharusnya berada pada baris tertentu alih-alih di jajaran header/toolbar ini. Sebuah aksi link tanpa resolusi safe URL pada saat serialize akan langsung dihilangkan dari hasil data (payload) dibanding menampilkannya sebagai tombol pemicu POST yang gagal. Jadi, gunakanlah CreateAction::modal() bila meletakkan aksi di dalam toolbar ini; karena pada dasarnya halaman daftar otomatis akan menampilkan tautan bawaan "New {label}" sendiri apabila sumbernya telah mendeklarasikan halaman pembuatan dan memiliki izin dari canCreate().
Empty state (Kondisi data kosong)
use PandaPanel\Tables\TableSchema;
TableSchema::emptyState(string $heading, ?string $description = null, ?string $icon = null): self
TableSchema::emptyStateActions(array $actions): self
TableSchema::emptyStateComponent(string $component): self2
3
4
5
6
| Bagian | Default |
|---|---|
| heading | No records found |
| description | null |
| icon | null |
| actions | [] |
| component | null |
$table
->emptyState(
heading: 'No orders match this view',
description: 'Adjust the search or filters, or create one.',
icon: 'shopping-cart',
)
->emptyStateActions([CreateAction::modal(OrderResource::class)]);2
3
4
5
6
7
8
Memberikan opsi aksi yang sama pada header dan empty state adalah sebuah praktik yang lazim dan sah. Karena keduanya merupakan daftar (set) yang terpisah, sebaiknya buat satu private method bersama agar keduanya selalu identik dan mencegah perbedaan (drifting) di masa mendatang.
Fungsi emptyStateComponent() digunakan untuk mengganti keseluruhan tampilan empty state menggunakan komponen aplikasi yang terdaftar, sesuai dengan kunci registri di fase build-time yang merujuk pada resources/js/pages/Panels/{Panel}/EmptyStates/. Anda tidak boleh melempar markup raw (seperti html mentah) di sini:
$table->emptyStateComponent('Panels/Admin/EmptyStates/NoOrders');2
Jika Anda memanggil komponen yang tak dikenal (unknown), sistem akan merubah fallback ke empty state biasa alih-alih tampil blank. Lihat selengkapnya di Component registries.
Apa yang di-serialize
$schema->toArray();
// [
// 'headerActions' => [ /* Action::toArray(null), aksi null disaring/dibuang */ ],
// 'toolbarActions' => [ ... ],
// 'emptyState' => [
// 'heading' => 'No records found',
// 'description' => null,
// 'icon' => null,
// 'component' => null,
// 'actions' => [ ... ],
// ],
// ...
// ]2
3
4
5
6
7
8
9
10
11
12
13
14
Halaman list menampilkan aksi headernya sendiri terlebih dahulu — tautan "New {label}" dari resource apabila halaman pembuatan terdeklarasi dan diizinkan oleh canCreate() — barulah diikuti dengan render headerActions milik tabel tersebut.
Endpoint
POST {panel path}/actions/table nama route: panel.{panelId}.actions.table2
{ "resource": "orders", "action": "refresh" }2
Jika resource yang dieksekusi bersarang (nested resource), argumen pendukung parent juga turut dikirim. Controller kemudian akan memvalidasi secara berurutan sebagai berikut:
- Apakah slug resource berhasil di-resolve dari dalam panel yang sesuai pada request, jika gagal dilempar kode 404.
TableSchema::getTableAction($name)mencari eksistensi aksi, jika tidak ditemukan: 404 — "Unknown action."- Fungsi pemicu eksekusi dicek via
Action::isTableExecutable(), jika tidak sesuai tipe: 400 — "This action cannot be executed." Action::isAuthorizedFor(null)memeriksa hak akses pengguna, jika tidak punya izin kode 403 dikembalikan.
Sesudah semua lulus, barulah executeWithoutRecord() jalan secara aktual dan respons akan melakukan redirect back ditambah pesan tipe kilat bawaan atau pesan kustom dari metode Action::getSuccessMessage().
Aksi yang mewajibkan inputan form akan dilarikan ke POST {panel path}/actions/submit bersama properti pendamping scope: "table", ia akan tetap diresolve menggunakan getTableAction() plus menjalani seluruh aturan verifikasi (validation) milik properti form schema lalu baru handler yang sama dieksekusi setelahnya.
Catatan
tableAction()adalah apa yang membuatnya dapat dijalankan. Aksi yang hanya membawaaction()merupakan record handler (aksi untuk basis satu data spesifik); mengeksekusinya via post dengan kedok aksi tabel akan ditolak (400).- Aksi link tidak bekerja di dalam kategori toolbar baris-barisan ini. Fungsi
url()bergantung resolusi tautannya dari variabel record, sedang di baris ini tidak ada record yang berasosiasi, maka aksi jenis ini akan diabsenkan paksa sejak masa struktur serialize. Jika ingin berpaut link, arahkan tautan via aksi satu data dari fitur record action atau bawaan area header. - Sebuah table action tidak bisa membaca selection (data ter-cek). Kebutuhan spesifik itu dibebankan ke spesifikasi bulk action di mana otorisasi diberlakukan per satu data record sebelum sentuhan modifikasi diaplikasikan.
getTableAction()akan mereturn hasil pencocokan pertama berdasar 3 urutan kumpulan setnya. Apabila dua aksi mengadopsi identitas nama variabel sama tapi diletak di dua tipe set terpisah (semisal header vs toolbar), validasi duplikasi tidak mendeteksinya di tingkat awal, tapi pada saat diklik endpoint tak akan bisa membedakannya, alias langsung mengambil mana yang posisinya ketemu duluan.- Deklarasi aksi yang mati (inert) melempar error saat penyusunan kode (declaration). Ketimbang menghadirkan fitur mati ketika ditekan, pesan Exception bawaan framework berupa
PanelSchemaException::inertAction()langsung mendaftarkan rincian perihal perbaikannya apa saja. - Aksi pada header biasanya juga sangat relevan diposisikan di layar kosong (empty state). Keduanya bagaimanapun tidak menginduksi satu sama lain; jadi lempar saja array aksi identik tersebut ke masing-masing pendefinisi fungsinya.
- Tabel berbasis pengelola relasi (Relation manager) tidak menjalankan table actions. Toolbarnya menyajikan set header action unik kepunyaan si relasinya (seperti fungsi create, attach, associate) ditambah area endpoint kepemilikan khusus (relation) tersebut sejatinya murni tak dilengkapi route pendukung pemanggilan table-action. Baca kelanjutannya di Relation tables.
- Pengaturan default standar lintas-panel (Panel-wide) otomatis mencakup implementasi di sini. Fitur utilitas macam
Panel::configureActions()dijalankan bersamaan ketika tiap aksi terbangun. Dengan ini pakem visual standar dari keseluruhan web Anda layaknya perwujudan ikon serta konfirmasi dialog tertular (reach out) merata hingga deretan toolbar secara efisien tanpa harus merapalnya (repeat) berulang kali di setiap penulisan fungsional tabel baru.