4

Say I have the path widget/123 to view a widget, and have another path to edit some properties of this widget at admin/acme/widget/123/edit. In hook_menu it looks like I can still have the admin path appear as a local task (tab) by setting the tab_parent and tab_root properties in hook_menu(), but I can't seem to get it to work. I know how to setup widget/%/edit as a local task, but in this case I'm only interested in setting a different "root" path as a local task.

Here's my code:

<?php

function acme_menu() {

  // View a widget.
  $items['widget/%'] = array(
    'type' => MENU_CALLBACK,
    'page callback' => 'acme_widget_view',
    'page arguments' => array(1),
    'access callback' => 'acme_widget_access',
    'access arguments' => array(1, 'view'),
    'file' => 'includes/widget.view.inc',
  );

  // Edit a widget.
  $items['admin/acme/widget/%/edit'] = array(
    'type' => MENU_LOCAL_TASK,
    'tab_parent' => 'widget/%',
    'tab_root' => 'widget/%',
    'title' => 'Edit Widget',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('acme_widget_edit', 3),
    'access callback' => 'acme_widget_access',
    'access arguments' => array(1, 'edit'),
    'file' => 'includes/widget.edit.inc',
  );

  return $items;
}

?>

Am I misunderstanding tab_parent/root, or can this be done?

Update: I was missing a MENU_DEFAULT_LOCAK_TASK. Adding

$items['widget/%/view'] = array(
    'type' => MENU_DEFAULT_LOCAL_TASK, 
    'title' => 'View'
);

allows the tabs to appear. However, they don't share the same argument position and so the argument (the widget ID) isn't passed between the two. The 'Edit' tab, for example, links to admin/acme/widget//edit. Is there a way to have different routes like this and pass the argument position around?

  • 2
    That looks right - but in order to invoke tabs, you need more than one in the parent path. Usually you'd have something like $items['widget/%/view'] as well, which is a MENU_DEFAULT_LOCAL_TASK, and becomes a sibling of the other MENU_LOCAL_TASKs under the same parent. At the moment I think the fact you've only got a single local task under the parent means that none will show – Clive Oct 9 '13 at 13:46
  • @Clive I wanted to post something similar as an answer, but you are first... only why as a mere comment? – Mołot Oct 9 '13 at 13:50
  • @Mołot It's just a theory, I haven't had time to check if it's true (sounds logical but this is Drupal, you never know!) Do feel free to put the answer in – Clive Oct 9 '13 at 13:52
  • @Clive now when you said that... will need to test :P I'll try to test it tomorrow, no way I'll be able to do this today. – Mołot Oct 9 '13 at 13:59
  • That's it, Clive! Adding this item solved it: $items['widget/%/view'] = array('type' => MENU_DEFAULT_LOCAL_TASK, 'title' => 'View');. If you post your comment as an answer I'll accept it. – Charlie Schliesser Oct 9 '13 at 15:01
3
+50

If I'm not mistaken the tab_root and tab_parent properties need to point to a path prefix of the router item in question - at least if there are wildcards on the path. The code responsible for this restriction is somewhat convoluted, but what you find when stepping through it is the following:

When generating the tab-links, the _menu_translate function is responsible for replacing the %-wildcards with actual values (node-id, etc). However the replacement values are not computed from the actual object-ids but are derived from the current URL. For example when accessing widget/5, the object-id is on position 1 and therefore when generating tab-links, the value 5 is only injected into paths having the %-wildcard at position 1.

This also leads to another consequence. When navigating to admin/acme/widget/5/edit, you will find that there are no tabs on this page - even if widget number 5 actually exists. This is because when generating the URLs for the other tabs, they will start with widget/acme instead of widget/5 because the value acme is at position 1 in the current path.

If you like to provide tabs on your widget page as well as have it in the backend, then simply duplicate the router item. E.g:

function acme_menu() {

  // View a widget.
  $items['widget/%'] = array(
    'type' => MENU_CALLBACK,
    'page callback' => 'acme_widget_view',
    'page arguments' => array(1),
    'access callback' => 'acme_widget_access',
    'access arguments' => array(1, 'view'),
    'file' => 'includes/widget.view.inc',
  );

  $items['widget/%/view'] = array(
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'title' => 'View'
  );

  // Edit a widget - frontend
  $items['widget/%/edit'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'Edit Widget',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('acme_widget_edit', 1),
    'access callback' => 'acme_widget_access',
    'access arguments' => array(1, 'edit'),
    'file' => 'includes/widget.edit.inc',
  );

  // Edit a widget - backend
  $items['admin/acme/widget/%/edit'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'Edit Widget',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('acme_widget_edit', 3),
    'access callback' => 'acme_widget_access',
    'access arguments' => array(3, 'edit'),
    'file' => 'includes/widget.edit.inc',
  );

  return $items;
}

If you like to move around stuff from a third-party module, you still can implement additional router-items on behalf of it in a custom module - or even move them using hook_menu_alter.

  • Thanks znerol. I ended up doing something similar to this, i.e. duplicating the widget/% item, and leaving all other items at the admin path. I think I'll use a user_access/drupal_goto to send proper users to admin/acme/widget/% if they visit widget/%. – Charlie Schliesser Oct 13 '13 at 15:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.