This issue was previously discussed in the forums. With Drupal 6's new menu system, there appears to be no way for a module to define a MENU_LOCAL_TASK (a tab) that only applies to certain node types.

For example, to add a new tab for ALL node types, do:

$items['node/%node/new_tab'] = array(
    'title' => 'New Tab',
    'page callback' => 'mycallback',
    'page arguments' => array(1),
    'access callback'   => TRUE,
    'type' => MENU_LOCAL_TASK
)

In D5, we could use hook_menu !$may_cache to conditionally create the tab based on the node type. This is not available in D6.

One possible workaround is to use the 'access callback' key to check for the node type and return false when needed:

$items['node/%node/new_tab'] = array(
    'title' => 'New Tab',
    'page callback' => 'mycallback',
    'page arguments' => array(1),
    'access callback'   => check_type(arg(1)),
    'type' => MENU_LOCAL_TASK
)

function check_type($node)
{
  if($node->type == 'story')
    return TRUE;
  }
  return FALSE;
}

This will work for non-admin users. However when you are admin (user 1), the 'access callback' check is skipped. This results in node pages with irrelevant tabs on them.

Comments

ryan_courtnage’s picture

Status: Active » Closed (fixed)

Turns out the workaround using 'access callback' does work, even for user 1.

Damien Tournoud’s picture

The best way remains to define your own loader. Using access callback for this is simply ugly.

ryan_courtnage’s picture

Can you please elaborate? Do you mean do something like:

$items['node/%my_node_type/new_tab'] = array(
    'title' => 'New Tab',
    'page callback' => 'mycallback',
    'page arguments' => array(1),
    'access callback'   => TRUE,
    'type' => MENU_LOCAL_TASK
)

...

function my_node_type_load($arg) {
  $node = node_load($arg);
  if($node->type == 'my_type')
    return $node;
  return FALSE;
}

?

Dang, i never considered that before. I'll have to try!

Damien Tournoud’s picture

I meant exactly that.

axelc’s picture

If you have 5 tabs definied, does that mean that the node gets loaded 5 times ? Or is there a workaound ?

$items['node/%my_node_type/new_tab']= array(...
$items['node/%my_node_type/new_tab1']= array(...
$items['node/%my_node_type/new_tab2']= array(...
$items['node/%my_node_type/new_tab3']= array(...
$items['node/%my_node_type/new_tab4']= array(...
...

function my_node_type_load($arg) {
$node = node_load($arg);
if($node->type == 'my_type')
return $node;
return FALSE;
}

Apfel007’s picture

Hi, did you solve this problem?

Can you give me the whole code to implement a tab? I can't figure it out.

Cheers

markpayne’s picture

Version: 6.4 » 6.8

I am having real problems just getting the tabs to appear on all nodes. Does the code above need to be placed in a hook_menu or just as php inside my module? can someone post the full code for a module including a sample page callback as this is possible where my error lies. I am just returning a string from my callback for testing purposes.

Thanks.

farald’s picture

Category: bug » support
Priority: Normal » Minor
Status: Closed (fixed) » Active

As Apfel007 mentioned, could someone post a complete working code for us noobs to learn from? :)

radman16’s picture

It goes into hook_menu in a custom module named my_module (or whatever you want to call it). Here is some working code:

/**
* Implementation of hook_menu().
*/
function my_module_menu() {
$items['node/%/new_tab'] = array(
'title' => 'Pay Here',
'page callback' => 'mycallback',
'page arguments' => array(1),
'access callback' => 'custom_loader',
'access arguments' => array(1),
'type' => MENU_LOCAL_TASK,
);
return $items;
}

function custom_loader($param) {
$node = node_load($param , $revision = NULL, $reset = NULL);
if($node->type == 'my_node_type')
return TRUE;
return FALSE;
}
/**
* Menu callback; do whatever you want the tab to do here.
*/
function mycallback($param) {
$node = node_load($param , $revision = NULL, $reset = NULL);
// Do cool stuff here
return ;
}

farald’s picture

Status: Active » Fixed

Beautiful! Thank you!

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

ncameron’s picture

Don't forget to clear your menu caches when testing!

Michali’s picture

That's not the correct example, this is not the best way to do this:


function my_module_menu() {
  $items['node/%my_module_node/action'] = array(
    'title' => 'Action',
    'type' => MENU_LOCAL_TASK,
    'page callback' => 'my_module_page',
    'page arguments' => array(1),
  );
  return $items;
}

function my_module_node_load($node_id) {
  return node_load(array('nid' => $node_id, 'type' => 'my_module_content_type'));
}

function my_module_page($node) {
  // display page based on $node of type my_module_content_type
}

If someone sees an error please notify me so I can change it.

jwaxman’s picture

Well...
any attempt I make to change the argument in $items[] to anything other than '%' results in a 'failure to convert stdObject to a string' error and fails to pass the argument.

Why would this be the better solution even if it worked?

Michali’s picture

Can you show a bit of the code you have?

TrevorBradley’s picture

Thank you Michali! I could never get any of the previous examples that used "arg(1)" in the menu definition to work, as everything seemed to be cached for the user as soon as the caches were cleared. The %myfunction / myfunction_load functions are the key to making this work!

fadgadget’s picture

hi thanks for the code. Once i had changed the module name and the node type it worked as i hoped. Advice for the complete noobs there.

I was wondering how i would get a View in there? Also would there be a way to add the node author's name in the title?

Thanks very much.

timdiels’s picture

Warning to others: Be careful not to accidentally implement hook_node_load with your loader.

protitude’s picture

So after poking around, this is what I came up with. It works fine for me on Drupal 7. This is also pulling in a view to export out a CSV. I threw in another access check along with the node type check.

function mymodule_menu() {
  $items = array();
  $nid = arg(1);
  $items['node/%node/export'] = array(
    'title' => t('Export'),
    'page callback' => 'views_page',
    'page arguments' => array('responses_export', 'views_data_export_1', $nid),
    'access callback' => 'mymodule_access_check',
    'type' => MENU_LOCAL_TASK,
  );
  return $items;
}

function mymodule_access_check() {
  $node = node_load(arg(1));
  if (($node->type == 'book') && user_access('access pdf comments')) {
    return TRUE;
  }
    return FALSE;
}
MacSim’s picture

I would rather do something like this :

function mymodule_menu() {
  $items = array();
  $items['node/%node/export'] = array(
    'title' => t('Export'),
    'page callback' => 'views_page',
    'page arguments' => array('responses_export', 'views_data_export_1', arg(1)),
    'access callback' => 'mymodule_access_check',
    'access arguments' => array(1),
    'type' => MENU_LOCAL_TASK,
  );
  return $items;
}

function mymodule_access_check($node) {
  if (($node->type == 'book') && user_access('access pdf comments')) {
    return TRUE;
  }
  return FALSE;
}
vertikal.dk’s picture

Issue summary: View changes

In the example in #21, the argument passed to the access callback is not the node but the nid (from the URL), and the function needs to be something like this:

function mymodule_access_check($nid) {
  $node = node_load($nid);
  return ($node->type == 'book') && user_access('access pdf comments');
}
MacSim’s picture

Vertikal.dk, what you do is wrong !
Sorry, I should have linked my suggestion.
see Auto-Loader Wildcards in hook_menu() https://api.drupal.org/api/drupal/modules%21system%21system.api.php/func... (this is for drupal 7.x but it was already true in drupal 6.x)

Registered paths may also contain special "auto-loader" wildcard components in the form of '%mymodule_abc', where the '%' part means that this path component is a wildcard, and the 'mymodule_abc' part defines the prefix for a load function, which here would be named mymodule_abc_load(). When a matching path is requested, your load function will receive as its first argument the path component in the position of the wildcard; load functions may also be passed additional arguments (see "load arguments" in the return value section below). For example, your module could register path 'my-module/%mymodule_abc/edit':

  $items['my-module/%mymodule_abc/edit'] = array(
    'page callback' => 'mymodule_abc_edit',
    'page arguments' => array(1),
  );

When path 'my-module/123/edit' is requested, your load function mymodule_abc_load() will be invoked with the argument '123', and should load and return an "abc" object with internal id 123:

  function mymodule_abc_load($abc_id) {
    return db_query("SELECT * FROM {mymodule_abc} WHERE abc_id = :abc_id", array(':abc_id' => $abc_id))->fetchObject();
  }

This 'abc' object will then be passed into the callback functions defined for the menu item, such as the page callback function mymodule_abc_edit() to replace the integer 1 in the argument array. Note that a load function should return FALSE when it is unable to provide a loadable object. For example, the node_load() function for the 'node/%node/edit' menu item will return FALSE for the path 'node/999/edit' if a node with a node ID of 999 does not exist. The menu routing system will return a 404 error in this case.

#21 is the best way to do it.
Auto-loader wilcards are made to avoid what you're doing.