TableAction targets a row, a Lunar\Panel\Actions\PageAction targets a page’s header — “Import” above a listing, “Audit log” on a record page.
Registering a page action
RegisterPageAction classes from a Section’s pageActions() hook, keyed by page id — the same route-name-derived ids a slot zone uses:
Section, call Panel::addPageAction($pageId, ActionClass::class) directly.
Listing pages vs. record pages
Lunar\Panel\Http\Middleware\HandlePanelInertiaRequests resolves the current page’s actions automatically on every request, with no controller-side wiring: it derives the page id from the route name, finds the first Eloquent model bound to the current route (if any), and passes it as $context to each action:
customers.index) has no bound model, so its actions resolve with $context === null. A record page (customers.edit) passes the route-bound record — the customer — as $context. The same PageAction class shape covers both cases; only what url() (and visible(), if overridden) does with $context differs.
The PageAction contract
Every content page has a header home for it
APageAction needs no cooperation from the page it targets. Every content page renders its header through the shared <PageHeader> (or <SettingsShell> for settings pages), and both always carry the page-action ellipsis — enforced by tests/panel/Unit/PageScaffoldTest.php for every content page in the panel. Registering a page action is therefore enough on its own; there’s no equivalent of a slot’s zone-name mismatch to get wrong.
Worked examples
ImportPageAction — a listing-page action with a static URL, ignoring $context:
AuditPageAction — a record-page action, building its URL from $context:
lunarphp/panel-addon-example’s src/ExampleSection.php.
See also
- Extending Tables —
TableAction, the row-scoped equivalent of a page action. - Slots — injecting body content rather than a header action.
- Ordering with Position.