I will write a summary later, for now: I am moving the tabs to links instead of a router. The patch is work in progress. You are warned. It is very likely that just applying this patch will cause your computer hit by a meteorite or something.

Comments

robertdouglass’s picture

Getting rid of the default menu item for local tasks would be a step forward in developer UX.

pwolanin’s picture

Maybe - I have doubts about this approach, however.

chx’s picture

Me too. Let's throw out tasks from both menu tables and introduce a whole new structure and table. So we have hook_menu_tabs

$items['node/%']  = array(
  'title' => t('View'),
  'equals' => 'node/%/view',
  'access callback' => _can_add_new_separate_callback just for the tab
 usual title stuff
);
$items['node/%/edit']  = array(
  'title' => t('Edit'),
  'appears' => 'node/%', // defaults to the path key but this appears on more than node/%/edit
);
$item['node/1234/edit'] = array(
  'title' => t('Special edit page'),
);

So we have the ancestors of the current page, we lick up possible tabs based on 'appears', replace wildcards, throw out general (node/%/edit) in favor of specific (node/1234/edit) if the two would point to the same path (node/1234/edit). Parenting is not too hard, on save time either you define by default or we find a parent based on path. Sprinkle with alters and serve.

Edit: titles, of course, should not have a t(), sorry!

chx’s picture

Note: equals will be used so that you can have node/%/view/whatever on the second level.

chx’s picture

Title: Big task cleanup and the death of type » Big task cleanup: remove type and tasks from hook_menu
moshe weitzman’s picture

Title: Big task cleanup: remove type and tasks from hook_menu » Big task cleanup and the death of type

This would help us with the nasty regression that we are working around in #362031: Ideas for dynamic tabs for OG Panels (can't dynamically add/remove/retitle tasks).

chx’s picture

Title: Big task cleanup and the death of type » Big task cleanup: remove type and tasks from hook_menu

Crosspost.

chx’s picture

Peter and I have discussed this in length and arrived to the conclusion at the same second that we want a hook that gathers tabs run time much like D5 and below -- but now the router has all the info, the hook_menu_tab is very similar to hook_link, a simple entry would be $tabs['node/123'] = array('title' => t(View'), 'default' => TRUE); and if you want to be on a lower level then $tabs['user/123/edit/foo'] = array('title' => t('foo'), 'parent' => 'user/123/edit'); so if you want a 2nd or 3rd etc level tab then you explicitly state your parent. No magic.
Most item will be 1st level so they do not need to bother. Tabs become easy, there is no double-definition of default items (where the child item uses the path of the parent...).

pwolanin’s picture

Possibly we shoudl do like D6 hook_help() and pass both the router path and the translated path into this hook to allow for easy conditionals.

pwolanin’s picture

Also, the returned data should be a full set of params to l()
e.g. so I can set query params, attributes, etc if needed. So, we need text, path, and options elements.

chx’s picture

Before you ask "are you two insane?" I would like to point out that unlike in earlier Drupal when page routing needed to collect every possible path with its double-barelled hook_menu and handle huge data structures as a sad result, now we have the menu_router table and the hook_menu_tab only contains the tabs for the current page and those are few.

catch’s picture

Tabs as menu links makes a load of sense.

pwolanin’s picture

@catch, no I don't think they make much sense as links - a lot of practical roadblocks.

chx’s picture

StatusFileSize
new72.59 KB

This is a work in progress but the node module has a test implementation and the new menu_local_tasks contain the code that is quite close to final, I feel like the variable names are a bit clumsy but otherwise I fell it's pretty OK for the max 20 or so tabs a page can possibly have. Feedback is welcome.

Oh -- exposing the 'hidden' key helped keeping the patch simpler but I feel its not the best name.

Edit: the node implementation probably should just use menu_get_objects... i will think on it.

chx’s picture

StatusFileSize
new73.13 KB

Current state and aidan promised to help with the task conversion.

bjaspan’s picture

subscribe

aidanlis’s picture

I'm keen to do some of this grunt work, but I feel like I'm pissing in the wind at the moment. If someone can whip up a before and after example, I'm sure I can nut the rest out. Otherwise, I don't understand what we're trying to accomplish here.

This is all I've managed to do for the users module, which is evidently wrong.

[removed lots of pasted code. it was wrong anyways.]

E.g. I've moved all of the MENU_DEFAULT_LOCAL_TASKs into the local_task hook, and changed the other MENU_LOCAL_TASKs to hidden. It seems no matter what I do, I'm getting all sorts of notices and errors (Notice: Undefined index: original_path in menu_local_tasks() (line 1384), etc).

I've had to do the "$user = new stdClass;" thing because menu_get_object('user', 1) or menu_get_object('user_category') or even just menu_get_object() return nothing.

Sorry to take up so much of your time chx!

aidanlis’s picture

StatusFileSize
new3.66 KB
new19.53 KB
new4.04 KB

I'm all over it, I understand what's going on now. I've attached unified diff's of the work so far (including fixing up some bugs in menu.inc).

This is what they are looking like, any other changes before I get too far into it?

chx’s picture

I mentioned a few times that user_access('user_view_access')does not exist and user_view_access($account) is what you want. Also I think you missed the secondary level (category) user edit tabs. Aside from these, great work, please keep em coming. There is no need to post walls of texts the patch is quite enough, if you do not midn I will actually edit them out to make the issue more readable.

pwolanin’s picture

Having to use strpos for every hook looks like a bad API to me.

Perhaps we can pass in an array of sub-paths?

essentially:

$path[0] == arg(0)
$path[1] == arg(0) .'/'. arg(1)
...

Similar (as suggested before) to D6+ hook_help - we need both the router path and the translated path, most likely.

Then we could do more simply something like:

if ($path[2] == 'admin/content/aggregator')
...

or, e.g. or a specific node:

if ($translated_path[1] == 'node/99')
...

chx’s picture

I definitely did not want to use strpos, I used $link_map I do not know how I missed that. Check $link_map[0] = 'user' etc.

aidanlis’s picture

StatusFileSize
new4.79 KB
new4.25 KB
new19.56 KB

Cleaned up based on comments and fixed another bug in menu.inc

aidanlis’s picture

StatusFileSize
new2.59 KB

Modules remaining:
modules/book/book.module
modules/comment/comment.module
modules/contact/contact.module
modules/forum/forum.module
modules/locale/locale.module
modules/menu/menu.module
modules/path/path.module
modules/system/system.module [50% done]
modules/taxonomy/taxonomy.module
modules/tracker/tracker.module

aidanlis’s picture

StatusFileSize
new19.74 KB
new9.64 KB

- Ensure tabs are not shown if there's only 1 tab for that level
- Convert system.module

chx’s picture

aidanlis, thanks. You can post one patch , no need to post per file. Some remarks. I see constructs like

  if (count($item['link_map']) == 1 && $item['link_map'][0] === 'admin') {
    if (user_access('access administration pages')) {

you can definitely merge these two lines into one if

  if (count($item['link_map']) == 1 && $item['link_map'][0] === 'admin' && user_access('access administration pages')) {

This occurs in many, many places. Also, there is a hunk in system_block_view , please remove that , it's my fault, it's from an earlier version.

Very nice work, keep it coming!

sun’s picture

Holy. Just had a quick peek at one of the patches (system.module) - do we really want to duplicate all menu router data? And did we talk back to merlinofchaos (who heavily struggled with local tasks in Panels/Delegator/Views) and Moshe (who mentioned difficulties in #6) whether this approach solves their needs?

I wasn't involved in any of those D6-local-task issues, but I'd imagine that those issues could also be solved by introducing this hook, but only passing what the menu system thinks the local tasks should be - leaving complete freedom of (dynamic or not) customization to modules, if required... ?

chx’s picture

duplicate all? We can discuss centralizing access -- might be a good idea, after all -- would be a little slower than this but not slower than current. After that, you only have a path and a title and possibly a weight and a parent per tab. Hardly repeating all.

chx’s picture

Yes. We should remove access control from the tabs (sorry aidan) and run menu_get_item on them in menu.inc:

    drupal_alter('menu_local_task', $tasks);
    foreach ($tasks as $path => $task) {
       // Allow skipping the menu access
       if (empty($task['access'])) {
         $item = menu_get_item($path);
         if (empty($item['access'])) {
           unset($tasks[$path]);
         }
       }
    }
chx’s picture

After this, the only repeated thing is the path -- the title is not, previously we could not specify a separate title for the page and the tab but now that we can I expect they wont be the same too often. (For eg, the "View" tab corresponds to a page with the title $user->name or $node->title etc)

pwolanin’s picture

@chx:

count($item['link_map']) == 1 && $item['link_map'][0] === 'admin'

I don't really like this - see above suggesiton re: menu substrings vs alwys needing to test against each arg.

aidanlis’s picture

StatusFileSize
new92.48 KB

I've made the suggested alterations,

Modules remaining:
modules/locale/locale.module
modules/menu/menu.module
modules/path/path.module
modules/taxonomy/taxonomy.module
modules/tracker/tracker.module

chx’s picture

aidanlis, please wait before continue. I am working on this a bit.

aidanlis’s picture

StatusFileSize
new98.62 KB

I'm all done! I couldn't wait, sorry chx. I figure the API won't change too much so any changes we make will be minor.

I've also changed the key sorting, asort() won't do what we need. ksort() however seems to work as expected. See http://drupalbin.com/10112 for an example.

merlinofchaos’s picture

Ok, I'm largely in agreement with Peter. I really dislike the count() stuff. However, what about this:

This:

  // admin/content/aggregator
  if (count($item['link_map']) >= 3 && $item['link_map'][0] === 'admin' && $item['link_map'][1] === 'content' && $item['link_map'][2] === 'aggregator') {
    // ...
  }

Becomes:

  // admin/content/aggregator
  if (array_slice($item['link_map'], 0, 3) == array('admin', 'content', 'aggregator')) {
    // ...
  }

I'm not sure 1) how fast array_slice() is, or 2) if it will notice if the array doesn't have enough elements.

Second, would the use of a callback attached to the menu and an alter function be more efficient? With this setup, on every page you are still testing every possible path that can contain tabs. However, if we included a local task callback and a corresponding alter we could gain a lot of efficiency:

  // totally back of the napkin code:
  if (function_exists($item['local tasks callback'])) {
    $local_tasks = $item['local tasks callback']($item);
    drupal_alter($item['local tasks callback'], $local_tasks, $item);
  }

My immediate concern is that this has the disadvantage of making it difficult to add local tasks to an arbitrary path, because you have to somehow identify the callback. That could use some suggestions.

merlinofchaos’s picture

I think what this leads to is that maybe what I'd really like to see is a formalization of the creation of a tabset.

For example, all tabs that you might see at node/% would be a tabset. We would somehow need to name that. Which is part of how the callback gets named, but naming stuff after functions is ugly.

In IRC chx suggested a callbacks array, but:

node/% and node/%/edit and node/%/foo would all need to reference this tabset.

sun’s picture

I still don't understand why squeezing a simple drupal_alter() to alter tabs/local tasks somewhere into the right location wouldn't solve the entire issue. You get the defaults, i.e. what the menu system _thinks_ should be output, but modules are able to alter that based on the current page or context...?

chx’s picture

  1. tab titles and page titles are not the same. Tab title is typically a single action word while the page title is more expressive.
  2. there were several requests where the access control was different too
  3. specifying the default local task is a DX nightmare.
  4. sometimes the local tasks are bound to the actual path and not the router path. biggest example is og_panels.
sun’s picture

  1. Allowing for a separate menu item and page title is a separate issue. All items below admin/settings could use that, since the menu should display "Region" only, but the page title should be "Regional settings".
  2. Can be customized via drupal_alter(), no?
  3. Simply use the first accessible (based on the user's permissions) after executing that _alter hook - based on weight?
  4. Not sure what that means. But most probably also do-able via drupal_alter().
chx’s picture

sun, the fundamental problem with defaults is that if you are on user/123/edit then the 'account' secondary tab also should point to this page. Because the menu system was always keyed by path, we got around this by defining a menu_default_local_task at an path that is never used and the tab instead points to its parent using this menu_default_local_task defined title. That's what i call a nightmare. This issue solves it simply, you can have as many tabs pointing to the same as you want. Or that was the goal originally... I guess I need to change the path around...

chx’s picture

The problem with having a tabset is that it's not too trivial figuring out what is a tabset. Let's say, user pages. You have the 'view' and the 'edit' tabs, simple. Then you add more categories and suddenly there are other tabs below edit which are not visible when on 'view' but are visible when on 'edit' and yet 'view' is still visible when on 'edit' so it really gets messy. Is 'view' and 'edit' as visible on 'view' is a separate tabset and 'view' , 'edit', 'account', 'category' another tabset? Or 'view' , 'edit', 'account', 'category' is one tabset but we do not display account and category when on 'view'? As said, this is messy.

I still think simply defining what tabs are visible on a page is the simplest. Peter's idea of array('user', 'user/%', 'user/%/foo', '', '', '', '', '', '', '', '') and then -- my idea -- another array('user', 'user/123', 'user/123/foo', '', '', '', '', '', '', '', '') makes it simple and very quick to check for a tab if ($item['partial_router_paths'][1] == 'user/%') { .... }. That makes just a few ifs per module so the overhead is very low after all. It simply does not worth trying to store this too-dynamic mess in a database.

chx’s picture

Continuing this, in a followup maybe we can add a somewhat block-like caching issue with cache => MENU_TASK_CACHE_NONE MENU_TASK_CACHE_PER_ROUTER_PATH MENU_TASK_CACHE_PER_PATH and also MENU_CACHE_ROUTER_PER_UID MENU_CACHE_ROUTER_PER_RID -- might be worth it or might not. I am less sure and I think it's a followup.

I am fairly confident, however, that collecting tabs on build time is a mistake.

chx’s picture

Continuing this, in a followup maybe we can add a somewhat block-like caching issue with cache => MENU_TASK_CACHE_NONE MENU_TASK_CACHE_PER_ROUTER_PATH MENU_TASK_CACHE_PER_PATH and also MENU_CACHE_ROUTER_PER_UID MENU_CACHE_ROUTER_PER_RID -- might be worth it or might not. I am less sure and I think it's a followup.

I am fairly confident, however, that collecting tabs on build time is a mistake.

merlinofchaos’s picture

I am also fairly confident that these "Just a few ifs" are bad DX. They are worse than a giant switch. They are something that should be reserved only for the really dynamic cases.

I spent a little time thinking about this and I came up with this, because doing subtabs is a little bit tricky, but this allows it.

In hook_menu, the path 'node/%node' has 'tabset' => 'node/%node' as does 'node/%node/edit'.

  function node_menu_tabset() {
    $tabsets = array();
    $tabsets['node/%node'] = array(
      'node/%node' => array(
        'title' => 'View',
        'weight' => -10
      ),
      'node/%node/edit' => array(
        'title' => 'Edit',
        'weight' => -9
      ),
      'node/%node/revisions' => array(
        'title' => 'Edit',
        'weight' => -9
      ),
    );
    return $tabsets;
  }

Now, along comes project.module which wants to add some subtabs under edit. No problem:

  function project_menu_tabset_alter($tabsets) {
    $tabsets['node/%node']['node/%node/edit']['subtabs'] = 'node/%/edit';
    $tabsets['node/%node/edit']['node/%node/edit/issues'] = array(
      'title' => 'Issues',
    );
    $tabsets['node/%node/edit']['node/%node/edit/releases'] = array(
      'title' => 'Releases',
    );
  }

Dynamic tabs should work just fine with a third hook, which allows us to do the realtime stuff:

  function views_menu_tabset_realtime(&$tabset, $name, $item) {
    // Views looks at $item to determine that this page is a view:
    // ...
    // And if it *is* a view, it does this.
    $tabset["admin/build/views/edit/$viewname"] = array(
      'title' => 'Edit view',
      'weight' => 0,
    ),
    $tabset["admin/build/views/export/$viewname"] = array(
      'title' => 'Export view',
      'weight' => 0,
    ),
  }

Now, if we go with chx's function, Views is going to have to load every view on every page load to determine if it should add tabs. This is NOT 'just a few ifs' in that case. This could be loading 100 views, sifting through which ones have tabs and then adding them all. Every. Page. Load. Because there is simply now way to tell how many views have set up tabs on any given path without loading every view that has tabs and checking.

og_panels gets its problem solved easily:

  function og_panels_tabsets_realtime(&$tabset, $name, $item) {
    if ($name == 'node/%') {
      $node = menu_get_object('node');
      if (og_is_group()) {
       og_panels_add_tabs($node, $tabset);
      }
    }
  }

Now, og is at least not so much in a position to have to load every node to determine what its tabs are, but at the same time, it does need to be able to put them in absolutely dynamicly to make sure they work. And it won't use node/%node/foo, it will use node/15/foo for any given tab. That's an interesting problem all its own for other reasons.

chx’s picture

A few problems. The tabset indexes as we discussed on IRC are not router paths, they are just convinience. This will lead to confusion. They cant be the default tab's router path because then changing the default becomes a first class nightmare.

  function project_menu_tabset() {
    $tabsets['node_edit_tabset_we_need_to_name_better'] = array
      'node/%/edit/issues' => array(
        'title' => 'Issues',
      );
      'node/%/edit']['node/%/edit/releases' => array(
        'title' => 'Releases',
      ),
    );    
  }
  function project_menu_tabset_alter(&$tabsets) {
    $tabsets['node_tabset_we_need_to_name_better']['node/%/edit']['subtabs'] = 'node/%/edit';
  }

I like this a lot better. Note how I am using "node/%" instead of "node/%node" when using paths. These are how router paths are stored. Edit: also note that I define tabs and just do the minimal necessary in alter.

Storing and rendering this will be quite unfun... but sure.

pwolanin’s picture

If 90% of modules can populate the tabs at runtime and this give the best DX for the most people, then I think it might make sense for modules needing to define a ton of tabs figure out some caching or storage for them.

While decoupling from the router item totally would be ideal, I'm wondering if there will be problems in terms of deciding who "owns" the default tab.

merlinofchaos’s picture

A few problems. The tabset indexes as we discussed on IRC are not router paths, they are just convinience. This will lead to confusion.

If you name the tabset anything other than node/% then how will people figure out this magic name? There will be absolutely no intuitive way to understand that the tabset that goes on node/% will be named whatever you call it. What's wrong with naming it after the path? Using a standard like that, the naming is intuitive and obvious. *Not* naming it after the path will lead to confusion.

If 90% of modules can populate the tabs at runtime and this give the best DX for the most people, then I think it might make sense for modules needing to define a ton of tabs figure out some caching or storage for them.

Please define 'best DX'. What I've outlined looks like pretty good DX for me. The single hook that has a massive ugly series of ifs is not good DX.

I am suggesting that we cache basically normal tabs that aren't very dynamic just like we do menu items. Stick 'em in a the databse, possibly their own table, possibly share the menu items table. That much is fuzzy to me as I don't fully understand the table, and what table it's stored in is kind of an implementation detail that doesn't strike me as that important. As long as it exists. Now, if people are saying that storing this stuff is hard, I haven't actually seen people *say* that. Maybe there's this unspoken agreement that it is, but I don't see why it's hard to store this stuff the same way we do menu links.

chx’s picture

So to close an earlier question, The "interesting problem for all its own reasons" is irrelevant to this issue.

Tabsets might work and we might just get away with saying 'if your item is not like that, do it during runtime'. If the tabset key is 'node/%' and this implies that the parent is, indeed, node/% for everything in there then we even have a reason for putting that in tabset key. If we follow that convention then in the above examples we do not need function project_menu_tabset_alter which is great. We still want an alter hook to alter existing hooks just you do not need that too often.

chx’s picture

*however* we still have a problem here -- what if you have a second level tabset under node/%?

merlinofchaos’s picture

That was what the 'project' example was for. Project has several subtabs under 'edit'. So there's a specific 'subtab' which points to another tabset. It assumes that particular tab is the default tab for that tabset, and possibly needs some way to alter that assumption if there are actually use cases to do so (I can't think of any offhand).

So when visiting node/%node/edit it sees that hey, there's a subtab set here, and renders that as the level 1 tabs. (In theory we could convince the system to support tabs beyond 2 levels which I've occasionally wished for but am not sure are a good idea in reality).

chx’s picture

yeah but how will you key that? the problem is that the tabs below node/%/edit can claim node/%/edit as their parent but as node/% is the default tab, this won't fly like that. If node/%/view/foo is a secondary tab it needs to express somehow and different that the tab node/% is its parent while for node/%/edit the node/% router item is its parent. This is why I had an explicit parent key. Yes we can say under node/% tab item "subtabs" and it can recurse for all I care but still, how do you key that set?

chx’s picture

So today we came up with this

$items['node/%'][0] = array( /* view and edit tabs */);
$items['node/%/edit'][0] = array( /* subtabs of node/%/edit */);
// Only subtabs of default tabs need more than level:
$items['node/%'][1] = array( /* subtabs of 'view' */ );
merlinofchaos’s picture

Hmm. But node/%/edit is a tab on node/% and has its own subtabs. Does this really work? I'm a little confused. It also means that you need a special syntax that differs a little based upon the default tab vs non default. What about:

What about:

  $tabset['node/%'] = array(
    'node/%' => array(
      'title' => 'View',
      'weight' => -10,
      'subtabs' => array(
        'node/%' => array(
          'title' => 'Foo',
        ),
        'node/%/view/bar' => array(
          'title' => 'Bar',
        ),
      ),
    ),
    'node/%/edit' => array(
      'title' => 'Edit',
      'weight' => -9,
      'subtabs' => array(
        'node/%/edit' => array(
          'title' => 'General',
        ),
        'node/%/edit/Issues' => array(
          'title' => 'Issues',
        ),
        'node/%/edit/releases' => array(
          'title' => 'Releases',
        ),
      ),
    ),
  );
merlinofchaos’s picture

Ok, I just read my example, and it's hideously mixed syntax. I apologize. Please, er, pretend that I was consistent. I think the basic premise still comes through.

kika’s picture

Can we please remove referring to everything "tab"? Tabs are just one of the many user interface element variations menu items can be rendered to. In the same reason we have MENU_LOCAL_TASK, not MENU_(SUB)TAB.

Anonymous’s picture

subscribe

EvanDonovan’s picture

Is there a current patch to review on here? (Also, subscribe - very much excited about advancements to the menu system... :)

gábor hojtsy’s picture

I was pointed to here from #542658: Move action "tabs" out of local tasks, where I put out a hack to separate some of the tabs to "action buttons". The d7ux wind of thinking is that tabs should be used for "different views of the same thing", while things like "add" and similar actions would be shown in an "action area" on the page. It might be good to keep that in mind, so we can keep those in the menu system but expose them under a specific UI. @sun reminded us that we should keep this somehow under the menu system umbrella, so alternate navigation modules like admin_menu can display these just like before.

chx’s picture

Assigned: chx » Unassigned
sun’s picture

Version: 7.x-dev » 8.x-dev
sun’s picture

Title: Big task cleanup: remove type and tasks from hook_menu » Big task cleanup: Remove type and tasks from hook_menu()
Issue tags: +MenuSystemRevamp

Tagging related issues.

pwolanin’s picture

Discussing w/ chx in NYC.

We are are settling on a taskset (tabset) concept - dynamic behavior comes from adding different tasksets for each path. We key them by router path and menu.inc filters out by the current path.

function hook_local_task($router_item) {
  // The value is the level at which the tasks are displayed.
  $tasks = array (
    'pony/%' => array(
       'pony' => 0,
       'pony_actions' => 'action',
       'pony_below' => 1,
       'pony_also' => 1,
    ),
    'pony/%/edit' => array(
       'pony' => 0,
       'pony_edit_below' => 1,
    ),
  );
  if ($router_item['map'][0] == 'pony' &&
      is_object($router_item['map'][1]) &&
      $router_item['map'][1]->pony_id == 99) {
    // On only pony 99 display an additional special tabset on the top level.
    $tasks['pony/%']['pony_express'] = 0;
    $tasks['pony/%/edit']['pony_express'] = 0;
  }
  return $tasks;
}

This gives use the name of a taskset, so we then invoke a corresponding hook, which returns a FIXED set of tabs. Example for node module:


/**
 * Implements hook_local_task().
 */
function node_local_task($router_item) {
  // The value is the level at which the tasks are displayed.
  $tasks = array (
    'node/%' => array(
       'node' => 0,
    ),
    'node/%/edit' => array(
       'node' => 0,
    ),
    'admin/structure/types' => array(
      'node_admin_types' => 0,
      'node_admin_types_action' => 'action',
    ),
    'admin/structure/types/manage/%' => array(
      'node_admin_type_manage' => 0,
    ),
  );
  return $tasks;
}

/**
 * Implements hook_taskset_TASKNAME().
 */
function node_taskset_node() {
  return array(
    'node/%' => array(
      'text' => t('View'),
      'weight' => -10,
     ),
     'node/%/edit' => array(
       'text' => t('Edit'),
       'weight' => 0,
     ),
     'node/%node/revisions' => array(
       'text' => t('Revisions'),
       'weight' => 2,
     ),
  );
}

/**
 * Implements hook_taskset_TASKNAME().
 */
function node_taskset_node_admin_types_action() {
  return array(
    'node_admin_types' => array(
      'text' => t('List'),
      'weight' => -10,
      'options' => array(),
    ),
  ),
}

/**
 * Implements hook_taskset_TASKNAME().
 */
function node_taskset_node_admin_types_action() {
  return array(
    'admin/structure/types/add' => array(
      'text' => t('Add content type'),
      'weight' => -10,
    ),
  );
}

/**
 * Implements hook_taskset_TASKNAME().
 */
function node_taskset_node_admin_type_manage() {
  return array(
    'admin/structure/types/manage/%' => array(
      'text' => t('Edit'),
      'weight' => -10,
    ),
  );
}

sun’s picture

Sorry, but the NYC proposal in #62 is way too abstract (calling a hook to define multiple hooks to call, plus alter hooks).

IMHO, the most sound proposal so far was @merlinofchaos' in #44. I've tried to parse and understand the counter-arguments against it, but they are written in a very cryptic way. What I remotely understood are the questions:

  1. Alright, make tabsets indexed by the normalized menu link path (%), not a router path (%node).
  2. There's no need to have node/%/edit/foo specify that node/% is a remote parent. It's a tab on node/%/edit, and that's all we need to know.

Also:

  1. To avoid to generate needless tab info for irrelevant paths, make the router item define a tab callback.
  2. Invoke a hook to add tabs by other modules.
  3. Naturally, invoke a single final alter hook to allow manipulations of the collected results.
function node_menu() {
  $items['node/%node'] = array(
    'page callback' => 'node_view',
    'tab callback' => 'node_menu_tabs_node',
    'tab arguments' => array(1),
  );
  // Ladies, router paths are still required.
  $items['node/%node/edit'] = array(
    'page callback' => 'drupal_get_form',
    // tab callback and arguments is derived to children.
  );
}

function node_menu_tabs_node($node) {
  $tabs['node/%'] = array(
    'node/%/view' => array(
      'title' => 'View',
      'weight' => -10,
    ),
    'node/%/edit' => array(
      'title' => 'Edit',
    ),
    'node/%/revisions' => array(
      'title' => 'Revisions',
    ),
  );
  $tabs['node/%/revisions'] = array(
    'node/%/revisions' => array(
      'title' => 'List',
      'weight' => -10,
    ),
    // This tab obviously cannot be generated on node/%/revisions,
    // it serves as a hierarchical pointer to tabs for a single revision.
    'node/%/revisions/%' => array(
      'title' => 'View revision',
    ),
  );
  $tabs['node/%/revisions/%'] = array(
    'node/%/revisions/%' => array(
      'title' => 'View',
      'weight' => -10,
    ),
    'node/%/revisions/%/revert' => array(
      'title' => 'Revert',
    ),
    'node/%/revisions/%/delete' => array(
      'title' => 'Delete',
    ),
  );
  return $tabs;
}

...but in general, I think this whole idea is doomed. Instead of detaching, we should be attaching. To path arguments. Java's Jersey is something worth to look at.

In a nutshell, we forget about the path, and only care for the argument:

  $tabs['%node'] = array(
    'view' => array(
      'title' => 'View',
    ),
    'edit' => array(
      'title' => 'Edit',
    ),
    'revisions' => array(
      'title' => 'Revisions',
    ),
  );
  $tabs['%node_revision'] = array(
    'view' => array(
      'title' => 'View',
    ),
    'revert' => array(
      'title' => 'Revert',
    ),
    'delete' => array(
      'title' => 'Delete',
    ),
  );
jibran’s picture

Do we still need this? While we are shifting to routes.

hass’s picture

Issue summary: View changes
Status: Needs work » Closed (won't fix)

This case sounds very outdated. I guess this has been solved already in core.