Kolom yang Dapat Diedit
Kolom yang dapat diedit menampilkan kontrol langsung di dalam sel dan menulis perubahan ke record ketika nilainya berubah. Gunakan kolom ini ketika satu atribut cukup sering diubah sehingga membuka form edit hanya untuk perubahan tersebut terasa tidak efisien — misalnya status publikasi, status proses, nama tampilan, atau bobot pengurutan.
Sel yang dapat diedit pada dasarnya adalah endpoint penulisan yang tampil sebagai bagian dari tabel. Karena itu, semua aturan yang berlaku pada form tetap berlaku di sini, ditambah satu aturan tambahan: kondisi disabled harus diverifikasi kembali di server ketika perubahan dikirim.
Contoh tabel editable minimal
use App\Panels\Admin\Resources\Orders\OrderResource;
use PandaPanel\Tables\Columns\SelectColumn;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\Columns\TextInputColumn;
use PandaPanel\Tables\Columns\ToggleColumn;
use PandaPanel\Tables\TableSchema;
public static function table(TableSchema $table): TableSchema
{
return $table->columns([
TextColumn::make('reference')->toggleable(false),
TextInputColumn::make('customer_name')->maxLength(255)->rules(['required']),
SelectColumn::make('status')->options([
'open' => 'Open',
'shipped' => 'Shipped',
'done' => 'Done',
]),
ToggleColumn::make('is_priority')->label('Priority'),
]);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Tidak diperlukan wiring tambahan. Frontend mengirim perubahan ke endpoint sel milik panel, server membangun ulang seluruh aturan dari schema, lalu halaman dirender kembali dengan kondisi record terbaru. Perubahan tidak diterapkan secara optimistis karena optimistic update harus menebak hasil validasi, otorisasi, dan kondisi disabled per record — tiga hal yang hanya dapat diputuskan dengan benar oleh server.
Empat aturan yang wajib dipenuhi saat menulis nilai
- Harus dideklarasikan. Hanya kolom yang merupakan
EditableColumnyang dapat ditulis, dan hanya atribut yang dideklarasikan kolom tersebut. Request yang menyebut nama kolom lain dianggap mengarah ke sesuatu yang tidak tersedia. - Harus tervalidasi.
validationRules()adalah aturan milik server, sama seperti pada field form. Kontrol di dalam sel hanya menentukan nilai apa yang mudah dimasukkan pengguna, bukan apa yang dianggap valid. - Harus diotorisasi per record.
Resource::canEdit($record)diperiksa untuk record yang benar-benar akan diubah, bukan hanya sekali untuk seluruh tabel. - Status disabled diperiksa ulang.
disabledUsing()dievaluasi ketika kontrol dirender dan ketika request penulisan diterima. Kontrol yang disabled bukanlah mekanisme permission.
Jenis kolom
| Class | type() | Bentuk sel | Aturan implisit |
|---|---|---|---|
ToggleColumn | toggle | {value: bool, disabled: bool} | boolean |
CheckboxColumn | checkbox | {value: bool, disabled: bool} | boolean |
TextInputColumn | text_input | {value: string, disabled: bool} | string atau numeric, ditambah max: |
SelectColumn | select | {value, label, disabled: bool} | Rule::in(...) berdasarkan opsi yang dideklarasikan |
Keempatnya mewarisi PandaPanel\Tables\Columns\EditableColumn, yang sendiri mewarisi Column. Artinya alignment, width, tooltip, freezing, dan visibility bekerja sama seperti pada kolom lain. Definisi yang diserialisasi membawa editable: true, dan nilai inilah yang memberi tahu renderer untuk menggambar kontrol input alih-alih hanya menampilkan nilai.
ToggleColumn dan CheckboxColumn
use PandaPanel\Tables\Columns\CheckboxColumn;
use PandaPanel\Tables\Columns\ToggleColumn;
ToggleColumn::make('is_active')->label('Active');
CheckboxColumn::make('is_featured');2
3
4
5
Keduanya melakukan penulisan yang sama tetapi dengan tampilan kontrol berbeda. Keduanya menggunakan alignment center, memvalidasi dengan aturan boolean, lalu mengubah nilai yang dikirim menggunakan filter_var($value, FILTER_VALIDATE_BOOLEAN) sebelum ditulis. Dua class digunakan, bukan satu class dengan flag tampilan, karena jenis kontrol adalah pembeda utama dan type memang menjadi mekanisme yang digunakan frontend untuk memilih renderer setiap jenis kolom.
TextInputColumn
use PandaPanel\Tables\Columns\TextInputColumn;
TextInputColumn::make('name')->maxLength(255)->rules(['required']);
TextInputColumn::make('weight')->numeric()->rules(['min:0']);2
3
4
| Method | Signature | Efek |
|---|---|---|
numeric() | numeric(): self | merender type="number" dan menggunakan validasi numeric, bukan string |
maxLength() | maxLength(int $length): self | merender atribut batas panjang dan menambahkan aturan max:{length} |
SelectColumn
use PandaPanel\Tables\Columns\SelectColumn;
SelectColumn::make('status')->options(['open' => 'Open', 'done' => 'Done']);2
3
options(array $options): self berfungsi sebagai whitelist. Aturan implisitnya adalah Rule::in() terhadap seluruh key yang dideklarasikan. Tidak seperti select berbasis relasi pada form, tidak ada pagination terbatas yang membantu di sini — inline select pada tabel mengirim seluruh opsi yang dimilikinya, jadi gunakan daftar yang pendek. Jika tidak ada opsi yang dideklarasikan, tidak ada aturan implisit yang ditambahkan.
label pada sel menggunakan label opsi ketika nilai tersimpan cocok dengan salah satu opsi. Jika tidak cocok, nilai mentah tetap ditampilkan sehingga record dengan nilai legacy masih dapat dibaca.
Kemampuan yang tersedia pada semua editable column
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Tables\Columns\TextInputColumn;
TextInputColumn::make('display_name')
->writeTo('name')
->rules(['required', 'min:2'])
->disabledUsing(static fn (Model $record): bool => $record->is_locked)
->mutateUsing(static fn (mixed $value, Model $record): string => trim((string) $value));2
3
4
5
6
7
8
| Method | Signature | Catatan |
|---|---|---|
rules() | rules(array $rules): static | ditambahkan setelah aturan implisit milik tipe kolom |
disabledUsing() | disabledUsing(Closure $callback): static | fn (Model $record): bool |
mutateUsing() | mutateUsing(Closure $callback): static | fn (mixed $value, Model $record): mixed, dijalankan setelah type cast |
updateUsing() | updateUsing(Closure $callback): static | fn (mixed $value, Model $record): void, menggantikan proses penulisan bawaan |
writeTo() | writeTo(string $attribute): static | digunakan ketika nilai harus ditulis ke atribut yang berbeda |
Method sisi baca: getWriteAttribute(): string, isDisabledFor(Model $record): bool, validationRules(): array, dan write(Model $record, mixed $value): void.
rules()
TextInputColumn::make('sku')->maxLength(32)->rules(['required', 'alpha_dash']);
// validationRules() === ['string', 'max:32', 'required', 'alpha_dash']2
Aturan implisit diletakkan terlebih dahulu, kemudian aturan tambahan yang Anda berikan. Semuanya merupakan aturan validasi Laravel biasa dan divalidasi terhadap key value, sehingga pesan error akan muncul sebagai errors.value.
disabledUsing()
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
ToggleColumn::make('is_admin')
->disabledUsing(static function (Model $record): bool {
$actor = auth()->user();
return ! ($actor instanceof User && $actor->is_admin)
|| $actor->is($record);
});2
3
4
5
6
7
8
9
10
Satu sel dapat read-only untuk record tertentu tetapi tetap editable untuk record lain. Closure dievaluasi per record ketika row diserialisasi, lalu dievaluasi kembali di endpoint sebelum perubahan ditulis. Contoh di atas menunjukkan mengapa kedua pemeriksaan diperlukan: policy yang mengizinkan pengguna mengedit dirinya sendiri tidak boleh secara tidak sengaja menjadi cara untuk memberikan hak administrator kepada diri sendiri.
writeTo()
TextInputColumn::make('display_name')->writeTo('name');Kolom membaca nilai dari display_name tetapi menulis ke name. Gunakan ketika nilai yang ditampilkan berasal dari accessor sementara nilai yang disimpan berada pada atribut lain.
mutateUsing() dan updateUsing()
use Illuminate\Database\Eloquent\Model;
// Transform on the way in.
TextInputColumn::make('slug')->mutateUsing(
static fn (mixed $value): string => str($value)->slug()->toString(),
);
// Replace the write entirely.
SelectColumn::make('status')
->options(['open' => 'Open', 'done' => 'Done'])
->updateUsing(static function (mixed $value, Model $record): void {
$record->transitionTo($value);
});2
3
4
5
6
7
8
9
10
11
12
13
mutateUsing() berjalan setelah type cast milik kolom dan sebelum record disentuh. updateUsing() mengambil alih proses penulisan sepenuhnya — tidak ada assignment dan tidak ada save() otomatis kecuali closure tersebut melakukannya sendiri. Gunakan ini ketika perubahan bukan sekadar menulis kolom database, misalnya untuk state machine, pemanggilan service, atau perubahan pada record relasi.
Tanpa updateUsing(), penulisan bawaan adalah $record->forceFill([$attribute => $value])->save(). forceFill sengaja melewati $fillable, karena whitelist pada alur ini adalah schema tabel, bukan model.
Endpoint
POST {panel}/actions/cellNama route adalah panel.{panel_id}.actions.cell. Satu endpoint tersedia untuk setiap panel, bukan satu endpoint per resource. Payload-nya:
| Key | Wajib | Arti |
|---|---|---|
resource | ya | slug resource, di-resolve terhadap registry milik panel ini |
record | ya | key record; harus berupa string atau integer |
column | ya | nama kolom |
value | — | nilai baru, divalidasi menggunakan aturan milik kolom |
parent | untuk nested resource | key record parent |
Pemeriksaan dijalankan dalam urutan berikut dan setiap jenis kegagalan memiliki status yang jelas:
| Kegagalan | Status |
|---|---|
| slug resource tidak dikenal | 404 |
| nama kolom tidak dikenal | 404 |
kolom dikenal tetapi bukan EditableColumn | 400 |
| key record bukan scalar | 422 |
| record berada di luar query resource | 404 |
Resource::canEdit() menolak | 403 |
disabledUsing() bernilai true untuk record tersebut | 403 |
| validasi gagal | redirect kembali dengan errors.value |
Penulisan dijalankan melalui DatabaseTransaction::run() sesuai konfigurasi transaction milik panel. Respons melakukan redirect kembali dan mengirim flash success dengan nilai Saved..
Testing
use PandaPanel\Tables\TableSchema;
it('writes a cell', function (): void {
$this->post('/admin/actions/cell', [
'resource' => 'orders',
'record' => $order->getKey(),
'column' => 'status',
'value' => 'done',
])->assertRedirect();
expect($order->fresh()->status)->toBe('done');
});
it('carries the disabled state per record', function (): void {
$schema = OrderResource::table(TableSchema::make());
expect($schema->toRow($locked)['cells']['status']['disabled'])->toBeTrue();
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Hal yang perlu diperhatikan
- Flag
disabledpada row bersifat per record, bukan per kolom. Dua row pada tabel yang sama dapat memiliki nilai berbeda; itulah tujuandisabledUsing(). - Validasi
booleantidak menerima'yes'. Aturan implisit menggunakan aturan Laravel, sehingga bentuk yang diterima adalahtrue,false,1,0,'1', dan'0'. writeTo()tidak mengubah sumber nilai yang dibaca sel. Kolom tetap melakukan resolve berdasarkan nama kolomnya sendiri.updateUsing()melewatisave()bawaan sepenuhnya. Jika closure tidak menyimpan perubahan, tidak ada perubahan yang dipersistensikan.- Editable column bukan pengganti form edit. Fitur ini menulis satu atribut tanpa lifecycle form dan tanpa state form bersama. Jika beberapa field harus berubah sebagai satu operasi, gunakan halaman edit atau form milik action.
- Endpoint tidak memiliki segment parent. Nested resource harus mengirim
parent, dan tabel melakukannya secara otomatis melalui propresource.parentKey.