Dasar InfolistSchema
PandaPanel\Infolists\InfolistSchema adalah representasi read-only sebuah record: apa yang ditampilkan halaman view, bagaimana informasinya dikelompokkan, dan operasi apa yang tersedia di sampingnya. Gunakan ini ketika halaman view sebuah resource perlu menampilkan informasi yang lebih kaya — atau justru lebih terbatas — daripada apa yang dideklarasikan form.
Class ini sengaja dipisahkan dari FormSchema, bukan dijadikan mode lain dari form. Form melakukan validasi, dehidrasi, dan visibility per page; infolist tidak melakukan semua itu. Jika keduanya memakai class yang sama, setiap entry akan membawa rule yang tidak pernah dipakai dan halaman view akan bergantung diam-diam pada deklarasi edit form. Password pada halaman view user tidak ada sama sekali, bukan sekadar difilter — infolist yang tidak pernah membacanya tidak dapat membocorkannya.
Infolist minimal
Resource mendeklarasikannya dengan override infolist():
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Users;
use App\Models\User;
use PandaPanel\Infolists\Components\BadgeEntry;
use PandaPanel\Infolists\Components\BooleanEntry;
use PandaPanel\Infolists\Components\DateTimeEntry;
use PandaPanel\Infolists\Components\TextEntry;
use PandaPanel\Infolists\InfolistSchema;
use PandaPanel\Infolists\Layouts\Section;
use PandaPanel\Resources\Resource;
use PandaPanel\Tables\Enums\BadgeColor;
final class UserResource extends Resource
{
protected static string $model = User::class;
public static function infolist(InfolistSchema $schema): InfolistSchema
{
return $schema->columns(2)->schema([
Section::make('Account')->columns(2)->schema([
TextEntry::make('name'),
TextEntry::make('email'),
BadgeEntry::make('status')->colors(['active' => BadgeColor::Success]),
BooleanEntry::make('email_verified_at')->labels('Verified', 'Unverified'),
DateTimeEntry::make('created_at')->since(),
]),
]);
}
// table(), form() and pages() omitted
}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
34
35
Tidak ada langkah tambahan. /admin/users/{record} sekarang merender infolist, bukan fallback yang diturunkan dari form.
Mengaktifkan infolist
Secara default Resource::infolist() mengembalikan schema tanpa perubahan:
public static function infolist(InfolistSchema $schema): InfolistSchema
{
return $schema;
}2
3
4
Schema yang tidak disentuh tetap kosong. Dalam kondisi itu ViewRecord fallback dengan membuat entry dari Resource::form() — satu baris label dan nilai untuk setiap form field, dengan PasswordInput dilewati. Artinya infolist adalah peningkatan yang dipilih resource ketika diperlukan, bukan migrasi yang dipaksakan. Kedua jalur tidak pernah dirender bersamaan:
Resource::infolist() | Yang dirender halaman view |
|---|---|
| Dibiarkan default (kosong) | entries — label dan nilai string yang diturunkan dari form |
| Mengembalikan schema non-kosong | infolist — keseluruhan component tree |
isEmpty() yang menentukan jalur mana yang dipakai:
use PandaPanel\Infolists\InfolistSchema;
InfolistSchema::make()->isEmpty(); // true
UserResource::infolist(InfolistSchema::make())->isEmpty(); // false2
3
4
Tidak ada generator yang membuat infolist secara otomatis. php artisan make:panel-resource membuat table, form, dan pages; infolist ditulis manual ketika halaman view memang membutuhkannya.
InfolistSchema, method demi method
Class ini final. Semua method mengembalikan $this kecuali signature-nya menyatakan lain.
| Method | Signature | Fungsi |
|---|---|---|
make() | static make(): self | Membuat schema baru yang kosong. Satu kolom, tanpa component dan action |
schema() | schema(array $components): self | Mengganti component top-level. Di-reindex dengan array_values() |
columns() | columns(int $columns): self | Membagi root grid. Di-clamp ke 1–4 oleh ColumnCount::clamp() |
actions() | actions(array $actions): self | Operasi untuk record secara keseluruhan, dirender di atas infolist |
allActions() | allActions(): array<string, Action> | Semua action yang dideklarasikan schema di mana pun posisinya — whitelist endpoint |
getAction() | getAction(string $name): ?Action | Mengambil satu action berdasarkan nama, atau null |
getComponents() | getComponents(): list<InfolistComponent> | Component top-level, berguna saat caller menggabungkan dua schema |
entries() | entries(): list<Entry> | Semua entry yang sudah di-flatten dari layout |
isEmpty() | isEmpty(): bool | Apakah schema() pernah diberi component |
toArray() | toArray(Model $record): array | ['columns' => int, 'schema' => list, 'actions' => list] — payload yang dikirim ke frontend |
Di luar resource, seluruh siklus pemakaiannya hanya empat baris:
use App\Models\User;
use PandaPanel\Infolists\Components\TextEntry;
use PandaPanel\Infolists\InfolistSchema;
$schema = InfolistSchema::make()
->columns(2)
->schema([TextEntry::make('name'), TextEntry::make('email')]);
$definition = $schema->toArray(User::query()->firstOrFail());
// ['columns' => 2, 'schema' => [['component' => 'entry', 'name' => 'name', ...], ...], 'actions' => []]2
3
4
5
6
7
8
9
10
columns()
Root grid. Entry yang dideklarasikan di luar layout ditempatkan di grid ini; layout selalu mengambil seluruh row.
$schema->columns(3); // three columns
$schema->columns(9); // clamped to 42
Maksimum empat kolom karena resources/js/panel/lib/grid.ts hanya memiliki class Tailwind literal untuk satu sampai empat. Class interpolasi grid-cols-${n} tidak terlihat oleh compiler Tailwind dan tidak akan tersedia di bundle, sehingga schema yang meminta enam kolom tanpa clamp justru bisa tampil sebagai satu kolom. Clamp memastikan renderer selalu memiliki class yang sesuai dengan deklarasi schema.
Grid bertambah lebar mengikuti viewport: selalu satu kolom di phone, maksimal dua pada md, dan tiga atau empat baru pada lg. Span yang lebih lebar dari container di-clamp pada tiap breakpoint alih-alih overflow.
schema()
Menerima apa pun yang mewarisi PandaPanel\Infolists\Components\InfolistComponent — entry maupun layout, dalam kombinasi apa pun:
use PandaPanel\Infolists\Components\TextEntry;
use PandaPanel\Infolists\Layouts\Section;
$schema->schema([
TextEntry::make('reference'), // loose entry
Section::make('Customer')->schema([TextEntry::make('email')]), // layout
]);2
3
4
5
6
7
Loose entry dikumpulkan renderer ke dalam satu card, bukan diberi box masing-masing, sehingga infolist datar tetap terbaca sebagai satu panel. Layout muncul setelah card tersebut sesuai urutan deklarasi.
entries()
Mengembalikan semua entry dalam tree, sedalam apa pun nesting-nya, sesuai urutan deklarasi:
$schema = InfolistSchema::make()->schema([
TextEntry::make('name'),
Section::make('Nested')->schema([
Section::make('Deeper')->schema([TextEntry::make('email')]),
]),
]);
array_map(static fn ($entry): string => $entry->getName(), $schema->entries());
// ['name', 'email']2
3
4
5
6
7
8
9
RepeatableEntry dihitung sebagai satu entry, bukan child entry di dalam item. Child tersebut milik item, bukan record utama. Lihat Repeatable entries.
actions() dan allActions()
actions() mendeklarasikan operasi untuk record secara keseluruhan. allActions() mengumpulkan action tersebut ditambah semua section header action, entry action, dan action yang diregistrasikan pada dialog milik action lain — semuanya di-key berdasarkan nama:
use PandaPanel\Actions\Action;
$schema = InfolistSchema::make()
->actions([Action::make('approve')])
->schema([
Section::make('Details')
->headerActions([Action::make('resend')])
->schema([TextEntry::make('name')->action(Action::make('rename'))]),
]);
array_keys($schema->allActions()); // ['approve', 'resend', 'rename']2
3
4
5
6
7
8
9
10
11
Map itu menjadi whitelist yang dipakai PandaPanel\Http\Controllers\PanelActionController::infolist(). Whitelist ini berbeda dari milik table, sehingga action yang hanya ditampilkan di halaman view tidak bisa dijalankan dari list yang tidak pernah menawarkannya. Lihat Actions in infolists.
Apa yang dilakukan halaman view
PandaPanel\Resources\Pages\ViewRecord::render() membangun schema baru pada setiap request, menyerialisasikannya terhadap record yang berhasil di-resolve, lalu memberikan kedua jalur data tersebut ke Inertia:
$infolist = static::$resource::infolist(InfolistSchema::make());
return Inertia::render('panel/resources/View', [
'infolist' => $infolist->isEmpty() ? null : $infolist->toArray($model),
'entries' => $infolist->isEmpty() ? $this->entries($model) : [],
'recordKey' => $model->getKey(),
'actionEndpoints' => $this->actionEndpoints(),
// page, resource, relations and widgets omitted
]);2
3
4
5
6
7
8
9
resources/js/pages/panel/resources/View.vue merender InfolistRenderer ketika infolist tidak null, dan fallback list jika infolist null. Record selalu di-resolve melalui Resource::query() seperti lookup lain, sehingga record di luar scope menjadi 404 dan tidak dapat dilewati hanya melalui halaman view.
Data yang dikirim ke frontend
[
'columns' => 2,
'schema' => [
['component' => 'entry', 'name' => 'name', 'label' => 'Name', 'type' => 'text', 'value' => 'Grace Hopper', ...],
['component' => 'section', 'heading' => 'Account', 'columns' => 2, 'schema' => [...], 'headerActions' => []],
],
'actions' => [
['name' => 'approve', 'label' => 'Approve', 'type' => 'callback', ...],
],
]2
3
4
5
6
7
8
9
10
Ada empat discriminant component: entry, section, grid, dan tabs. Di bawah entry, field type membedakan sebelas tipe entry. Mirror TypeScript berada di resources/js/panel/types/infolist.ts; switch renderer bersifat exhaustive terhadap union tersebut, sehingga tipe PHP yang tidak memiliki branch frontend akan menjadi compile error.
Tidak ada sesuatu yang executable melewati boundary. formatUsing(), visible(), state(), serta semua action handler dievaluasi di server dan hanya hasil akhirnya yang diserialisasi:
$encoded = json_encode($schema->toArray($record));
// Never contains 'Closure'.2
3
Mengorganisasi infolist besar
Infolist yang memenuhi satu layar sebaiknya tidak ditulis inline di resource. Convention yang dipakai contoh adalah satu class khusus schema, berdampingan dengan form dan table:
app/Panels/Admin/Resources/Users/
├── UserResource.php
├── Forms/UserForm.php
├── Infolists/UserInfolist.php
└── Tables/UsersTable.php2
3
4
5
public static function infolist(InfolistSchema $schema): InfolistSchema
{
return UserInfolist::configure($schema);
}2
3
4
configure() hanyalah static method biasa yang menerima dan mengembalikan schema. Framework tidak mewajibkan pemisahan ini — satu-satunya contract adalah infolist() — tetapi pemisahan tersebut menjaga resource tetap mudah dibaca dan memungkinkan test membangun schema tanpa page.
Styling
Renderer infolist memiliki class panel-infolist, dan panel dapat menambahkan class sendiri melalui CSS hook infolist:
$panel->cssHooks(['infolist' => 'my-infolist']);Nama hook menggunakan allowlist; infolist termasuk salah satunya. Lihat CSS hooks.
Testing
Dua helper membaca deklarasi schema menggunakan lookup yang sama dengan halaman dan endpoint:
use App\Models\User;
use App\Panels\Admin\Resources\Users\UserResource;
it('shows what the view page should show', function (): void {
$record = User::factory()->create();
expect(panelInfolistLabels(UserResource::class, $record))
->toContain('Email')
->not->toContain('Password');
panelInfolistActions(UserResource::class)->assertExists('note');
});2
3
4
5
6
7
8
9
10
11
12
panelInfolistLabels() menelusuri serialized tree dan mengembalikan semua label entry, seberapa dalam pun nesting-nya. panelInfolistActions() adalah PandaPanel\Testing\TestsActions::infolist() yang dibatasi pada whitelist infolist — action yang tidak ditemukan helper juga tidak dapat dijalankan endpoint. Lihat Testing actions.
Gotcha
- Schema kosong bukan berarti halaman kosong. Resource tanpa infolist tetap memiliki view page yang diturunkan dari form. Jika halaman view benar-benar harus menampilkan tidak ada data, Anda tetap perlu mendeklarasikan infolist dengan struktur yang sesuai.
- Fallback melakukan stringification melalui
displayValue(). Boolean menjadiYes/No, scalar dicast, dan nilai lain menjadi null. Fallback sengaja dibuat sederhana; formatting yang kaya adalah tugas infolist. toArray()membutuhkan record. Berbeda denganFormSchema::toArray(), model tidak opsional — entry tanpa record tidak memiliki nilai yang bisa di-resolve.- Layout yang kosong karena semua entry hidden hilang sepenuhnya. Section, grid, atau tab yang seluruh child-nya mengembalikan null tidak merender heading kosong. Lihat Layouts.
columns()di-clamp, bukan divalidasi.columns(6)diam-diam menjadi empat. Itu lebih dekat dengan intent developer daripada hasil satu kolom yang akan muncul jika nilai enam tidak di-clamp.- Schema dibangun ulang setiap request.
infolist()adalah static method yang menerimaInfolistSchemabaru; tidak ada state yang dicache antar-request, sehingga closure yang membacaauth()->user()selalu melihat user pada request saat ini.