Inertia Pages
Setiap screen panel adalah Inertia response biasa: nama component, sejumlah props, dan shared props yang ditambahkan middleware panel. Tidak ada SPA API terpisah. Gunakan halaman ini ketika Anda perlu mengetahui component apa yang menjawab sebuah URL, prop apa yang diterima, atau mengapa screen panel justru dirender di dalam shell milik application.
Contoh minimal yang berfungsi
Page class menyebut nama component; Inertia merendernya; component mendeklarasikan layout-nya sendiri.
use PandaPanel\Pages\Page;
final class Reports extends Page
{
protected static ?string $title = 'Reports';
protected static string $component = 'Panels/Admin/Pages/Reports';
}2
3
4
5
6
7
8
<!-- resources/js/pages/Panels/Admin/Pages/Reports.vue -->
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import PageHeader from '@/panel/components/PageHeader.vue';
import PanelLayout from '@/panel/layouts/PanelLayout.vue';
import type { PageMetadata } from '@/panel/types/page';
defineOptions({ layout: PanelLayout });
defineProps<{ page: PageMetadata }>();
</script>
<template>
<Head :title="page.title" />
<PageHeader :heading="page.heading" :subheading="page.subheading" />
</template>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Tidak ada yang perlu diregistrasikan di resources/js/app.ts.
Shipped page components
vendor:publish --tag=panda-panel-assets menulis components berikut ke resources/js/pages/panel/. Masing-masing menjadi default untuk satu jenis screen.
| Component | Dirender oleh | Default untuk |
|---|---|---|
panel/Dashboard | PandaPanel\Pages\Dashboard | root screen panel |
panel/Page | PandaPanel\Pages\Page | seluruh standalone page |
panel/resources/Index | Resources\Pages\ListRecords | list screen resource |
panel/resources/Create | Resources\Pages\CreateRecord | create form |
panel/resources/Edit | Resources\Pages\EditRecord | edit form |
panel/resources/View | Resources\Pages\ViewRecord | record view |
panel/resources/ManageRelated | Resources\Pages\ManageRelatedRecords | relation page |
panel/resources/Integrations | PanelIntegrationController | integrations screen |
panel/settings/Profile | Pages\Settings\ProfileSettings | profile settings |
panel/settings/Security | Pages\Settings\SecuritySettings | security settings |
panel/settings/Appearance | Pages\Settings\AppearanceSettings | appearance settings |
panel/auth/Login | PanelAuthController::login | panel dengan login page sendiri |
panel/auth/Register | PanelAuthController::register | |
panel/auth/ForgotPassword | PanelAuthController::requestPasswordReset | |
panel/auth/ResetPassword | PanelAuthController::resetPassword | |
panel/auth/VerifyEmail | PanelAuthController::verifyEmail | |
panel/auth/EmailCode | PanelTwoFactorController | email code challenge |
Nama component adalah path di bawah resources/js/pages/, sesuai yang diharapkan Inertia resolver. Lowercase panel/ adalah milik framework; capitalised Panels/ adalah milik application Anda.
Shared props
PandaPanel\Http\Middleware\SharePanelData menambahkan tujuh prop pada setiap response web melalui Inertia::share(). Method tersebut melakukan merge, sehingga HandleInertiaRequests milik application tidak diubah.
export interface PanelSharedProps {
panel: PanelDefinition | null; // null di luar panel, tidak pernah absent
navigation: NavigationGroup[];
panels: PanelSummary[];
broadcasting: PanelBroadcasting;
search: PanelSearchSettings;
notifications: PanelNotificationSettings;
tenancy: PanelTenancy | null; // null jika panel tidak menggunakan tenancy
}2
3
4
5
6
7
8
9
Baca props tersebut melalui composable, bukan langsung melalui usePage():
import { usePanel } from '@/panel/composables/usePanel';
import { useNavigation } from '@/panel/composables/useNavigation';
const { panel, shell, notifications } = usePanel();
const { groups, activeItem } = useNavigation();2
3
4
5
panelSharedProps() di @/panel/types/shared adalah satu-satunya cast yang disengaja pada seluruh frontend, dan sengaja dipusatkan di satu file. Module augmentation terhadap @inertiajs/core harus dapat dijangkau tsconfig milik host dan merge dengan declaration starter kit. Jika salah satu bagian tersebut gagal, type dapat jatuh ke {} dan setiap read kemudian menjadi compile error pada build application. Test memastikan tidak ada file lain di resources/js/panel yang membaca shared prop langsung dari usePage().
Page props per screen
Setiap jenis page mengirim props miliknya sendiri di samping shared props.
panel/Page
withDefaults(
defineProps<{
page: PageMetadata;
widgets: WidgetDefinition[];
widgetData?: WidgetData | null;
}>(),
{ widgetData: null },
);2
3
4
5
6
7
8
Page::render() juga mengirim filters, walaupun generic renderer tidak mendeklarasikannya. Hanya panel/Dashboard yang menggambar filter bar.
panel/Dashboard
withDefaults(
defineProps<{
page: PageMetadata;
widgets: WidgetDefinition[];
widgetData?: WidgetData | null;
filters?: { form: FormDefinition } | null;
}>(),
{ widgetData: null, filters: null },
);2
3
4
5
6
7
8
9
panel/resources/Index
Screen ini memiliki tiga belas props, paling banyak dibanding screen lain:
| Prop | Type | Catatan |
|---|---|---|
page | PageMetadata | |
resource | ResourceMeta | slug, labels, index URL |
table | TableDefinition | serialized schema |
state | TableState | search, sort, direction, per page, filters, columns, group |
rows | TableRow[] | cells yang sudah diformat |
pagination | PaginationMeta | |
summaries, groupSummaries | TableSummaries | kosong jika table tidak mendeklarasikan summary |
actionEndpoints | ActionEndpoints | URL yang dipakai useActions untuk POST |
tabs | TableTab[] | kosong jika table tidak memiliki tabs |
headerWidgets, footerWidgets | WidgetDefinition[] | widget yang ditempatkan page di sekitar content |
widgetData | WidgetData | null | deferred |
panel/resources/Create dan panel/resources/Edit
| Prop | Dikirim oleh |
|---|---|
page, resource | keduanya |
form | serialized FormSchema, termasuk state pada edit |
submitUrl | store pada create, update pada edit |
optionsUrl | endpoint untuk live/searchable select options |
uploadUrl | file upload endpoint |
formStateUrl | endpoint yang rebuild schema untuk live field |
validateStepUrl | hanya tersedia untuk wizard; null jika bukan wizard |
recordKey | edit saja |
relations | edit saja |
canCreateAnother | create saja |
| widget props | keduanya |
panel/resources/View
Mengirim page, resource, infolist (null jika resource tidak mendeklarasikan infolist), entries (fallback yang diturunkan dari form ketika dibutuhkan), recordKey, actionEndpoints, relations, serta widget props.
Auth screens
Masing-masing auth screen menerima panel secara langsung karena guest tidak memiliki shared panel prop yang dapat diandalkan untuk auth shell:
| Component | Props |
|---|---|
panel/auth/Login | panel, canResetPassword, canRegister, status |
panel/auth/Register | panel, passwordRules |
panel/auth/ForgotPassword | panel, status |
panel/auth/ResetPassword | panel, email, token, passwordRules |
panel/auth/VerifyEmail | panel, status |
Layouts
Setiap panel page mendeklarasikan layout-nya sendiri. Tidak ada registration tambahan di resources/js/app.ts.
defineOptions({ layout: PanelLayout }); // panel screens
defineOptions({ layout: PanelBlankLayout }); // auth screens milik panel2
| Layout | Peran |
|---|---|
PanelLayout | memilih shell dari panel.sidebar.variant, mendaftarkan error/broadcast listener, dan me-resolve breadcrumbs |
SidebarPanelLayout | shell dengan side rail |
HeaderPanelLayout | shell dengan top navigation |
PanelBlankLayout | tanpa chrome |
PanelAuthLayout | frame yang dirender auth pages sendiri |
Auth pages mendeklarasikan PanelBlankLayout, lalu merender PanelAuthLayout di dalam template. Keduanya dipisahkan karena shell authenticated tidak relevan bagi guest — tidak ada navigation, notifications, maupun user menu — dan karena layout: null tidak bekerja pada resolver host yang umum: page.default.layout = page.default.layout || AppLayout, sedangkan null || AppLayout tetap menghasilkan AppLayout.
Satu kesalahan application yang paling berpengaruh
// resources/js/app.ts
createInertiaApp({
resolve: (name) => {
const page = resolvePageComponent(name, import.meta.glob('./pages/**/*.vue'));
page.default.layout = AppLayout; // salah
page.default.layout ??= AppLayout; // benar
return page;
},
});2
3
4
5
6
7
8
9
10
11
Assignment unconditional menimpa shell panel setelah page sudah meminta layout miliknya sendiri. Akibatnya seluruh panel screen dirender di dalam sidebar application, panel navigation hilang, response tetap HTTP 200, dan tidak ada log error.
panel:install membaca app.ts, app.js, ssr.ts, dan ssr.js. Jika menemukan assignment bermasalah, installer menyebut file dan line number serta tidak menyelesaikan proses secara silent:
resources/js/app.ts line 4 overwrites the layout every panel page declares:
page.default.layout = AppLayout;
Make it fall back instead, so a page that names its own layout keeps it:
page.default.layout ??= AppLayout2
3
4
5
6
7
||=, ??=, dan assignment yang right-hand side-nya sudah menggunakan fallback semuanya dianggap valid.
Page metadata
Setiap panel screen membawa prop page dengan shape yang sama, sehingga layout dapat merender header dan breadcrumbs tanpa setiap page melakukan wiring sendiri.
export interface PageMetadata {
title: string;
heading: string;
subheading: string | null;
breadcrumbs: PanelBreadcrumbItem[];
headerActions: unknown[];
scope: string | null;
subNavigation: PageSubNavigation;
cluster: ClusterNavigation | null;
}2
3
4
5
6
7
8
9
10
Metadata ini divalidasi, bukan sekadar di-cast, karena melintasi boundary PHP/TypeScript. Shape mismatch seharusnya degrade menjadi bare page, bukan melempar exception di layout:
import {
usePanelPage,
normalizePageMetadata,
} from '@/panel/composables/usePanelPage';
const page = usePanelPage(); // ComputedRef<PageMetadata | null>
normalizePageMetadata(someUnknownValue); // PageMetadata | null2
3
4
5
6
7
scope digunakan oleh render hook scoping — misalnya resource:{slug} atau page:{slug}. Nilainya berupa slug, bukan class name. Page metadata tidak pernah mengirim PHP class name ke browser.
Deferred props
widgetData adalah Inertia deferred prop: nilainya tidak ada pada initial response dan tiba melalui follow-up request. Deklarasikan optional di semua tempat:
withDefaults(
defineProps<{ widgetData?: WidgetData | null }>(),
{ widgetData: null },
);2
3
4
WidgetRenderer menampilkan LoadingState skeleton selama lazy widget payload belum tersedia dan baru me-mount real renderer setelah data datang. Dengan begitu custom widget tidak melihat undefined props.
Navigation
Setiap href berasal dari server atau Wayfinder. Panel tidak membangun URL panel secara manual di frontend.
<script setup lang="ts">
import { Link, router } from '@inertiajs/vue3';
import { useNavigation } from '@/panel/composables/useNavigation';
const { items } = useNavigation();
function refresh(): void {
router.reload({ only: ['rows', 'pagination'] });
}
</script>
<template>
<Link v-for="item in items" :key="item.href" :href="item.href">
{{ item.label }}
</Link>
</template>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Table state ditulis ke query string oleh useResource menggunakan preserveState dan preserveScroll. Karena itu mengetik di search box tidak kehilangan focus atau posisi scroll, dan back/forward/refresh/bookmark tetap memiliki semantic yang benar.
Navigation item dapat mendeklarasikan fullPage: true, yang berarti destination memerlukan real browser navigation, bukan Inertia visit. PanelNavigationItem merender plain anchor untuk kasus tersebut. Prefetching tidak berguna karena response berupa document penuh yang tidak dapat digunakan Inertia client.
Menambahkan page milik Anda sendiri
- Buat component di
resources/js/pages/Panels/{Panel}/Pages/{Name}.vue. - Deklarasikan
defineOptions({ layout: PanelLayout }). - Deklarasikan
page: PageMetadatapadadefineProps, ditambah prop yang dikembalikanprops(). - Arahkan PHP class ke component:
protected static string $component = 'Panels/{Panel}/Pages/{Name}'; - Rebuild frontend.
php artisan make:panel-page Reports --panel=Admin --component menjalankan langkah 1 dan 4. Generator tidak menulis declaration layout; tambahkan sendiri.
Lihat Custom Page Components.
Gotchas
$componentbukan registry key. Nama melewati Inertia resolver milik application. Bad name menghasilkan runtime error, bukan fallback. Custom columns, fields, widgets, hooks, dan shell replacements yang menggunakanimport.meta.glob.- Page component tanpa layout adalah failure yang tetap menghasilkan HTTP 200. Tidak ada log. Jika panel screen tiba-tiba memiliki sidebar application, ini penyebab pertama yang perlu diperiksa.
- Page props dapat overwrite framework props.
Page::props()di-spread paling akhir, sehingga key bernamapage,widgets,widgetData, ataufiltersmengganti value framework. headerActionsbertipeunknown[]. Cast di titik pemakaian.unknownmempertahankan type safety lebih baik daripadaany.- Shared
panelprop bernilai null di luar panel. Prop tetap selalu ada pada responseweb, tetapi/,/login, dan page non-panel menerimanull. Consumer harus null-safe. - Tidak ada API kedua. Guard, middleware, routing, session, flash toast, dan build yang sama melayani panel screens dan application screens. API boundary terpisah hanya akan menggandakan authorization tanpa keuntungan nyata.