Introduction
Beyond the standard "New" button, a module can contribute its own action buttons to a specific model's admin listing page — for example, a "Create Demo Site" button on the Sites listing. This is handled by the ManagesModelListActions trait on App, via App::registerModelListAction().
Registering an Action (DemoGenerator\Module::init)
App::registerModelListAction('sites', [
'label' => 'Create Demo Site',
'url' => '/api/v1/admin/demo/create',
'method' => 'post',
'confirm' => 'Create a new kitchensink demo site? It will be seeded with sample content and expire automatically in 24 hours.',
'module_dependency' => $this->getId(),
'precedence' => 10,
]);
Action Config Fields & Visibility Gating
method => 'get' renders a plain link; method => 'post' renders a confirm+fetch button that shows the confirm prompt before firing. module_dependency hides the button unless that module is enabled for the active site, and super_admin_only hides it from non-super_admin roles. Actions are sorted by ascending precedence (default 100) before rendering.
Deliberately, isModelListActionVisible() does not reuse the admin sidebar's super_admin bypass on module_dependency — a list action gated to a module should only ever appear when that module is genuinely active for the current site. Reusing the sidebar's bypass would make the gate a no-op on any model whose listing page is itself super_admin-only, since only super admins could ever reach it.
Handling the Action (AdminCreateDemoSiteController)
public function handle($matches): void
{
App::applyAuthMiddleware();
App::applyRoleMiddleware('super_admin');
App::applyCsrfMiddleware();
$demo = (new DemoSiteFactory())->createDemoSite($syntheticEmail, 'kitchensink');
Logger::log($userId, 'admin_demo_site_created', 'sites', null, ['domain' => $demo['domain']]);
echo json_encode([
'success' => true,
'message' => "Demo site created: http://{$demo['domain']} (login: {$syntheticEmail} / password: {$demo['password']}). It expires automatically in 24 hours."
]);
}