Detail Hasil Pencarian
Setiap hasil pada palet ditampilkan sebagai judul dan, tepat di bawahnya, satu baris detail berlabel. Judul membantu pengguna mengenali record yang ditemukan; detail membantu membedakan beberapa record yang mungkin memiliki nama yang sama. Keduanya ditentukan di server, sehingga frontend tidak perlu menebak nama maupun identitas record.
Contoh minimal yang dapat digunakan
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Users;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
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, string>
*/
public static function globalSearchResultDetails(Model $record): array
{
return [
'Email' => (string) $record->getAttribute('email'),
'Role' => $record instanceof User && $record->is_admin ? 'Administrator' : 'Member',
];
}
// ... table(), form(), pages()
}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
Hasil pencarian sekarang akan terlihat seperti berikut:
Ada Lovelace
Email: ada@example.com Role: Administrator2
Struktur sebuah hasil
PandaPanel\Search\GlobalSearch mengubah setiap record menjadi PandaPanel\Search\GlobalSearchResult sebelum data apa pun meninggalkan server:
namespace PandaPanel\Search;
final readonly class GlobalSearchResult
{
/**
* @param array<string, string> $details
*/
public function __construct(
public string $title,
public string $url,
public array $details = [],
) {}
/**
* @return array{title: string, url: string, details: array<string, string>}
*/
public function toArray(): array;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Objek ini tidak membawa model, query, maupun closure. Pada saat objek tersebut dibuat, resource sudah melalui proses authorization, judul dan detail sudah ditentukan, serta URL sudah dihasilkan. Anda hampir tidak pernah perlu membuat objek ini secara manual; isi hasil ditentukan melalui tiga method resource yang dijelaskan di bawah.
Judul hasil
use Illuminate\Database\Eloquent\Model;
public static function globalSearchResultTitle(Model $record): string
{
return static::recordTitle($record);
}2
3
4
5
6
Secara default, method tersebut mendelegasikan penentuan judul ke Resource::recordTitle(), yaitu method yang sama yang digunakan oleh breadcrumb dan label record tunggal lainnya:
public static function recordTitle(Model $record): string
{
$attribute = static::$recordTitleAttribute ?? 'name';
$value = $record->getAttribute($attribute);
return is_scalar($value) ? (string) $value : (string) $record->getKey();
}2
3
4
5
6
7
8
Karena itu, cara paling umum untuk menentukan judul hasil bukan dengan meng-override method, melainkan cukup menetapkan properti berikut:
protected static ?string $recordTitleAttribute = 'reference';Override method jika judul perlu disusun dari beberapa nilai, bukan berasal dari satu atribut yang tersimpan:
use Illuminate\Database\Eloquent\Model;
public static function globalSearchResultTitle(Model $record): string
{
return sprintf(
'%s %s',
(string) $record->getAttribute('first_name'),
(string) $record->getAttribute('last_name'),
);
}2
3
4
5
6
7
8
9
10
Perhatikan mekanisme fallback-nya: atribut yang tidak ada atau bukan scalar akan menghasilkan primary key, bukan baris kosong. Hasil pencarian tanpa judul yang terlihat praktis tidak berguna bagi pengguna.
Detail hasil
use Illuminate\Database\Eloquent\Model;
/**
* @return array<string, string>
*/
public static function globalSearchResultDetails(Model $record): array
{
return [];
}2
3
4
5
6
7
8
9
Secara default nilainya kosong. Array tersebut merupakan map label ke value, dan keduanya harus berupa string. Frontend menampilkannya apa adanya:
<span v-for="(value, key) in result.details" :key="key">
{{ key }}: {{ value }}
</span>2
3
Urutan insertion dipertahankan, sehingga entry pertama tampil paling kiri. Jika array kosong, seluruh baris detail tidak akan ditampilkan.
Formatting sebaiknya dilakukan di server karena array dikirim melalui jaringan sebagai string dan Vue tidak melakukan formatting tambahan:
use App\Enums\OrderStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\Number;
/**
* @return array<string, string>
*/
public static function globalSearchResultDetails(Model $record): array
{
$placedAt = $record->getAttribute('placed_at');
$status = $record->getAttribute('status');
return [
'Total' => Number::currency((float) $record->getAttribute('total'), 'EUR'),
'Placed' => $placedAt instanceof Carbon ? $placedAt->toFormattedDateString() : '—',
'Status' => $status instanceof OrderStatus ? $status->label() : (string) $status,
];
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Jika panel menggunakan localization, terjemahkan label dengan mekanisme Laravel biasa:
return [__('Email') => (string) $record->getAttribute('email')];Detail yang membaca relasi
globalSearchResultDetails() dipanggil satu kali untuk setiap hasil. Karena itu, membaca relasi di dalam method tersebut akan menghasilkan satu query per hasil jika relasi belum di-eager load. globalSearchQuery() dimulai dari Resource::query(), dan Resource::query() menerapkan $with, sehingga cukup deklarasikan relasi di sana:
use Illuminate\Database\Eloquent\Model;
final class PostResource extends Resource
{
/** @var list<string> */
protected static array $with = ['author'];
/**
* @return array<string, string>
*/
public static function globalSearchResultDetails(Model $record): array
{
$author = $record->getAttribute('author');
return [
'Author' => $author instanceof Model ? (string) $author->getAttribute('name') : '—',
];
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Jika Anda meng-override globalSearchQuery() dengan query yang tidak melalui query(), tambahkan eager load secara manual pada query tersebut.
Data yang tidak boleh dimasukkan ke detail
Detail merupakan data presentasi dan dapat dibaca oleh siapa pun yang diizinkan mencari resource tersebut. Ada dua aturan utama:
- Hanya scalar/string. Anotasi mendefinisikan
array<string, string>dan tidak ada casting otomatis saat runtime. JikaCarbon, enum, atau model dimasukkan ke array tersebut,json_encodeakan menserialisasikannya sesuai bentuk objeknya, lalu palet dapat menampilkan[object Object]. Lakukan casting dan formatting di server. - Jangan tampilkan data yang tidak semestinya terlihat di halaman record.
canViewAny()membatasi akses pada level resource, bukan per row. Artinya, detail terlihat oleh setiap pengguna yang dapat melakukan pencarian terhadap resource tersebut. Jangan masukkan token, hash, internal note, atau field privat milik pengguna lain. Lihat Keamanan pencarian.
Value dirender melalui interpolasi Vue sehingga akan di-escape. Jika detail mengandung markup, markup tersebut akan tampil sebagai teks, bukan HTML. Dengan demikian, detail tidak dapat menyuntikkan markup ke dalam palet.
Hal yang perlu diperhatikan
- Value panjang tidak dipotong secara otomatis. Baris detail akan membungkus teks; note sepanjang 500 karakter dapat membuat satu hasil lebih tinggi daripada dialog. Potong string di server menggunakan
Str::limit(). - Atribut
nullmenjadi string"", sehingga dapat tampil sepertiLabel:tanpa isi. Gunakan placeholder eksplisit seperti'—'jika value boleh kosong. recordTitle()secara default menggunakan atributname, bukan kolom string pertama pada model. Model tanpa atributnameakan menampilkan primary key sampai$recordTitleAttributedikonfigurasi.- Judul hasil berbeda dengan label navigasi.
pluralLabel()memberi nama pada grup;globalSearchResultTitle()memberi nama pada setiap row hasil. - Detail dihitung untuk setiap hasil, termasuk hasil yang tidak pernah dilihat pengguna. Sebaiknya isi method hanya dengan pembacaan atribut. Jika method detail memanggil API, satu input keyboard dapat berubah menjadi beberapa HTTP request tambahan.
- Hasil tidak memiliki ikon, warna, atau badge per record. Field per hasil hanya
title,url, dandetails; ikon berada pada level grup resource.