Navigation Group
Navigation group adalah heading pada sidebar sekaligus penentu urutan tampilnya kelompok navigation. Resource atau page menyebut group tempat ia berada; panel mendeklarasikan group mana yang tersedia dan urutannya. Tidak ada navigation yang hardcoded: navigation dibangun per request dari registry panel lalu difilter berdasarkan apa yang boleh dilihat current user.
Mendeklarasikan urutan
use PandaPanel\Core\Panel;
use PandaPanel\Core\PanelProvider;
final class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->path('admin')
->auth()
->navigationGroups([
'User Management',
'System',
])
->discoverResources(app_path('Panels/Admin/Resources'));
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use BackedEnum;
use PandaPanel\Resources\Resource;
final class UserResource extends Resource
{
protected static string|BackedEnum|null $navigationGroup = 'User Management';
protected static ?string $navigationIcon = 'users';
protected static int $navigationSort = 10;
}2
3
4
5
6
7
8
9
10
11
Sidebar sekarang menampilkan User Management sebelum System, dengan Users berada di group pertama.
Method
/** @param array<array-key, string|BackedEnum|UnitEnum> $groups */
public function navigationGroups(array $groups): self
/** @return list<string> */
public function getNavigationGroups(): array
/** @return array<string, string> child label => parent label */
public function getNavigationGroupParents(): array2
3
4
5
6
7
8
Pemanggilan bersifat akumulatif dan duplikat dihapus, sehingga plugin dapat menambahkan group tanpa mengganti konfigurasi panel:
$panel
->navigationGroups(['User Management'])
->navigationGroups(['System', 'User Management']);
$panel->getNavigationGroups(); // ['User Management', 'System']2
3
4
5
Menamai group dengan enum
Group dapat menggunakan string, backed enum, atau pure enum:
enum NavigationGroup: string
{
case Content = 'Content';
case System = 'System';
}2
3
4
5
$panel->navigationGroups([
NavigationGroup::Content,
NavigationGroup::System,
]);2
3
4
protected static string|BackedEnum|null $navigationGroup = NavigationGroup::Content;PandaPanel\Support\NavigationGroupName::resolve() menyederhanakan ketiganya menjadi label: string tetap string, backed enum memakai value, dan pure enum memakai case name. Saat mencapai registry semuanya sudah berupa string karena pencocokan group dilakukan berdasarkan label.
Enum layak digunakan ketika lebih dari satu class memakai group yang sama. Typo pada string membuat group kedua yang tampak mirip lalu diam-diam membelah sidebar; typo pada enum case tidak akan lolos compile.
Nested group
String key membuat group yang dinamainya menjadi child dari value-nya:
$panel->navigationGroups([
'Content',
'System',
'Access' => 'System', // Access is drawn indented under System
]);2
3
4
5
Baca pasangan tersebut sebagai child => parent: key adalah group yang ditempatkan, value adalah parent-nya. Kedua label juga melewati NavigationGroupName::resolve(), sehingga enum dapat digunakan di kedua sisi.
Nested group tetap merupakan satu group dengan satu kumpulan item. Sidebar hanya merendernya dengan indent di bawah parent, bukan sebagai top-level heading kedua. Jika parent yang dideklarasikan tidak tampil karena seluruh item di dalamnya ditolak authorization, child group tetap naik ke top level daripada ikut menghilang.
Cara urutan dihitung
PandaPanel\Core\NavigationRegistry memberi sort weight pada setiap group:
| Bucket | Weight | Urutan |
|---|---|---|
| Item tanpa group | -1 | Selalu paling awal. |
| Group yang dideklarasikan panel | 0 + declaration index | Sesuai urutan declaration. |
| Group yang tidak dideklarasikan panel | 1000 + alphabetical index | Alfabetis, setelah semua declared group. |
Undeclared group diurutkan alfabetis, bukan berdasarkan urutan discovery, agar sidebar tidak berubah susunan hanya karena file diganti namanya.
Di dalam group, item diurutkan berdasarkan [sort, label]: $navigationSort terlebih dahulu dan label sebagai tiebreaker. Karena itu dua item dengan sort 0 muncul secara alfabetis.
use PandaPanel\Core\NavigationRegistry;
$registry = new NavigationRegistry(['User Management', 'System']);
$registry->declaredGroups(); // ['User Management', 'System']
$registry->isDeclared('System'); // true
$registry->sortFor(null); // -1
$registry->sortFor('User Management');// 0
$registry->sortFor('Finance', ['Finance', 'Audit']); // 1001
$registry->isCollapsible('System'); // true, unless the panel turned collapsing off2
3
4
5
6
7
8
9
10
Kontribusi sebuah class
Resource dan page menyediakan static property yang sama:
| Property | Type | Default | Efek |
|---|---|---|---|
$navigationGroup | string|BackedEnum|null | null | Heading group. null menaruh item pada ungrouped bucket yang selalu dirender paling awal. |
$navigationLabel | ?string | null | Fallback ke plural label untuk resource atau title untuk page. |
$navigationIcon | ?string | null | Icon registry key. |
$activeNavigationIcon | ?string | $navigationIcon | Icon alternatif ketika item aktif. |
$navigationSort | int | 0 | Urutan item di dalam group. |
$shouldRegisterNavigation | bool | true | false tetap mempertahankan class dan route-nya, tetapi menghapus sidebar entry. |
use PandaPanel\Pages\Page;
final class AuditLog extends Page
{
protected static ?string $navigationLabel = 'Audit';
protected static ?string $navigationIcon = 'shield';
protected static ?string $activeNavigationIcon = 'shield-check';
protected static string|BackedEnum|null $navigationGroup = 'System';
protected static int $navigationSort = 20;
protected static bool $shouldRegisterNavigation = true;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
activeNavigationIcon selalu dikirim bersama item, baik item sedang aktif maupun tidak, sehingga pergantian icon dapat terjadi saat client-side navigation tanpa round trip tambahan.
Panel dapat meng-override posisi resource tanpa mengubah class — lihat Per-Panel Configuration:
use PandaPanel\Resources\ResourceConfiguration;
$panel->resources([
ResourceConfiguration::for(UserResource::class)
->navigationLabel('Directory')
->navigationGroup('Company')
->navigationIcon('building-2')
->navigationSort(99),
]);2
3
4
5
6
7
8
9
Badge
Tidak ada static navigationBadge(). Badge dibuat dengan membangun navigation item sendiri:
use PandaPanel\Contracts\PanelContract;
use PandaPanel\Support\NavigationItem;
public static function navigationItem(PanelContract $panel): ?NavigationItem
{
return NavigationItem::make(
label: 'Users',
href: static::url(panel: $panel instanceof Panel ? $panel : null),
icon: 'users',
badge: static fn (): int => User::query()->whereNull('verified_at')->count(),
sort: 10,
group: 'User Management',
);
}2
3
4
5
6
7
8
9
10
11
12
13
14
Closure badge dievaluasi di server, hanya untuk item yang lolos authorization filter, dan hanya scalar result yang diserialisasi. Closure tidak pernah melewati wire.
Guarantee dari navigation builder
PandaPanel\Support\NavigationBuilder menghasilkan shared prop navigation:
use PandaPanel\Support\NavigationBuilder;
app(NavigationBuilder::class)->for(panel('admin'), request()->path()); // list<array>
app(NavigationBuilder::class)->groupsFor(panel('admin'), request()->path()); // list<NavigationGroup>2
3
4
- Item yang tidak boleh dilihat user dibuang sebelum badge dievaluasi, sehingga badge query tidak pernah berjalan bagi user yang tidak dapat melihat item tersebut.
- Group yang kosong setelah authorization dihapus sepenuhnya.
- Tepat satu item ditandai aktif: exact path match menang; jika tidak, digunakan matching prefix terpanjang. Karena itu
/admin/users/3/editmembuat Users aktif tanpa ikut menyalakan/admin. - Active state dihitung di server dan dikirim sebagai boolean.
- Item yang mengarah ke absolute external URL tidak ikut active matching.
- Resource tanpa
indexpage, dan nested resource, tidak menghasilkan navigation item sama sekali karena memang tidak ada destination yang bisa ditautkan.
Serialized shape per group:
[
'label' => 'User Management', // null for the ungrouped bucket
'sort' => 0,
'collapsible' => true,
'parent' => null, // the group this one nests under
'items' => [
[
'label' => 'Users',
'href' => '/admin/users',
'icon' => 'users',
'activeIcon' => 'users',
'badge' => 7,
'active' => false,
'sort' => 10,
'fullPage' => false,
'children' => [],
],
],
]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Collapse state
Heading group dirender sebagai button jika group collapsible, dan plain label jika tidak:
$panel->sidebar(collapsible: false); // also makes every group non-collapsibleNavigationRegistry::isCollapsible() selalu false untuk ungrouped bucket — heading yang tidak ada tentu tidak dapat diklik — dan untuk group lain mengikuti sidebar flag panel.
Daftar group yang ditutup user adalah satu-satunya navigation state yang dimiliki client. useNavigation() menyimpannya ke local storage menggunakan key panel:{id}:collapsed-groups, sehingga state bertahan setelah reload dan tidak bocor ke panel lain.
import { useNavigation } from '@/panel/composables/useNavigation';
const { groups, items, activeItem, isCollapsed, toggle } = useNavigation();2
3
Catatan
- Group dicocokkan berdasarkan label, termasuk label yang dideklarasikan panel. Mendeklarasikan
NavigationGroup::Systemlalu menulis'System 'dengan trailing space pada resource akan menghasilkan dua group. - Built-in settings page menggunakan group
Accountdengan sort 10, 20, dan 30. Panel yang mendeklarasikanAccountmelaluinavigationGroups()mengontrol posisi block tersebut; jika tidak, group akan ditambahkan secara alfabetis di antara undeclared group. - Root dashboard panel tersedia di panel path dan tidak didaftarkan sebagai page, sehingga tidak memiliki sidebar entry kecuali Anda mendaftarkannya eksplisit melalui
pages([Dashboard::class]). Extra dashboard yang dideklarasikan melaluidashboards()tetap muncul. - Navigation visibility hanyalah convenience. Route, action, page, dan widget melakukan authorization masing-masing; hidden item tidak pernah menjadi security control.
- Tambahkan icon key baru dengan
php artisan panel:icons. Key yang tidak terdaftar tidak merender icon dan tidak memunculkan runtime error.