URL Hasil Pencarian
Setiap hasil pada palet membawa URL yang dihasilkan oleh server. Frontend tidak pernah membangun URL sendiri, tidak melakukan resolve route, dan tidak menentukan di mana sebuah record berada — frontend hanya me-render link yang diberikan. Halaman ini menjelaskan tujuan default, cara mengubahnya, serta kondisi ketika mekanisme default tidak dapat digunakan.
Contoh minimal yang dapat digunakan
Tidak diperlukan konfigurasi tambahan. Resource yang sudah mengaktifkan pencarian otomatis menghasilkan link yang dapat digunakan.
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Users;
use App\Models\User;
use App\Panels\Admin\Resources\Users\Pages\EditUser;
use App\Panels\Admin\Resources\Users\Pages\ListUsers;
use App\Panels\Admin\Resources\Users\Pages\ViewUser;
use PandaPanel\Resources\Resource;
final class UserResource extends Resource
{
protected static string $model = User::class;
/** @var list<string> */
protected static array $globalSearchAttributes = ['name', 'email'];
/**
* @return array<string, class-string>
*/
public static function pages(): array
{
return [
'index' => ListUsers::class,
'view' => ViewUser::class,
'edit' => EditUser::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
29
30
31
Hasil untuk user dengan ID 1 akan memperoleh "url": "/admin/users/1" — yaitu halaman view, karena resource ini mendeklarasikan halaman tersebut.
Perilaku default
use Illuminate\Database\Eloquent\Model;
public static function globalSearchResultUrl(Model $record): string
{
$pages = static::pages();
if (array_key_exists('view', $pages)) {
return static::url('view', $record);
}
return array_key_exists('edit', $pages)
? static::url('edit', $record)
: static::url();
}2
3
4
5
6
7
8
9
10
11
12
13
14
Urutan pemilihan tujuan adalah sebagai berikut:
| Kondisi | Tujuan |
|---|---|
pages() memiliki key view | static::url('view', $record) |
jika tidak, pages() memiliki key edit | static::url('edit', $record) |
| jika keduanya tidak ada | static::url() — halaman index |
Fallback ke index mencegah resource yang tidak memiliki halaman view maupun edit menghasilkan link ke route yang sebenarnya tidak pernah didaftarkan. Setiap halaman tujuan melakukan authorization secara independen ketika dibuka, sehingga urutan di atas menentukan route yang dapat dijangkau, bukan permission pengguna.
Mengubah URL hasil
use Illuminate\Database\Eloquent\Model;
public static function globalSearchResultUrl(Model $record): string
{
return static::url('edit', $record);
}2
3
4
5
6
Key apa pun yang dideklarasikan pada pages() dapat digunakan, termasuk custom page:
use Illuminate\Database\Eloquent\Model;
/**
* @return array<string, class-string>
*/
public static function pages(): array
{
return [
'index' => ListOrders::class,
'view' => ViewOrder::class,
'invoice' => OrderInvoice::class,
];
}
public static function globalSearchResultUrl(Model $record): string
{
return static::url('invoice', $record);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Tujuan juga dapat ditentukan per record jika halaman yang tepat bergantung pada status record:
use Illuminate\Database\Eloquent\Model;
public static function globalSearchResultUrl(Model $record): string
{
return $record->getAttribute('status') === 'draft'
? static::url('edit', $record)
: static::url('view', $record);
}2
3
4
5
6
7
8
Anda juga dapat mengarahkan hasil ke resource lain atau halaman di luar resource jika memang di sanalah record tersebut seharusnya dikelola:
use App\Panels\Admin\Resources\Invoices\InvoiceResource;
use Illuminate\Database\Eloquent\Model;
public static function globalSearchResultUrl(Model $record): string
{
return InvoiceResource::url('view', $record->getAttribute('invoice_id'));
}2
3
4
5
6
7
Resource::url()
Gunakan method ini sebagai cara utama untuk membangun URL panel. URL dibuat berdasarkan nama route, sehingga perubahan path panel tidak meninggalkan string URL hard-coded yang harus diperbaiki secara manual:
public static function url(
string $page = 'index',
Model|int|string|null $record = null,
Panel|string|null $panel = null,
Model|int|string|null $parent = null,
): string;2
3
4
5
6
| Argument | Tipe | Default | Keterangan |
|---|---|---|---|
$page | string | 'index' | key dari pages() |
$record | Model|int|string|null | null | record yang dituju; model akan diubah menjadi key-nya |
$panel | Panel|string|null | null | panel tujuan; secara default menggunakan panel aktif |
$parent | Model|int|string|null | null | parent record untuk nested resource |
Method mengembalikan URL relatif menggunakan route(..., absolute: false), sesuai kebutuhan navigasi Inertia. Nama route yang di-resolve adalah panel.{panelId}.resources.{slug}.{page}, dengan slug sesuai slug yang digunakan ketika resource didaftarkan pada panel tersebut.
Ada dua guard yang penting diketahui:
assertRegisteredIn()melemparPandaPanel\Exceptions\PanelRegistrationExceptionjika resource tidak terdaftar pada panel tujuan. Isolasi panel hanya benar-benar aman jika permintaan URL menuju panel yang salah gagal secara eksplisit.resolvePanel()melempar exception jika tidak ada panel aktif dan tidak ada panel yang diberikan sebagai argument, alih-alih menebak panel mana yang dimaksud.
Method pembaca terkait:
public static function routeName(string $page = 'index', Panel|string|null $panel = null): string;
// 'panel.admin.resources.users.view'2
Singular resource
Halaman pada singular resource tidak memiliki {record} karena tidak ada record lain yang perlu dipilih. Karena itu, url() akan mengabaikan parameter record:
if ($record !== null && ! static::isSingular()) {
$parameters['record'] = $record instanceof Model ? $record->getKey() : $record;
}2
3
Mekanisme default tetap bekerja tanpa perubahan: seluruh hasil dari singular resource mengarah ke halaman yang sama, dan itu benar karena resource hanya mewakili satu record. Memberikan parameter record tetap aman karena parameter tersebut akan diabaikan.
Nested resource
Nested resource — yaitu resource yang mendeklarasikan $parentResource — adalah kasus ketika perilaku default tidak dapat bekerja:
Resource::query()dimulai dari relasi parent dan memanggilParentRecord::require(). Method tersebut melempar exception jika tidak ada parent yang terikat ke request. Request pencarian tidak mengikat parent apa pun, sehinggaglobalSearchQuery()akan gagal.Resource::url()juga memanggilParentRecord::require()untuk nested resource. Jadi, bahkan jika query dapat dijalankan, URL default tetap tidak dapat dibuat tanpa parent.
Cari nested resource hanya jika Anda meng-override keduanya dan menyediakan parent secara eksplisit:
use App\Models\Task;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
/**
* Jangan gunakan `static::query()`: method tersebut membutuhkan parent record,
* sedangkan request pencarian tidak memiliki parent yang terikat.
*/
public static function globalSearchQuery(): Builder
{
return Task::query()->with('project');
}
public static function globalSearchResultUrl(Model $record): string
{
return static::url('view', $record, parent: $record->getAttribute('project_id'));
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Perlu diperhatikan bahwa memulai query langsung dari model, bukan dari query(), berarti tenant scope dan modifikasi query per panel tidak otomatis diterapkan. Karena itu, deklarasikan kembali constraint yang dibutuhkan:
use App\Models\Task;
use Illuminate\Database\Eloquent\Builder;
use PandaPanel\Tenancy\Tenancy;
public static function globalSearchQuery(): Builder
{
$tenant = Tenancy::require();
return Task::query()
->with('project')
->whereHas('project', static fn (Builder $project): Builder => $project->whereBelongsTo($tenant));
}2
3
4
5
6
7
8
9
10
11
12
Jika konfigurasi ini terasa terlalu rumit atau berisiko, tidak memasukkan nested resource ke palet dan mengarahkan pengguna mencari parent resource adalah pilihan yang sepenuhnya masuk akal.
Bagaimana palet menggunakan URL
GlobalSearchResult hanya mempertahankan URL relatif dan scheme http, https, mailto, serta tel; custom URL yang dianggap tidak aman akan diubah menjadi #. resources/js/panel/components/PanelSearch.vue menerapkan guard yang sama sebelum me-render <Link> atau memanggil router.visit(). Konsekuensinya:
- URL harus dapat dikunjungi oleh Inertia — umumnya berupa relative path di dalam aplikasi. URL absolut ke host lain akan diperlakukan sebagai Inertia request dan dapat gagal.
- Pattern
fullPageUrls()milik panel tidak berlaku di sini. Pattern tersebut dievaluasi oleh navigation builder untuk item sidebar; hasil pencarian selalu menggunakan navigasi Inertia. - Navigasi apa pun akan menutup palet, termasuk navigasi yang dimulai dari bagian lain aplikasi.
Hal yang perlu diperhatikan
- Record yang dapat ditemukan belum tentu dapat dibuka. Mekanisme default tidak memanggil
canView(); ia hanya memilih halaman tujuan. Authorization dilakukan ketika halaman dibuka, sehingga pengguna bisa melihat hasil pencarian lalu menerima 403 saat mengkliknya. PersempitglobalSearchQuery()jika record memang tidak boleh muncul sama sekali — lihat Keamanan pencarian. - Resource yang hanya memiliki
indexpadapages()akan mengarahkan seluruh hasil ke daftar yang sama. Ini adalah fallback yang disengaja, bukan bug, tetapi biasanya membuat pencarian global kurang berguna untuk resource tersebut. Tambahkan halaman view atau override URL hasil. url()melempar exception jika resource tidak terdaftar pada panel aktif. Dari sisi UI, ini dapat terlihat sebagai pencarian yang menghasilkan "Nothing found." Periksa log aplikasi.- Key custom page bersifat bebas.
pages()dapat memetakan'invoice' => …; tidak ada validasi yang memastikanglobalSearchResultUrl()menggunakan key yang benar. Key yang tidak ada akan menghasilkan error route-not-defined saat pencarian dijalankan. - URL dibuat untuk setiap hasil. Secara normal ini hanya route lookup, bukan query database, sehingga biayanya kecil. Namun, jika
globalSearchResultUrl()Anda sendiri menjalankan query, biaya tersebut akan terjadi sekali per row hasil.