Dasar-dasar TableSchema
PandaPanel\Tables\TableSchema adalah deskripsi deklaratif dari tabel index sebuah resource: kolom, filter, action, serta cara tabel melakukan sorting, search, dan pagination. Gunakan setiap kali resource membutuhkan sebuah daftar — dan schema yang sama juga digunakan di relation manager, table widget, atau data yang sama sekali tidak berasal dari database.
Schema juga berfungsi sebagai whitelist. Sorting, searching, filtering, dan column visibility hanya membaca apa yang secara eksplisit dideklarasikan oleh kolom atau filter. Nilai lain dari URL diabaikan, bukan diteruskan begitu saja ke query builder.
Tabel minimal
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Posts;
use App\Models\Post;
use PandaPanel\Resources\Resource;
use PandaPanel\Tables\Columns\DateTimeColumn;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\Enums\SortDirection;
use PandaPanel\Tables\TableSchema;
final class PostResource extends Resource
{
protected static string $model = Post::class;
public static function table(TableSchema $table): TableSchema
{
return $table
->columns([
TextColumn::make('title')->searchable()->sortable()->toggleable(false),
TextColumn::make('status')->sortable(),
DateTimeColumn::make('created_at')->label('Published')->sortable(),
])
->defaultSort('created_at', SortDirection::Descending)
->searchPlaceholder('Search posts...');
}
// form() and pages() as usual
}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
ListRecords memanggil Resource::table(TableSchema::make()) pada setiap render. Karena itu schema dibangun ulang untuk setiap request dan closure di dalam schema dapat mengakses user, tenant, dan locale saat ini.
Cara request berubah menjadi row
Empat objek bekerja dalam urutan berikut:
Resource::query()menghasilkan builder. Layer tabel tidak membuat query sendiri, sehingga tenant scope, module scope, dan permission scope tetap diterapkan.PandaPanel\Tables\TableQuerymembaca URL —page,perPage,search,columnSearch,sort,direction,filters,columns,group— memvalidasi setiap value terhadap schema, lalu menerapkan value yang lolos.TableSchema::toRow()menserialisasi setiap record menjadi cells, metadata cell per row, dan record action yang boleh dijalankan pengguna.ListRecords::render()mengirimtable(definition),state(state yang benar-benar diterapkan server),rows,summaries,pagination, dantabske Vue.
use Illuminate\Http\Request;
use PandaPanel\Tables\TableQuery;
use PandaPanel\Tables\TableSchema;
$schema = PostResource::table(TableSchema::make());
$query = new TableQuery($schema, request(), namespace: null, sessionKey: null);
$records = $query->paginate(PostResource::query());
$rows = array_map(
static fn ($record): array => $schema->toRow($record),
$records->items(),
);
$state = $query->state();2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
state() berisi apa yang benar-benar diterapkan server, bukan semua yang diminta request. Sort column yang ditolak kembali sebagai null dan filter yang ditolak tidak muncul. Dengan demikian, kontrol UI tidak pernah terlihat aktif untuk sesuatu yang sebenarnya diabaikan query.
Kolom
Kolom adalah satu-satunya bagian schema yang wajib. Semua tipe meng-extend PandaPanel\Tables\Columns\Column, sehingga sortable(), searchable(), frozen(), tooltip(), url(), width(), dan summarize() tersedia pada semuanya.
use PandaPanel\Tables\Columns\BadgeColumn;
use PandaPanel\Tables\Columns\NumberColumn;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\Enums\BadgeColor;
$table->columns([
TextColumn::make('reference')->searchable(individually: true)->toggleable(false),
BadgeColumn::make('status')->colors(['open' => BadgeColor::Info])->sortable(),
NumberColumn::make('total')->prefix('$')->decimals(2)->sortable(),
]);2
3
4
5
6
7
8
9
10
Mendeklarasikan nama yang sama dua kali melempar PandaPanel\Exceptions\PanelSchemaException pada setter. Nama kolom adalah key yang digunakan cell, visibility, search term, dan sort, sehingga duplikasi tidak memiliki arti yang aman. Lihat Kolom.
Filter
use PandaPanel\Tables\Filters\SelectFilter;
use PandaPanel\Tables\Filters\TernaryFilter;
$table->filters([
SelectFilter::make('status')->options(['open' => 'Open', 'done' => 'Done']),
TernaryFilter::make('published_at')->nullable()->labels('Published', 'Draft', 'Anyone'),
]);2
3
4
5
6
7
Nama filter harus unik karena state filter disimpan di query string dengan nama sebagai key. Lihat Filter dan Filter query builder.
Sorting dan searching
use PandaPanel\Tables\Enums\SortDirection;
$table
->defaultSort('created_at', SortDirection::Descending)
->defaultSortOptionLabel('Newest first')
->searchPlaceholder('Search by name or reference...')
->searchDebounce(500)
->searchOnBlur()
->splitSearchTerms(false);2
3
4
5
6
7
8
9
defaultSort() divalidasi terhadap kolom yang dideklarasikan ketika schema diserialisasi, bukan saat setter dipanggil. Ini memungkinkan defaultSort() dipanggil sebelum columns(). Jika nama kolom tidak tersedia, PanelSchemaException::unknownDefaultSort() dilempar. Lihat Pengurutan dan Pencarian.
Selection dan action
use PandaPanel\Actions\DeleteAction;
use PandaPanel\Actions\DeleteBulkAction;
use PandaPanel\Actions\EditAction;
use PandaPanel\Actions\ViewAction;
use PandaPanel\Tables\Enums\RecordActionsPosition;
$table
->recordActions([
ViewAction::make(PostResource::class),
EditAction::make(PostResource::class),
DeleteAction::make(PostResource::class),
])
->recordActionsPosition(RecordActionsPosition::AfterColumns)
->recordActionsLabel('Manage')
->bulkActions([DeleteBulkAction::make(PostResource::class)]);2
3
4
5
6
7
8
9
10
11
12
13
14
15
Memberikan array non-empty ke bulkActions() otomatis mengaktifkan selectable(true), karena bulk action tanpa mekanisme memilih row tidak memiliki kegunaan. selectable() tetap tersedia secara mandiri untuk tabel yang membutuhkan checkbox tanpa bulk action. Setiap kumpulan action menolak duplicate name dan menolak action tanpa handler, URL, form, atau modal melalui PanelSchemaException::inertAction().
Lihat Action record, Bulk action, dan Header dan toolbar action.
Row per halaman
$table->perPageOptions([10, 25, 50, 100])->defaultPerPage(25);perPageOptions() membuang nilai non-positif, menghapus duplikasi, dan melakukan sort. ?perPage= yang berada di luar list diabaikan. Jika defaultPerPage() bukan salah satu pilihan, nilai tersebut fallback ke pilihan pertama. Lihat Pagination.
Grouping dan tab
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Tables\Group;
$table
->groups([
Group::make('status')->titleUsing(static fn (Model $r): string => ucfirst($r->status)),
])
->defaultGroup('status');2
3
4
5
6
7
8
Grouping membagi row ke band dengan heading tetapi tidak mengubah record yang dikembalikan query. Tab dideklarasikan pada list page, bukan pada schema, karena tab merupakan named scope atas query resource. Lihat Grouping dan Tab.
Empty state
use PandaPanel\Actions\CreateAction;
$table
->emptyState(
heading: 'No posts match this view',
description: 'Adjust the search or filters, or write one.',
icon: 'file-text',
)
->emptyStateActions([CreateAction::modal(PostResource::class)])
->emptyStateComponent('Panels/Admin/EmptyStates/NoPosts');2
3
4
5
6
7
8
9
10
Heading default adalah No records found. emptyStateComponent() menerima build-time registry key di bawah resources/js/pages/Panels/{Panel}/EmptyStates/, bukan markup.
Column manager, persistence, reordering, dan freezing
$table
->reorderableColumns()
->columnManagerInModal()
->persistColumnsInSession()
->persistSearchInSession()
->persistSortInSession()
->persistFiltersInSession()
->reorderable('position')
->frozenActions();2
3
4
5
6
7
8
9
reorderableColumns() mengizinkan pengguna mengatur ulang kolom. reorderable('position') mengizinkan pengguna menyeret baris, menulis urutan ke kolom tersebut, dan menetapkan default sort ascending ke kolom yang sama. Lihat Pengelola kolom, State tabel yang dipertahankan, Reordering, dan Kolom frozen dan pinned.
Semua method TableSchema
Semua builder method mengembalikan self.
| Method | Default | Dokumentasi |
|---|---|---|
make(): self | — | halaman ini |
columns(array $columns) | [] | Kolom |
filters(array $filters) | [] | Filter |
groups(array $groups) | [] | Grouping |
defaultGroup(string $name) | null | Grouping |
defaultSort(string $column, SortDirection $direction = SortDirection::Descending) | tidak ada, Descending | Pengurutan |
defaultSortOptionLabel(string $label) | null | Pengurutan |
perPageOptions(array $options) | [10, 25, 50, 100] | Pagination |
defaultPerPage(int $perPage) | 25 | Pagination |
searchPlaceholder(string $placeholder) | Search... | Pencarian |
searchDebounce(int $milliseconds) | 300, di-clamp ke >= 0 | Pencarian |
searchOnBlur(bool $onBlur = true) | false | Pencarian |
splitSearchTerms(bool $split = true) | true | Pencarian |
persistSearchInSession(bool $persist = true) | false | State yang dipertahankan |
persistSortInSession(bool $persist = true) | false | State yang dipertahankan |
persistFiltersInSession(bool $persist = true) | false | State yang dipertahankan |
persistColumnsInSession(bool $persist = true) | false | Pengelola kolom |
deferFilters(bool $defer = true) | false | Filter |
filtersTrigger(string $label, ?string $icon = null) | Filters, null | Filter |
filtersApplyLabel(string $label) | Apply filters | Filter |
filtersResetLabel(string $label) | Clear | Filter |
showFiltersResetAction(bool $show = true) | true | Filter |
reorderableColumns(bool $reorderable = true) | false | Pengelola kolom |
deferColumnManager(bool $defer = true) | false | Pengelola kolom |
columnManagerTrigger(string $label, ?string $icon = null) | Columns, null | Pengelola kolom |
columnManagerInModal(bool $inModal = true) | false | Pengelola kolom |
showColumnManagerReset(bool $show = true) | true | Pengelola kolom |
emptyState(string $heading, ?string $description = null, ?string $icon = null) | No records found | halaman ini |
emptyStateComponent(string $component) | null | halaman ini |
selectable(bool $selectable = true) | false | Bulk action |
reorderable(string $column) | nonaktif | Reordering |
recordActions(array $actions) | [] | Action record |
recordActionsPosition(RecordActionsPosition $position) | AfterColumns | Action record |
recordActionsLabel(string $label) | null | Action record |
bulkActions(array $actions) | [], mengaktifkan selection | Bulk action |
headerActions(array $actions) | [] | Toolbar action |
toolbarActions(array $actions) | [] | Toolbar action |
emptyStateActions(array $actions) | [] | Toolbar action |
frozenActions(bool $frozen = true) | false | Pinned column |
Method pembaca, untuk code yang menginspeksi schema alih-alih membangunnya:
$schema->getColumns(); // list<Column>
$schema->getColumn('title'); // ?Column
$schema->getSortableColumns(); // list<Column>
$schema->getIndividuallySearchableColumns();
$schema->getIndividuallySearchableColumn('title');
$schema->getSearchColumns(); // list<string>, local columns only
$schema->getSearchRelations(); // list<string>, dotted names
$schema->isSearchable(); // bool
$schema->columnNames(); // list<string>
$schema->defaultVisibleColumnNames(); // list<string>
$schema->toggleableColumnNames(); // list<string>
$schema->getFilters(); // list<Filter>
$schema->getFilter('status'); // ?Filter
$schema->defaultFilters(); // array<string, mixed>
$schema->getGroups(); // list<Group>
$schema->getGroup('status'); // ?Group
$schema->getDefaultGroup(); // ?string
$schema->getDefaultSortColumn(); // ?string
$schema->getDefaultSortDirection(); // SortDirection
$schema->getPerPageOptions(); // list<int>
$schema->getDefaultPerPage(); // int, clamped to the options
$schema->isSelectable(); // bool
$schema->isReorderable(); // bool
$schema->getReorderColumn(); // ?string
$schema->hasFrozenStart(); // bool
$schema->hasFrozenActions(); // bool
$schema->hasSummaries(); // bool
$schema->getRecordActions(); // list<Action>
$schema->getRecordAction('approve'); // ?Action, column actions included
$schema->getBulkActions();
$schema->getBulkAction('delete');
$schema->getHeaderActions();
$schema->getToolbarActions();
$schema->getEmptyStateActions();
$schema->getTableAction('export'); // ?Action, across all three bars2
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
Dan method yang menghasilkan payload:
$schema->applyColumnQueries($query); // void; aggregates into the select
$schema->summaries($query, $records); // array<string, list<array>>
$schema->groupSummaries($query, $records, $group); // array<string, array<string, list<array>>>
$schema->toRow($record, $group = null); // one serialized row
$schema->toArray(); // the definition sent to Vue2
3
4
5
Data yang diterima frontend
toArray() tidak mengirim closure, query, atau model class. Top-level key-nya adalah:
| Key | Bentuk |
|---|---|
columns | list definisi kolom, dibedakan oleh type |
filters | list definisi filter, dibedakan oleh type |
groups, defaultGroup | pilihan cara row dapat dikelompokkan |
columnManager | reorderable, deferred, triggerLabel, triggerIcon, resetLabel, showReset, modal, toggleable |
filterBehaviour | deferred, triggerLabel, triggerIcon, applyLabel, resetLabel, showReset |
searchable, searchPlaceholder, searchDebounce, searchOnBlur, individualSearchColumns | perilaku pencarian |
selectable, reorderable | row selection dan row dragging |
frozen | { start: bool, actions: bool } |
perPageOptions, defaultPerPage | pagination |
defaultSort | { column, direction, label } atau null |
bulkActions, headerActions, toolbarActions | action yang sudah diserialisasi dan diotorisasi tanpa record |
recordActions | { position, label }; tombol aktual berada pada setiap row |
emptyState | heading, description, icon, component, actions |
Hal yang perlu diperhatikan
- Schema dibangun ulang untuk setiap request dan menyimpan resolved state. Jangan cache instance
TableSchemaantar-request atau menggunakan instance yang sama untuk dua assertion test yang seharusnya independen. defaultSort()dapat melempar exception terlambat. Validasi dilakukan ditoArray(), sehingga nama kolom yang salah baru terlihat ketika halaman dirender.defaultPerPage()di-clamp. Menetapkan20tanpa menambahkannya keperPageOptions()akan menggunakan pilihan pertama, tanpa exception.- Kolom non-toggleable tidak pernah dapat disembunyikan, apa pun isi request. Ini menjaga kolom identitas tetap terlihat.
- Filter tidak mempersempit record lookup. Filter berada di
TableQuery::paginate(), bukanResource::query(). Record yang tidak terlihat karena filter masih dapat dibuka melalui URL jika user berhak mengaksesnya. - Search term di-escape.
%dan_pada kotak search dicocokkan secara literal, bukan sebagai wildcard LIKE. - State tabel menggunakan namespace ketika sebuah page memiliki lebih dari satu tabel. Relation manager menggunakan
relations.{key}, sedangkan table widget menggunakanwidgets.{widget-id}. Tanpa namespace, semua tabel akan berebut?page=yang sama.
Lihat juga
- Kolom
- Pencarian dan Pengurutan
- Filter, Filter query builder, Tab
- Grouping dan Summary
- Pagination dan State tabel yang dipertahankan
- Pengelola kolom, Kolom frozen dan pinned, Kolom editable
- Action record, Bulk action, Header dan toolbar action, Reordering
- Kolom relasi dan Tabel data array
- Referensi API tabel
- Membuat resource
- Action