Action Record
Record action adalah tombol yang berada di bagian akhir sebuah row: view, edit, delete, dan operasi lain yang berlaku untuk satu record. Action dideklarasikan pada schema tabel, di-resolve per record di server, lalu dieksekusi melalui endpoint action milik panel. Keberadaan tombol di frontend bukan mekanisme otorisasi operasi.
Contoh minimal
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Posts\Tables;
use App\Panels\Admin\Resources\Posts\PostResource;
use PandaPanel\Actions\DeleteAction;
use PandaPanel\Actions\EditAction;
use PandaPanel\Actions\ViewAction;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\TableSchema;
final class PostsTable
{
public static function configure(TableSchema $table): TableSchema
{
return $table
->columns([
TextColumn::make('title')->searchable()->sortable(),
])
->recordActions([
ViewAction::make(PostResource::class),
EditAction::make(PostResource::class),
DeleteAction::make(PostResource::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
27
28
Setiap row sekarang membawa tiga tombol tersebut, kecuali action yang ditolak policy untuk record tertentu.
Mendeklarasikan record action
use PandaPanel\Actions\Action;
use PandaPanel\Tables\Enums\RecordActionsPosition;
use PandaPanel\Tables\TableSchema;
TableSchema::recordActions(array $actions): self // array<array-key, Action>
TableSchema::recordActionsPosition(RecordActionsPosition $position): self
TableSchema::recordActionsLabel(string $label): self
TableSchema::frozenActions(bool $frozen = true): self
TableSchema::getRecordActions(): array // list<Action>
TableSchema::getRecordAction(string $name): ?Action2
3
4
5
6
7
8
9
10
recordActions() langsung menolak dua kondisi berikut pada baris tempat action dideklarasikan, bukan menunggu hingga render:
- Dua action memiliki nama yang sama — endpoint mencari action berdasarkan nama, sehingga duplikasi akan selalu membuat salah satunya tidak dapat dijangkau.
- Action tidak melakukan apa pun: tidak memiliki
url(),action(),bulkAction(),tableAction(),schema(),form(), ataumodal().PanelSchemaException::inertAction()memberi tahu action mana yang bermasalah dan apa yang perlu ditambahkan.
Record action bawaan
Setiap action berikut adalah static factory yang mengembalikan PandaPanel\Actions\Action yang sudah dikonfigurasi, sehingga seluruh method fluent milik Action tetap dapat di-chain setelahnya.
use PandaPanel\Actions\DeleteAction;
use PandaPanel\Actions\EditAction;
use PandaPanel\Actions\ForceDeleteAction;
use PandaPanel\Actions\ReplicateAction;
use PandaPanel\Actions\RestoreAction;
use PandaPanel\Actions\ViewAction;
ViewAction::make(string $resource): Action
EditAction::make(string $resource): Action
DeleteAction::make(string $resource): Action
RestoreAction::make(string $resource): Action
ForceDeleteAction::make(string $resource): Action
ReplicateAction::make(string $resource, array $except = [], ?Closure $using = null): Action2
3
4
5
6
7
8
9
10
11
12
13
| Factory | Name | Label | Icon | Variant | Tipe | Diotorisasi oleh |
|---|---|---|---|---|---|---|
ViewAction | view | View | eye | ghost | link | canView($record) |
EditAction | edit | Edit | pencil | ghost | link | canEdit($record) |
DeleteAction | delete | Delete | trash-2 | destructive | callback | canDelete($record) |
RestoreAction | restore | Restore | rotate-ccw | outline | callback | canRestore($record) |
ForceDeleteAction | forceDelete | Delete permanently | trash-2 | destructive | callback | canForceDelete($record) |
ReplicateAction | replicate | Replicate | copy | outline | callback | canCreate() dan canView($record) |
ViewAction dan EditAction juga disembunyikan ketika resource tidak mendeklarasikan halaman view atau edit, sehingga frontend tidak pernah menampilkan link menuju route yang tidak tersedia. RestoreAction dan ForceDeleteAction disembunyikan untuk record yang tidak berada dalam kondisi trashed — satu row menampilkan restore atau delete, bukan keduanya sekaligus.
DeleteAction dan ForceDeleteAction meminta konfirmasi secara default. ReplicateAction juga meminta konfirmasi dan menerima daftar kolom yang tidak boleh ikut disalin:
use App\Panels\Admin\Resources\Posts\PostResource;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use PandaPanel\Actions\ReplicateAction;
ReplicateAction::make(
PostResource::class,
except: ['slug', 'published_at'],
using: static function (Model $copy, Model $original): void {
$copy->forceFill([
'title' => $original->getAttribute('title').' (copy)',
'slug' => Str::uuid()->toString(),
]);
},
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
replicate() bawaan Eloquent sudah menghilangkan primary key dan timestamp. Parameter except ditujukan untuk kolom lain yang secara khusus tidak boleh diduplikasi pada model tersebut, misalnya unique slug, nomor invoice, atau API token.
Membuat record action sendiri
use App\Models\Order;
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Actions\Action;
use PandaPanel\Actions\Enums\ActionVariant;
Action::make('approve')
->label('Approve')
->icon('check')
->variant(ActionVariant::Outline)
->requiresConfirmation(
heading: 'Approve this order?',
description: 'The customer is notified immediately.',
button: 'Approve',
)
->successMessage('Order approved.')
->visible(static fn (?Model $record): bool => $record?->getAttribute('status') === 'pending')
->authorize(static fn (?Model $record): bool => $record !== null && auth()->user()?->can('approve', $record))
->action(static function (Order $record): void {
$record->approve();
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Method yang umum digunakan record action semuanya fluent dan mengembalikan static:
| Method | Signature | Default |
|---|---|---|
make | static make(string $name): static | — |
label | label(string $label): static | Str::headline($name) |
icon | icon(string $icon): static | tidak ada |
variant | variant(ActionVariant $variant): static | ActionVariant::Ghost |
requiresConfirmation | requiresConfirmation(bool $requires = true, ?string $heading = null, ?string $description = null, ?string $button = null): static | nonaktif |
successMessage | successMessage(string $message): static | "{Label} completed." |
successMessageUsing | successMessageUsing(Closure(int): string $callback): static | tidak ada |
visible | visible(Closure(?Model): bool $callback): static | selalu terlihat |
authorize | authorize(Closure(?Model): bool $callback): static | selalu diizinkan |
url | url(Closure(Model): string $callback): static | tidak ada — mengubahnya menjadi link |
action | action(Closure(Model, array): void $callback): static | tidak ada — mengubahnya menjadi callback |
before | before(Closure(Model, array): void $callback): static | tidak ada |
after | after(Closure(Model, array): void $callback): static | tidak ada |
schema | schema(Closure(?Model): FormSchema $callback): static | tidak ada — mengubahnya menjadi form |
form | form(Closure(?Model): string $callback): static | tidak ada — URL form eksternal |
modal | modal(Closure(Modal): void $callback): static | tidak ada |
modalWidth | modalWidth(ModalWidth $width): static | default modal |
slideOver | slideOver(bool $slideOver = true): static | nonaktif |
modalHeading | modalHeading(string $heading): static | label action |
modalDescription | modalDescription(string $description): static | tidak ada |
modalSubmitLabel | modalSubmitLabel(string $label): static | label action |
modalContent | modalContent(string $component, array $config = []): static | tidak ada |
registerModalActions | registerModalActions(array $actions): static | [] |
databaseTransaction | databaseTransaction(bool $enabled = true): static | null, mengikuti panel |
ActionVariant terdiri dari Default, Secondary, Outline, Ghost, dan Destructive. ActionType — hasil dari type() dan tidak diset langsung — bernilai Link ketika url() diberikan, Form ketika schema() atau form() diberikan, dan Callback pada kondisi lainnya.
before() dan after() dijalankan di dalam transaction yang sama dengan handler. Karena itu, jika hook after melempar exception, operasi utama ikut di-rollback daripada meninggalkan state setengah selesai. Hook berada pada action, bukan page, karena action endpoint dapat berjalan tanpa instance page.
Nama action hanya boleh berisi huruf, angka, dash, titik, dan underscore. Nama tersebut dikirim ke endpoint sebagai identifier; nama yang tidak dapat dicocokkan akan menciptakan tombol yang baru gagal saat ditekan. Karena itu PanelSchemaException::unusableActionName() menolaknya saat konstruksi.
Action dengan form
use App\Models\Order;
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Actions\Action;
use PandaPanel\Forms\Components\Textarea;
use PandaPanel\Forms\FormSchema;
Action::make('reject')
->label('Reject')
->modalHeading('Reject this order')
->modalSubmitLabel('Reject')
->schema(static fn (?Model $record): FormSchema => FormSchema::make()->schema([
Textarea::make('reason')->label('Reason')->required(),
]))
->action(static function (Order $record, array $data): void {
$record->reject($data['reason']);
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Schema diambil ketika dialog dibuka, bukan diserialisasi untuk setiap row sejak awal. Tanpa pendekatan ini, tabel dengan dua puluh record akan mengirim dua puluh salinan form yang kemungkinan hanya dibuka satu kali. Data yang dikirim telah divalidasi dan didehidrasi oleh schema sebelum handler menerimanya, sehingga key yang tidak pernah dideklarasikan form dibuang. Handler yang hanya menerima Model $record dapat mengabaikan argument data sepenuhnya.
Column action
Sel secara keseluruhan juga dapat menjalankan action:
use App\Models\Order;
use PandaPanel\Actions\Action;
use PandaPanel\Tables\Columns\TextColumn;
TextColumn::make('reference')->action(
Action::make('approve')->action(static fn (Order $record) => $record->approve()),
);2
3
4
5
6
7
use PandaPanel\Actions\Action;
use PandaPanel\Tables\Columns\Column;
Column::action(Action $action): static
Column::getAction(): ?Action2
3
4
5
Action di-resolve per record. Jika pengguna tidak diizinkan menjalankan action terhadap row tertentu, sel tersebut dirender sebagai nilai biasa daripada tombol yang hanya akan menghasilkan 403. TableSchema::getRecordAction() mencari action di daftar row action dan di setiap column action, sehingga endpoint dapat menemukannya tanpa lookup terpisah. Secara semantik, column action tetap merupakan record action.
Kolom juga dapat memiliki url(). Gunakan action atau URL, bukan keduanya. Renderer akan menempatkan tombol action di dalam anchor milik link dan sebuah sel yang sekaligus bernavigasi dan mengeksekusi operasi menghasilkan perilaku yang ambigu.
Posisi tombol action
use PandaPanel\Tables\Enums\RecordActionsPosition;
$table
->recordActionsPosition(RecordActionsPosition::AfterColumns) // the default
->recordActionsLabel('Manage')
->frozenActions();2
3
4
5
6
| Case | Nilai | Efek |
|---|---|---|
RecordActionsPosition::AfterColumns | after_columns | kolom action sendiri setelah seluruh data column |
RecordActionsPosition::BeforeColumns | before_columns | kolom action sendiri sebelum data column |
RecordActionsPosition::AfterCells | after_cells | tanpa kolom action khusus; tombol ditambahkan ke sel terakhir yang terlihat |
AfterCells cocok untuk tabel sempit ketika satu kolom action tersendiri justru mengambil terlalu banyak ruang. recordActionsLabel() memberi nama header kolom action dan default-nya adalah Actions. frozenActions() menjaga kolom action tetap terlihat ketika tabel di-scroll horizontal; fitur ini nonaktif secara default karena pinned column menggunakan ruang horizontal. Lihat pinned column.
Payload yang dibawa setiap row
TableSchema::toRow() me-resolve action untuk record yang sedang diserialisasi:
[
'key' => 42,
'group' => null,
'cells' => ['title' => 'Hello'],
'cellMeta' => ['title' => ['action' => ['name' => 'approve', /* ... */]]],
'actions' => [
['name' => 'edit', 'label' => 'Edit', 'icon' => 'pencil', 'variant' => 'ghost',
'type' => 'link', 'url' => '/admin/posts/42/edit', 'formUrl' => null,
'hasForm' => false, 'modal' => null, 'modalActions' => [], 'confirmation' => null],
],
]2
3
4
5
6
7
8
9
10
11
Action::toArray(?Model $record) mengembalikan null ketika action disembunyikan atau tidak diotorisasi untuk record tersebut, lalu row membuang entry tersebut. Tidak ada sesuatu yang dapat dieksekusi yang dikirim ke frontend: callback action hanya mengirim namanya, bukan handler PHP-nya.
Endpoint
Frontend mengirim record action ke satu endpoint untuk setiap panel:
POST {panel path}/actions/record route name: panel.{panelId}.actions.record{ "resource": "posts", "action": "approve", "record": 42 }Nested resource juga mengirim parent, yang di-resolve dan di-bind dengan aturan yang sama seperti middleware route pada halaman resource tersebut.
Controller memeriksa hal berikut secara berurutan:
- Slug resource harus dapat di-resolve di panel milik request tersebut. Resource yang hanya terdaftar di panel lain dianggap tidak ada.
TableSchema::getRecordAction($name)harus menemukan action; jika tidak, respons 404. Action yang tidak dideklarasikan resource tidak dapat dipanggil hanya dengan menulis namanya di request.Action::isExecutable()harus bernilai true. Link action tidak memiliki handler, sehingga melakukan POST terhadapnya menghasilkan 400.- Key record harus berupa string atau integer, jika tidak respons 422.
Resource::findRecord($key)harus berhasil, jika tidak respons 404. Ini sengaja menggunakan record lookup, bukan list query, karena restore secara valid perlu mengakses record yang mungkin disembunyikan dari list.Action::isAuthorizedFor($record)harus lolos, jika tidak respons 403.
Setelah itu action dijalankan dan respons melakukan redirect kembali dengan flash success berisi Action::getSuccessMessage().
Action yang memiliki form menggunakan POST {panel path}/actions/submit dengan scope: "record", kemudian menjalani pemeriksaan yang sama dengan validasi schema sebelum handler dieksekusi.
Default action tingkat panel
use PandaPanel\Actions\Action;
use PandaPanel\Actions\Enums\ActionVariant;
use PandaPanel\Core\Panel;
Panel::make('admin')
->configureActions(static function (Action $action): void {
if ($action->getVariant() === ActionVariant::Destructive) {
$action->requiresConfirmation();
}
});2
3
4
5
6
7
8
9
10
Configurator dijalankan ketika setiap action dibuat, sehingga konfigurasi yang kemudian ditetapkan schema tetap memiliki prioritas. Konfigurasi dibaca melalui panel aktif, bukan static registry, sehingga dua panel dapat memiliki default berbeda tanpa state bocor antar-request.
Catatan
- Menyembunyikan tombol tidak pernah menjadi mekanisme perlindungan record.
visible()danauthorize()memutuskan apakah action digambar; endpoint tetap memeriksaisAuthorizedFor()sebelum menjalankan operasi. visible()danauthorize()menerima?Model. Keduanya juga dapat dipanggil dengannullketika object action diserialisasi tanpa record, sehingga closure harus menangani kedua kondisi. Built-in action memulai pemeriksaan dengan$record !== null &&.RestoreActionmembutuhkan dua kondisi lain agar dapat dijangkau. Resource harus mendukung soft delete agar trashed record dapat di-resolve, dan tabel membutuhkanPandaPanel\Tables\Filters\TrashedFilteragar trashed row muncul sebagai tempat action tersebut ditampilkan. Lihat soft delete.- Record action juga bekerja pada tabel relation manager. Action dideklarasikan pada schema yang sama tetapi dikirim ke relation action endpoint. Lihat relation table.
- Success message dikirim sebagai flash, bukan return value handler. Handler tidak perlu mengembalikan apa pun; endpoint melakukan redirect dan panel menampilkan flash sebagai toast. Lihat toast notification.
- Record action tidak mengetahui selection. Operasi terhadap banyak record adalah bulk action dan memiliki alur otorisasi berbeda.