old wagon with signs in it

Beef up your menu administration in GovCMS SaaS

Thumbnail

Si Hobbs

|

Don't let your GovCMS editors miss out on some bling when they are managing those massive gov.au menus.

Note: this article is mostly targeted at GovCMS SaaS sites that cannot install extra Drupal modules or write custom modules. 

Government sites often make heavy use of the menu system. With GovCMS SaaS there is no option to enhance the editor experience with additional modules. This article describes some menu related tips and tricks to bling up your Drupal editing experience by hacking the menu admin table. While it's a bit clunky to preprocess the menu admin table, it does provide a reasonably nice place to wedge in some enhancements and the worst-case scenario is that the feature might stop working in the future because the menu system has actually been improved in Drupal core 🎉.

Keep in mind...

You need an admin theme

This tutorial assumes you have added a custom admin theme to your site. I recommend creating a sub-theme of the Adminimal theme, but you can add any Drupal administration theme to the /themes directory. Please sub-theme it.

Careful about performance

Any time you are tweaking tables or lists in a preprocess function you should be careful about scale. In this article we are talking about admin-only pages which are generally finite. However, the Drupal menu administration page isn't "paged", so your cute tricks might work fine for 5 links but fall over at 200.

Add operation links

When you add a link to a menu in Drupal, your link is often pointing to a node. Evidence for this is seen in the autocomplete widget, which pulls the title of the node when viewing the edit form.

Node edit link in menu

The basic steps are

  1. preprocess the table that is displayed when editing a menu
  2. inspect each table row, each menu link, and extract a node entity
  3. add an edit link to that node in the contextual links

The initial code

If your administration theme is called dangermouse then this code would go into new dangermouse.theme file. If you already have a .theme file then you'll be updating it with the code below. Remember to replace MYTHEME with the name of your theme.

Note that the full code with all examples is at the bottom of the article.

<?php

use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\node\Entity\Node;

/**
 * Implements hook_preprocess_table__menu_overview().
 */
function MYTHEME_preprocess_table__menu_overview(&$variables) {

  foreach ($variables['rows'] as $rid => $row) {
    foreach ($row['cells'] as $cid => $cell) {
      // Carefully detect a cell in the "Operations" column and a Url object.
      if (isset($cell['content']['#type']) && $cell['content']['#type'] == 'operations' && isset($cell['content']['#links']['edit']['url'])) {

        /** @var Drupal\Core\Url $menu_link_url **/
        $menu_link_url = $cell['content']['#links']['edit']['url'];
        if ($menu_link_url->isRouted() && 'entity.menu_link_content.canonical' == $menu_link_url->getRouteName()) {
          $params = $menu_link_url->getRouteParameters('menu_link_content');
          $menu_link = MenuLinkContent::load($params['menu_link_content']);

          // The URL object referenced by the menu link has a route (eg entity.node.canonical) and the parameters for
          // that route.
          // However, to get the full entity object we have to do a little work. An example in Drupal core
          // is when Drupal adds preview text for a menu link autocomplete - it needs to load the node entity to display
          // the text in the field and it only currently does this for nodes.
          // @see https://git.drupalcode.org/project/drupal/-/blob/9.1.x/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php#L74-80
          // So adapt the following to taste, in this case we're just adding a node edit link.

          /** @var Drupal\Core\Url $url **/
          $url = $menu_link->getUrlObject();
          $route_params = $url->getRouteParameters();
          if ($url->isRouted()) {

            switch ($url->getRouteName()) {
              case 'entity.node.canonical':
                // Load the node and get an edit link.
                $node = Node::load($route_params['node']);
                $edit_url = $node->toUrl('edit-form');
                $variables['rows'][$rid]['cells'][$cid]['content']['#links']['edit_node'] = [
                  'title' => 'Edit node',
                  'url' => $edit_url,
                ];
                break;

              // Add any other internal route related links you might care for.
              case 'entity.SOMETHINGELSE.canonical':
              default:
                // Ignore everything else.
                break;
            }
          }
        }
      }
    }
  }
}

Add extra columns

Since you can access the node (in applicable) in each row you might want to enhance the table further with useful information about the content. 

Picture of menu with extra suggestions

 

The basic steps

  1. preprocess the table that is displayed when editing a menu
  2. add a new th cell in the header
  3. inspect each table row, each menu link, and extract a node entity
  4. add a new td cell, optionally populated with whatever html

The extra code

The code is largely the same as the previous example. The additional lines added to the previous example are:
 

// Set a new header cell.
$variables['header'][] = ['tag' => 'th', 'content' => 'Additional information'];

// For each row define a new default empty cell.
$meta = ['tag' => 'td', 'content' => ['#children' => '']];

// If this is a node, populate it with any information you care for.
$meta['content']['#children'] = $node->get('title')->value . ' (' . $node->bundle() . ')';

// Finally the cell to the end of the row.
$variables['rows'][$rid]['cells'][] = $meta;

The full code

<?php

use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\node\Entity\Node;

/**
 * Implements hook_preprocess_table__menu_overview().
 */
function MYTHEME_preprocess_table__menu_overview(&$variables) {

  $variables['header'][] = ['tag' => 'th', 'content' => 'Additional information'];

  foreach ($variables['rows'] as $rid => $row) {

    // For the extra information column, default to an empty cell.
    $meta = ['tag' => 'td', 'content' => ['#children' => '']];

    foreach ($row['cells'] as $cid => $cell) {

      // Carefully detect a cell in the "Operations" column and a Url object.
      if (isset($cell['content']['#type']) && $cell['content']['#type'] == 'operations' && isset($cell['content']['#links']['edit']['url'])) {

        // Example if the menu link URL object points to an internal object like a node.
        /** @var Drupal\Core\Url $menu_link_url **/
        $menu_link_url = $cell['content']['#links']['edit']['url'];
        if ($menu_link_url->isRouted() && 'entity.menu_link_content.canonical' == $menu_link_url->getRouteName()) {
          $params = $menu_link_url->getRouteParameters('menu_link_content');
          $menu_link = MenuLinkContent::load($params['menu_link_content']);

          // The URL object knows its own route (eg entity.node.canonical) and the parameters for that route.
          // However, to get the full entity object to play with we have to do a little work. An example in Drupal core
          // is when Drupal adds preview text for a menu link autocomplete - it needs to load the node entity to display
          // the text in the field and it only currently does this for nodes.
          // @see https://git.drupalcode.org/project/drupal/-/blob/9.1.x/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php#L74-80
          // So adapt the following to taste, in this case we're just adding a node edit link.

          /** @var Drupal\Core\Url $url **/
          $url = $menu_link->getUrlObject();
          $route_params = $url->getRouteParameters();

          if ($url->isRouted()) {

            switch ($url->getRouteName()) {
              case 'entity.node.canonical':
                // Load the node that this menu link points to.
                $node = Node::load($route_params['node']);

                // 1. Add operations link.
                $edit_url = $node->toUrl('edit-form');
                $variables['rows'][$rid]['cells'][$cid]['content']['#links']['edit_node'] = [
                  'title' => 'Edit node',
                  'url' => $edit_url,
                ];

                // 2. Add meta information.
                $meta['content']['#children'] = $node->get('title')->value . ' (' . $node->bundle() . ')';
                break;

              // Add any other internal route related links you might care for.
              case 'entity.SOMETHINGELSE.canonical':
              default:
                // Ignore everything else.
                break;
            }
          }
        }
      }
    }
    $variables['rows'][$rid]['cells'][] = $meta;
  }
}

    Add new comment

    The content of this field is kept private and will not be shown publicly.

    Plain text

    • No HTML tags allowed.
    • Lines and paragraphs break automatically.
    • Web page addresses and email addresses turn into links automatically.

    Comments

    • Allowed HTML tags: <em> <strong> <cite> <blockquote cite> <ul type> <ol start type> <li> <dl> <dt> <dd> <p>
    • Lines and paragraphs break automatically.
    • Web page addresses and email addresses turn into links automatically.
    • Use [gist:#####] where ##### is your gist number to embed the gist
      You may also include a specific file within a multi-file gist with [gist:####:my_file].

    Spread the word