Tab Filter
Tab berada di atas daftar resource dan membaginya menjadi beberapa view bernama, misalnya semua, draft, dan arsip. Gunakan ketika pengguna memang bekerja dengan beberapa scope yang sudah dikenal. Tab membutuhkan satu klik, sedangkan filter ekuivalen biasanya membutuhkan beberapa klik dan dropdown.
Tab adalah named scope pada query milik resource itu sendiri, bukan query yang berdiri sendiri. Karena itu tenant scope atau permission scope tetap berlaku pada apa pun yang ditampilkan tab.
Contoh minimal
Tab dideklarasikan pada list page, bukan pada table schema:
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Posts\Pages;
use App\Models\Post;
use App\Panels\Admin\Resources\Posts\PostResource;
use Illuminate\Database\Eloquent\Builder;
use PandaPanel\Resources\Pages\ListRecords;
use PandaPanel\Tables\Tab;
final class ListPosts extends ListRecords
{
protected static string $resource = PostResource::class;
/**
* @return array<string, Tab>
*/
public function tabs(): array
{
return [
'all' => Tab::make('all')->badge(static fn (): int => Post::query()->count()),
'published' => Tab::make('published')
->icon('check')
->query(static fn (Builder $query): Builder => $query->whereNotNull('published_at')),
'drafts' => Tab::make('drafts')
->query(static fn (Builder $query): Builder => $query->whereNull('published_at')),
];
}
}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
Halaman sekarang menampilkan tiga tab. Memilih salah satunya menulis ?tab=drafts dan mengembalikan nomor halaman ke halaman pertama.
ListRecords::tabs()
use PandaPanel\Resources\Pages\ListRecords;
use PandaPanel\Tables\Tab;
/**
* @return array<string, Tab>
*/
public function tabs(): array2
3
4
5
6
7
Mengembalikan [] — nilai default — berarti tidak ada tab dan halaman mengirim tabs: [].
Array key adalah nilai tab di URL. Entry pertama adalah fallback: jika request tidak menyebut tab atau menyebut tab yang tidak dideklarasikan halaman, reset($tabs) menjadi tab aktif. Query string adalah input pengguna, sehingga key yang tidak dikenal fallback ke default alih-alih menghasilkan error, sama seperti sort column yang tidak dikenal.
Gunakan string yang sama untuk array key dan argumen pertama Tab::make(). Tab aktif dicocokkan melalui array key ketika request dibaca, sedangkan frontend menulis ?tab= dari key yang diserialisasi milik objek Tab. Jika keduanya berbeda, mengklik tab tidak akan memilih tab tersebut dan halaman akan fallback ke entry pertama.
Tab
use Closure;
use Illuminate\Database\Eloquent\Builder;
use PandaPanel\Tables\Tab;
Tab::make(string $key, ?string $label = null): self
Tab::query(Closure(Builder): Builder $callback): self
Tab::badge(string|int|Closure|null $badge): self
Tab::icon(?string $icon): self
Tab::getLabel(): string
Tab::apply(Builder $query): Builder
Tab::resolveBadge(): string|int|null
Tab::toArray(bool $active): array2
3
4
5
6
7
8
9
10
11
12
$key juga merupakan public readonly property: $tab->key.
| Method | Default | Catatan |
|---|---|---|
make($key, $label) | label null | jika label tidak diberikan, digunakan Str::headline($key) — all tampil sebagai "All" |
query($callback) | tidak ada | tanpa callback, tab tidak mempersempit query; inilah yang membuat tab "all" benar-benar menampilkan semuanya |
badge($badge) | null | string, int, closure yang mengembalikan salah satunya, atau null jika tanpa badge |
icon($icon) | null | registry key icon, bukan path component |
use App\Models\Post;
use Illuminate\Database\Eloquent\Builder;
use PandaPanel\Tables\Tab;
// A label that is not the key.
Tab::make('needs_review', 'Needs review')
->icon('alert-triangle')
->badge(static fn (): int => Post::query()->where('flagged', true)->count())
->query(static fn (Builder $query): Builder => $query->where('flagged', true)),
// A static badge, for a number that is not worth a query.
Tab::make('archived')->badge('90d'),2
3
4
5
6
7
8
9
10
11
12
Query closure
query() menerima Resource::query() — builder milik resource dengan scope, eager load, dan tenant narrowing yang sudah diterapkan — lalu harus mengembalikan builder:
use Illuminate\Database\Eloquent\Builder;
use PandaPanel\Tables\Tab;
Tab::make('mine')->query(
static fn (Builder $query): Builder => $query->where('author_id', auth()->id()),
);2
3
4
5
6
Tab::apply() mengembalikan query tanpa perubahan ketika tidak ada closure, sehingga Tab::make('all') menampilkan semua record yang memang dapat ditampilkan resource.
Badge
resolveBadge() menjalankan closure di server dan hanya nilai scalar yang dikirim ke Vue, sama seperti navigation badge. Tidak ada executable code yang diserialisasi. Frontend menyembunyikan badge jika nilainya null atau string kosong.
Badge closure dijalankan setiap kali halaman dirender, sekali untuk setiap tab, terlepas dari tab mana yang sedang aktif. Tiga tab dengan tiga closure count() berarti tiga query tambahan pada setiap page load; gunakan cache jika biaya tersebut signifikan.
Data yang dikirim halaman
tabs adalah list sesuai urutan deklarasi, dan setiap entry merupakan hasil Tab::toArray($active):
[
['key' => 'all', 'label' => 'All', 'icon' => null, 'badge' => 128, 'active' => true],
['key' => 'published', 'label' => 'Published', 'icon' => 'check', 'badge' => null, 'active' => false],
['key' => 'drafts', 'label' => 'Drafts', 'icon' => null, 'badge' => null, 'active' => false],
]2
3
4
5
active adalah keputusan server, seperti state tabel lainnya. Tab direpresentasikan oleh URL, bukan local state: menekan tab melakukan navigasi. Karena itu back, forward, refresh, dan bookmark tetap berfungsi dengan benar.
Scope yang diterapkan tab
List page membangun scoped query sekali dan menggunakannya untuk tiga hal:
$tab = $this->activeTab($request);
$scoped = fn (): Builder => $tab === null
? static::$resource::query()
: $tab->apply(static::$resource::query());
$query = $scoped();
$records = $tableQuery->paginate($query);2
3
4
5
6
7
- Rows, melalui
TableQuery::paginate(), sehingga search, filters, sort, dan pagination diterapkan di atas scope tab. - Summaries, karena
summaries()menerima builder yang sama dengan paginator. - Widget halaman, melalui
PageContext::forQuery($scoped)— widget menghitung berdasarkan data yang sedang dilihat pengguna, bukan seluruh tabel. Lihat Filter widget.
Tab, filter, dan URL
?tab= adalah query parameter top-level, sejajar dengan search, sort, page, dan filters. Memilih tab mengembalikan page ke 1 karena tab mempersempit result set; halaman 7 dari view sebelumnya hampir tidak pernah menjadi lokasi yang diinginkan pada view baru. State lain tetap dipertahankan. Filter yang aktif pada satu tab masih aktif ketika berpindah ke tab lain.
Tab tidak disimpan di session. persistFiltersInSession(), persistSearchInSession(), dan persistSortInSession() hanya mencakup state milik tabel; tab selalu dibaca langsung dari request dan fallback ke entry pertama.
Catatan
- Tab mempersempit resource query; tab tidak pernah menggantikannya. Authorization, tenancy, dan
Resource::$withsudah diterapkan sebelum closure menerima builder, sehingga tab tidak dapat memperluas record yang boleh dilihat pengguna. - Tab pertama yang dideklarasikan menjadi default. Letakkan view paling umum di posisi pertama atau deklarasikan tab "all" secara eksplisit.
- Tab adalah fitur list page.
TableSchematidak memilikitabs(); relation manager table dan table widget tidak menggunakan tab. Persempit konteks tersebut dengan filter. - Tab bukan filter indicator. Tab tidak muncul dalam
filterIndicators, dan menghapus seluruh filter tidak mengubah tab. - Badge tetap di-resolve untuk tab yang tidak aktif, sehingga badge dapat menunjukkan jumlah di tab lain sekaligus tetap memiliki biaya query.
- Icon adalah registry key.
panel:iconsmenemukan nama icon dari source code; key yang tidak dikompilasi hanya dirender tanpa icon, bukan menghasilkan broken import. Lihat Component registry.