Sorting in ModelAdmin

The Need:

In many sites I work on there is a need to manage data that will be used on all pages. While some times this is handled by GridField on different page types through a many_many relationship, other times the DataObjects are managed in a ModelAdmin as they'll be used across the site, regardless of page type. An example for this could be utility or footer navigation.

What Are Our Options:

In the case of navigation there are a couple solutions, some which have already been built by the community. Heyday's Menu Manager module does this well already, but for this exercise we'll forgo the module and write some code.

What We'll Need:

When it comes to managing data in the ModelAdmin we're still using a GridField. The main difference is it's not based on a relation like it would be for managing related objects (i.e. Page has_many Slides). Even though this is the case, we can still utilize tools to modify the GridField to achieve enhanced functionality. In this example we'll be utilizing the GridField Extensions module. The module adds a lot of enhancements to a GridField, but for now we'll be using GridFieldOrderableRows.

Get To The Code Already:

Let's get some things setup, starting with the Navigation Item.

NavigationItem.php

<?php

class NavigationItem extends DataObject
{

    private static $singular_name = 'Navigation Item';
    private static $plural_name = 'Navigation Items';

    private static $db = array(
        'Title' => 'Varchar(255)',
        'Active' => 'Boolean'
    );

    private static $has_one = array(
        'Link' => 'SiteTree'
    );

    private static $summary_fields = array(
        'Title',
        'Link.Title',
        'Active.Nice'
    );

    private static $field_labels = array(
        'Title' => 'Title',
        'Link.Title' => 'Link',
        'Active.Nice' => 'Active'
    );

    public function getCMSFields()
    {
        $fields = parent::getCMSFields();

        return $fields;
    }

    public function canCreate($member = null)
    {
        return true;
    }

    public function canEdit($member = null)
    {
        return true;
    }

    public function canDelete($member = null)
    {
        return true;
    }

    public function canView($member = null)
    {
        return true;
    }

}

Now the managing ModelAdmin

NavigationModelAdmin.php

<?php

class NavigationAdmin extends ModelAdmin
{

    private static $managed_models = array(
        'NavigationItem'
    );

    private static $url_segement = 'nav-items';
    private static $menu_title = 'Navigation';

}

That's all well and good. You'll have the model admin that manages the NavigationItem DataObjects, but we want to take it a step further and add the ability to sort those items. We'll first need to update our NavigationItem object to allow the sort to be stored and set the default sort.

    private static $db = array(
        'Title' => 'Varchar(255)',
        'Active' => 'Boolean',
        'Sort' => 'Int'
    );

    private static $has_one = array(
        'Link' => 'SiteTree'
    );

    private static $default_sort = 'Sort';

Now that our NavigationItem is all set we can focus on updating our ModelAdmin.

    public function getEditForm($id = null, $fields = null)
    {
        $form = parent::getEditForm($id, $fields);
        // $gridFieldName is generated from the ModelClass, eg if the Class 'Product'
        // is managed by this ModelAdmin, the GridField for it will also be named 'Product'
        if (class_exists('GridFieldOrderableRows')) {
            $gridFieldName = $this->sanitiseClassName($this->modelClass);
            if ($gridFieldName == 'NavigationItem' && array_key_exists('Sort',
                    DataObject::database_fields('NavigationItem'))
            ) {
                $gridField = $form->Fields()->fieldByName($gridFieldName);
                $gridField->getConfig()->addComponent(new GridFieldOrderableRows());
            }
        }

        return $form;
    }

Let's break down what's going on here. If we look at the ModelAdmin class there is a getEditForm() function. This function essentially creates the GridField for the model class that is currently loaded in the cms. By overriding this function we must make sure we're calling the parent function so we don't lose anything. This is similar to how you call parent::getCMSFIelds() on classes extending Page (and some times DataObject). The result is the CMSForm with the GridField in it.

We first make sure that the class that implements the sort on the GridField exists, so we check if that class exists. While this isn't required, it can reduce errors if for some reason the module didn't install properly. Next, we need to figure out what class we're managing as you can manage multiple models in a single ModelAdmin class. $this->modelClass holds the current class being managed. We sanitize the model class and then check if it matches the ClassName of the object we want to sort. Even though we're only managing a single class in this example I've found it's better to still check the class name as you or another developer may add more managed models to the ModelAdmin at a later time. We also double check that our Sort column exists in on the class we want to sort. While this doesn't ensure the column has been created in the database, it's a good start to ensuring everything is setup properly and will work.

Once we know we're working with the proper class, we get the GridField from the form by name. We then chain ->getConfig()->addComponent(new GridFieldOrderableRows()) to add the GridField component to the existing config. Once that's all done we simply return the form.

Our final code looks like this:

NavigationItem.php

<?php

class NavigationItem extends DataObject
{

    private static $singular_name = 'Navigation Item';
    private static $plural_name = 'Navigation Items';

    private static $db = array(
        'Title' => 'Varchar(255)',
        'Active' => 'Boolean',
        'Sort' => 'Int'
    );

    private static $has_one = array(
        'Link' => 'SiteTree'
    );

    private static $default_sort = 'Sort';

    private static $summary_fields = array(
        'Title',
        'Link.Title',
        'Active.Nice'
    );

    private static $field_labels = array(
        'Title' => 'Title',
        'Link.Title' => 'Link',
        'Active.Nice' => 'Active'
    );

    public function getCMSFields()
    {
        $fields = parent::getCMSFields();

        $fields->removeByName(array(
            'Sort'
        ));

        return $fields;
    }

    protected function onBeforeWrite() {
        if (!$this->Sort) {
            $this->Sort = NavigationItem::get()->max('Sort') + 1;
        }

        parent::onBeforeWrite();
    }

    public function canCreate($member = null)
    {
        return true;
    }

    public function canEdit($member = null)
    {
        return true;
    }

    public function canDelete($member = null)
    {
        return true;
    }

    public function canView($member = null)
    {
        return true;
    }

}

NavigationAdmin.php

<?php

class NavigationAdmin extends ModelAdmin
{

    private static $managed_models = array(
        'NavigationItem'
    );
    private static $url_segment = 'nav-items';
    private static $menu_title = 'Navigation';

    public function getEditForm($id = null, $fields = null)
    {
        $form = parent::getEditForm($id, $fields);
        // $gridFieldName is generated from the ModelClass, eg if the Class 'Product'
        // is managed by this ModelAdmin, the GridField for it will also be named 'Product'
        if (class_exists('GridFieldOrderableRows')) {
            $gridFieldName = $this->sanitiseClassName($this->modelClass);
            if ($gridFieldName == 'NavigationItem' && array_key_exists('Sort',
                    DataObject::database_fields('NavigationItem'))
            ) {
                $gridField = $form->Fields()->fieldByName($gridFieldName);
                $gridField->getConfig()->addComponent(new GridFieldOrderableRows());
            }
        }

        return $form;
    }

}

The Takeaway:

ModelAdmin, while pretty basic out of the box, can be customized to include many types of enhancements. Getting to know the class you are extending helps you understand what it is that needs to be changed for you to get the desired result.

Code for this snippet can be found on github.

Rate this post (1 rating(s))

Post your comment

Comments

  • Gregor Weindl 08/07/2019 11:10am (5 years ago)

    for SS4 it is something like this. I mean the model admin part

    public function getEditForm($id = null, $fields = null)
    {
    $form = parent::getEditForm($id, $fields);
    if (class_exists(GridFieldOrderableRows::class) && $this->getGridFieldName()) {
    $gridField = $form->Fields()->fieldByName($this->getGridFieldName());
    $gridField->getConfig()->addComponent(new GridFieldOrderableRows());
    }
    return $form;
    }

    protected function getGridFieldName(): string
    {
    return $this->sanitiseClassName($this->modelClass);
    }

RSS feed for comments on this page | RSS feed for all comments