Custom actions in ModelAdmin

I need to add a “free” action to a ModelAdmin. That action has just to trigger the execution of some PHP code and notify the results.

In my ModelAdmin I have the following function:

public function getEditForm($id = null, $fields = null)
{
    $form = parent::getEditForm($id, $fields);
    ...
    $field = LiteralField::create(
        'Docblock',
        '<div class="alert alert-info text-center mb-4">' .
          '<p>Some description here.</p>' .
          '<hr>' .
          '<a href="/api/update_items" class="btn btn-primary btn-lg data-pjax="Content">Update items</a>' .
        '</div>'
    );
    $form->Fields()->insertBefore('Gridfield', $field);
    ...
    return $form;
}

and the font-end side of things is fine.

In the back end I’m just doing nothing but… no matter what I try, the browser keeps redrawing everything from the ground up. And in fact the request does not include any PJAX related header.

public function update_items($request)
{
    $response = $this->getResponse();
    $response->addHeader('X-Status', 'Done!');
    return $response;
}

The PJAX documentation is still present in Silverstripe 5 so, although cited as legacy everywhere, I suppose it should still work.

What I would need is to execute some PHP code and show “Done!” at the end, similar to what the CMS does when saving a record.

You haven’t included all of the relevant code here and it’s not clear how /api/update_items is routed… so I’ve given my hand at reproducing what your code might look like. I’ve got this ModelAdmin:

<?php

use SilverStripe\Admin\ModelAdmin;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Security\Group;

class MyModelAdmin extends ModelAdmin
{
    private static $managed_models = [
        Group::class,
    ];

    private static $allowed_actions = [
        'update_items',
    ];

    private static $url_segment = 'mangy-groups';

    public function getEditForm($id = null, $fields = null)
    {
        $form = parent::getEditForm($id, $fields);
        // ...
        $url = $this->Link(Controller::join_links($this->sanitiseClassName($this->getModelClass()), 'update_items'));
        $field = LiteralField::create(
            'Docblock',
            '<div class="alert alert-info text-center mb-4">' .
            '<p>Some description here.</p>' .
            '<hr>' .
            '<a href="'.$url.'" class="btn btn-primary btn-lg data-pjax="Content">Update items</a>' .
            '</div>'
        );
        $form->Fields()->insertBefore('Gridfield', $field);
        // ...
        return $form;
    }

    public function update_items($request)
    {
        $response = $this->getResponse();
        $response->addHeader('X-Status', 'Done!');
        return $response;
    }
}

This works - though it updates the location bar, which means that:

  1. The button only works once
  2. Refreshing the pages actually tries to load the action URL instead of the modeladmin itself

You probably should just add the action as an action. The simplest way being:

<?php

use SilverStripe\Admin\ModelAdmin;
use SilverStripe\Forms\FormAction;
use SilverStripe\Security\Group;

class MyModelAdmin extends ModelAdmin
{
    private static $managed_models = [
        Group::class,
    ];

    private static $allowed_actions = [
        'update_items',
    ];

    private static $url_segment = 'mangy-groups';

    public function getEditForm($id = null, $fields = null)
    {
        $form = parent::getEditForm($id, $fields);
        // ...
        $action = FormAction::create('update_items', 'Update items')->addExtraClass('btn-primary btn-lg');
        $form->Actions()->add($action);
        // ...
        return $form;
    }

    public function update_items($request)
    {
        $response = $this->getResponse();
        $response->addHeader('X-Status', 'Done!');
        return $response;
    }
}

However I acknowledge that doesn’t give you the fancy background and description text, so you can do something like this for the best of both worlds:


<?php

use SilverStripe\Admin\ModelAdmin;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Security\Group;

class MyModelAdmin extends ModelAdmin
{
    private static $managed_models = [
        Group::class,
    ];

    private static $allowed_actions = [
        'update_items',
    ];

    private static $url_segment = 'mangy-groups';

    public function getEditForm($id = null, $fields = null)
    {
        $form = parent::getEditForm($id, $fields);
        // ...
        $action = FormAction::create('update_items', 'Update items')->addExtraClass('btn-primary btn-lg');
        $field = LiteralField::create(
            'Docblock',
            '<div class="btn-toolbar alert alert-info text-center mb-4">' .
            '<p>Some description here.</p>' .
            '<hr>' .
            $action->FieldHolder() .
            '</div>'
        );

        $form->Fields()->insertBefore('Gridfield', $field);
        // ...
        return $form;
    }

    public function update_items($request)
    {
        $response = $this->getResponse();
        $response->addHeader('X-Status', 'Done!');
        return $response;
    }
}

Note that the btn-toolbar css class is necessary for the button to actually do anything - but it also messes up the styling you had a little so you may need some custom css to tidy things up a bit.

Note also that there’s no field called Gridfield currently, so the action is rendered at the bottom of the modeladmin. If you want it to be above the default gridfield, you can use this:

$form->Fields()->insertBefore($this->sanitiseClassName($this->modelTab), $field);

About the pjax stuff

It’s not clear if you actually wanted to use pjax or not - you said “That action has just to trigger the execution of some PHP code and notify the results”, and depending on what you mean by “notify”, pjax may or may not be suitable.

If you just wanted a toast that says what happened, no pjax required! If you want some HTML content to be returned and injected into the DOM, then either pjax or some custom javascript will be necessary.

I’m happy to help give further guidance on how to use pjax for that if that’s what you want, but for now I’m assuming that when you said “notify” you meant you just meant you want some toast message.

1 Like

Many thanks for the detailed info, I just tried and everything works as you describe.

I did not include the routing because I did not expect that to be relevant, while instead it seems to be the core of my issue. In the original example /api/update_items was literally that, and I had an API.php controller with an update_items action.

I was using PJAX in an attempt to avoid redrawing the whole page, so it is not really needed per se. In reality, data-pjax was data-pjax-target and my idea was to create a dummy PJAX fragment (or trying using an unexistent one) to achieve my goal but nothing worked at all: the page was always refreshing.

With your solution I have to call the API code from my ModelAdmin, but I can certainly live with that.

you may need some custom css to tidy things up a bit.

By isolating the bnt-toolbar HTML it seems that can be solved without CSS:

$field = LiteralField::create(
    'Docblock',
    '<div class="alert alert-info text-center mb-4">' .
    '<p>Some description here.</p>' .
    '<hr>' .
    '<div class="btn-toolbar justify-content-center">' . $action->FieldHolder() . '</div>' .
    '</div>'
);
1 Like