Registering Custom Admin List Actions

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)

Registering an Action (DemoGenerator\Module::init) php
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)

Handling the Action (AdminCreateDemoSiteController) php
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."
    ]);
}