I have read all the documentation I can find on hook_menu(). I understand the normal menu system fine, but somehow I just can't get it through my thick skull how to implement the local tasks.

Is there a good online tutorial that someone could point me to? Or could someone please explain this in simple terms from beginning to end. The regular menu system is very logical, I'm just not sure what I'm missing here. Every built-in module seems a little to complicated for me to understand.

- Martin

Comments

mdixoncm’s picture

2 simple steps to understanding local tasks.

1. DONT PANIC.
2. Steal the code you need from somewhere else ... like the user module for example.

Local tasks are displayed in the tabs across the top of the page. A good example of an implementation of local tasks is the login screen - by default you see 3 tabs login, register and forgot password.

The code for this is below

    $items[] = array('path' => 'user/login', 'title' => t('log in'),
      'callback' => 'user_login', 'type' => MENU_DEFAULT_LOCAL_TASK);
    $items[] = array('path' => 'user/register', 'title' => t('register'),
      'callback' => 'user_register', 'access' => $user->uid == 0 && variable_get('user_register', 1), 'type' => MENU_LOCAL_TASK);
    $items[] = array('path' => 'user/password', 'title' => t('request new password'),
      'callback' => 'user_pass', 'access' => $user->uid == 0, 'type' => MENU_LOCAL_TASK);

So you can see that everything is exactly like defining a normal menu item, but just by setting the type to be MENU_LOCAL_TASK drupal will display the menu items as tabs.

Not sure if thats such a good explanation - but hopefully the code will paint a thousand words :)

cool cool,

Mike

Like books? Check out booktribes the new (Drupal based) community for book lovers
from Computerminds

Beltanin’s picture

Thanks for the input, it allowed me to make some great strides. A couple of follow up questions if I may...

(BTW, I have read and read and read the documentation, but I'm getting lost in it.)

1. When using the standard view/edit node screen [xxx_form()], is it possible to add another local task? It doesn't really appear to be working for me if it does.

2. If the above is not possible, I figure I can make up my own custom forms, etc. In this case then, how do I determine what action is being taken on the form. How do I know if it's a new record, an edit, or a deletion? This may sound a little naive but I'm also not sure where to put the code to execute the proper action.

Any help would be greatly appreciated.

- Martin

mooffie’s picture

When using the standard view/edit node screen [xxx_form()], is it possible to add another local task? It doesn't really appear to be working for me if it does.

Yes, it's possible. You use the same code mdixoncm provided, but the trick is to find out the correct path. Let's say we want to add a 'bozo' tab besides the 'view' and 'edit' ones. Drupal knows tabs are siblings by examining their paths. So the 'bozo' tab must have a 'node/1234/bozo' path for Drupal to know it's a sibling to 'view' (node/1234) and 'edit' (node/1234/edit).

Here's how to do it:

function mymodule_menu($may_cache) {

  $items = array();

  if ($may_cache) {
    // ...
  } else {
    $items[] = array(
       'path'     => 'node/'. arg(1). '/bozo',
       'title'    => t('Bozo this node'),
       'callback' => 'mymodule_bozo',
       'type'     => MENU_LOCAL_TASK,
    );
  }

  return $items;
}

function mymodule_bozo() {
  return "Your node has been bozoed successfully!";
}

Note the arg(1) thing. It extracts the node ID from the URL. If we're browsing node #234, our bozo 'path' will be 'node/234/bozo'.

I'm also not sure where to put the code to execute the proper action

Web programming is event-based. It's not the old-style programming we learned at high-school, where we were the ones to hand control to each procedure (aka 'function') ourselves. Event-based programming is somewhat different.

And we don't program the application from scratch. We use some framework. Drupal is such a framework. There are others as well.

Don't fret when you don't understand code you see. You're not supposed to understand it as long as you haven't learned the basics of the Drupal framework.

I'm also not sure where to put the code to execute the proper action

let's return to my code above. When I handed the $items array to Drupal, I described to him what piece of code it should hand control to when the 'node/123/bozo' path is accessed. That's event-based programming. It's different from traditional programming.

Start at the beginning. the handbooks contain some tutorials.

Beltanin’s picture

Thank you moofie.

This has provided that last little bit of understanding that I needed. After your additional notes and some more reading I've got exactly what I needed now. Thank you for all your time.

- Martin

cpp’s picture

Hi,
I read your reply I follow the same instructions. Still local tasks are not displayed.
code in mymodule_menu

$items[] = array(
'path' => 'node/add/edit',
'title' => t('Bozo this node'),
'callback' => 'mymodule_bozo',
'type' => MENU_IS_LOCAL_TASK,
);

I check in menu.inc following condition is not returning true.

menu.inc
if (($_menu['items'][$mid]['type'] & MENU_IS_LOCAL_TASK) && _menu_item_is_accessible($mid)) {

Can u please help me

mooffie’s picture

'path' => 'node/add/edit',

What are you trying to achieve here?
'node/add/edit' seems weird to me.

cpp’s picture

I want to give path for different modules like amazon search

Chill35’s picture

Yeah but why would you want to ADD and EDIT content at the same time ? It doesn't make sense.

Caroline
A coder's guide to file download in Drupal
Who am I | Where are we
11 heavens

oknate’s picture

If you are just starting building your first local task, keep in mind that you need at least two local tasks for them to show up!

From: http://api.drupal.org/api/drupal/includes--menu.inc/function/menu_local_...



 // We do not display single tabs.
    return (isset($tabs[$level]) && $tabs[$level]['count'] > 1) ? $tabs[$level]['output'] : '';
  }


justineggert’s picture

I'm not sure if this applies or will help anyone... but I was looking for a way to just add items to the local tasks tabs and only for specific nodes. I added this to my page.tpl.php file before the $tabs variable is called.

if ( !empty($tabs) && $_GET['q'] == 'node/' ) : $tabs = ereg_replace("</ul>", '<li><a href="?q=node/" target="_blank">Tab Title</a></li></ul>', $tabs); endif;

You can use this for as many items as you want and also have it apply to all pages by removing

&& $_GET['q'] == 'node/'

in the if statement

nicholasthompson’s picture

Adding functional stuff like this to the template it generally considered bad practice... A better method would be to create a module and in hook_menu do...


function example_perm() {
  return array('example callback permission');
}

function example_menu($may_cache) {
  $items = array();

  if ($may_cache) {
    //Nothing goes in here
  }
  else {
    //Non-cachable menu items
    if (arg(0) == 'node' && is_numeric(arg(1)) {
      $node = node_load(arg(1));
      if ($node->type == 'blog') {
        $items[] = array(
          'path' => 'node/' . arg(1) . '/example',
          'title' => t('Example Tab'),
          'callback' => 'example_node_callback',
          'access' => user_access('example callback permission'),
        );
      }
    }
  }

  return $items;
}

function example_node_callback() {
  return t('THIS IS AN EXAMPLE!');
}

That example will check the path and, if there is a node on this path (eg node/5) then it will load the node (eg, 5) and then check its type (eg, blog). If it IS a 'blog' then it will add a new tab to the list and it will also handle the callback AND the access permissions (so if your user does not have a role with that permission set then you will NOT see the tab).

nicholasthompson’s picture

Just noticed (a few weeks later) that there needs to be a type => MENU_LOCAL_TASK on the example menu item.

tROCK’s picture

VERY helpful... thanks.

There is one other minor typo... You need one more ")" at the end of your first conditional.

Here is the re-written code with the extra ) and the MENU_LOCAL_TASK edit implemented.


function example_perm() {
  return array('example callback permission');
}

function example_menu($may_cache) {
  $items = array();

  if ($may_cache) {
    //Nothing goes in here
  }
  else {
    //Non-cachable menu items
    if (arg(0) == 'node' && is_numeric(arg(1))) {
      $node = node_load(arg(1));
      if ($node->type == 'blog') {
        $items[] = array(
          'path' => 'node/' . arg(1) . '/example',
          'title' => t('Example Tab'),
          'type' => MENU_LOCAL_TASK,
          'callback' => 'example_node_callback',
          'access' => user_access('example callback permission'),
        );
      }
    }
  }

  return $items;
}

function example_node_callback() {
  return t('THIS IS AN EXAMPLE!');
}


ebeyrent’s picture

I cannot figure this out to save my life. When I tried to use this code, it definitely produced the local task as a tab. However, when I click on the tab, the callback does not get executed. Instead, the node reloads with node/nid/example in the url.

Any thoughts as to why this is happening?

aangel’s picture

My code was calling the parent item too but for a different situation. Perhaps this will help someone.

In my case it was because on the second time through (i.e. after the user had clicked the local link), the test for whether the user was allowed to access the local menu failed. The test depended on a specific form (the form of the parent url) which was different from the child url, so of course it would fail.

With that failed test, the local menu wasn't inserted into the menu table. All Drupal would find is the parent, which it promptly displayed.

Recrafting the test so that it could handle the URL form of both the parent AND the local menu did the trick.

heronog’s picture

All this techniques are very interesting, but checking the hook_menu documentations I think I came up with a way of creating local tasks depending on arbitrary conditions.

In my case, I was trying to add a local task to nodes of certain type, and noticed two very powerful properties of a menu entry, those are %placeholders and the 'access callback' property.

Using a custom 'access callback' function, we can create a menu entry that can be cached and that can be present on arbitrary conditions. Here's what I did:

function yourmodule_menu() {
  
    //notice the %node placeholder
    $items['node/%node/mylocaltask'] = array( 
    'title' => t("Test local task"),
    'description' => t("Make a test for local tasks"),
    // THIS IS THE TRICK
    'access callback' => "yourmodule _check_local_tasks", 
    // PRELOAD $node FOR US
    'access arguments' => array(1),

    // normal callback, just point to the function of your liking
    'page callback' => "carabas_send_test",
    // PRELOAD $node when executing the page callbac
    'page arguments' => array(1),

    // do this a local task
    'type' => MENU_LOCAL_TASK
  );
  
  return $items;
}

function yourmodule_yourlocaltaskcallback($node) {
// notice there is no "node_load" here, the $node is already loaded properly
  return "testing your node with id $node->nid and type $node->type";
}

function yourmodule_check_local_tasks($node) {
// same thing as above with $node
//change the test to your liking.
  return ($node->type == 'yournodetype') && user_access('your task permission'); 
}

Hope this helps!

chromix’s picture

The proper way to conditionally create a menu item in Drupal 6 is to use a loader function. For example:

<?php
function mymodule_menu() {
  $items = array();
  $items['node/%mymodule_node/crop'] = array(
    'title' => 'Crop',
    'description' => t('Do whatever my module does to a node.'),
    'page callback' => 'mymodule_edit',
    'page arguments' => array(1),
    'access callback' => 'mymodule_access_callback',
    'type' => MENU_LOCAL_TASK,
    'weight' => 50,
  );
  return $items;
}
function mymodule_node_load($nid) {
  $node = node_load($nid);
  return ($node->type == 'mymodule_type') ? $node : FALSE;
}
function mymodule_edit($nid) {
  //do whatever your module does...
}
?>

The menu processor looks at whatever appears after the %, adds "_load" and uses that to validate the argument. It's weird, but it works. See http://drupal.org/node/103114 for more info.

jaypan’s picture

But unlike the last posters method, your method will add the local task whether it's the proper node type or not.

Contact me to contract me for D7 -> D10/11 migrations.

chromix’s picture

I have this in a production environment, and the local task (i.e. tab) is only added if the node type is "mymodule_type", as per the example. Naturally I had to adapt the code so that it would be appropriate for an example. Did I miss something?

jaypan’s picture

Well, you didn't show your access callback, which is what will decide whether or not the tab will be rendered.

Contact me to contract me for D7 -> D10/11 migrations.

HJulien’s picture

There is a module called Tab Tamer that gives you access to work with the menu local tasks. You can change the order, hide some, change the titles, etc. Very, very handy!

Also, to add tabs, I followed WorldFalz's advice and it worked beautifully! See it here at http://drupal.org/node/370859.

In my case, I used the 'me aliases' module so I can name my pages (created with views) with just user/me/xxx and they appear in the list in Tab Tamer so I can order them there. I haven't tried it yet with Panel Pages. For the view pages to work, you have to select the Menu option: Menu Tab. You also have to set the argument to UID. The idea is that anything with user/% will appear in the local tasks menu so this gives you a lot more control over most things related to it.

Another thing I figured out has an impact are the Display Settings in the Content Profile Tab of your Content Profile content type. Play around with them to have View and Edit appear and disappear under the Tab Tamer Tabs. (Profile tabs have the names View and Edit in the user/% section of Tab Tamer - View shows the Profile and Edit is to edit the account).

Now if I could just figure out how to have all of my Profile-related tabs be grouped with a parent tab (Profile)and children (Account, View Profile, Edit Profile, Apply for Role), I'm in business! lol

I hope this saves someone time.

Hélène