Closed (won't fix)
Project:
Drupal core
Version:
8.0.x-dev
Component:
menu system
Priority:
Normal
Category:
Task
Assigned:
Unassigned
Issue tags:
Reporter:
Created:
7 Jun 2009 at 07:38 UTC
Updated:
10 Jan 2015 at 22:58 UTC
Jump to comment: Most recent, Most recent file
Comments
Comment #1
robertdouglass commentedGetting rid of the default menu item for local tasks would be a step forward in developer UX.
Comment #2
pwolanin commentedMaybe - I have doubts about this approach, however.
Comment #3
chx commentedMe too. Let's throw out tasks from both menu tables and introduce a whole new structure and table. So we have hook_menu_tabs
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!
Comment #4
chx commentedNote: equals will be used so that you can have node/%/view/whatever on the second level.
Comment #5
chx commentedComment #6
moshe weitzman commentedThis 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).
Comment #7
chx commentedCrosspost.
Comment #8
chx commentedPeter 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...).
Comment #9
pwolanin commentedPossibly 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.
Comment #10
pwolanin commentedAlso, 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.
Comment #11
chx commentedBefore 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.
Comment #12
catchTabs as menu links makes a load of sense.
Comment #13
pwolanin commented@catch, no I don't think they make much sense as links - a lot of practical roadblocks.
Comment #15
chx commentedThis 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.
Comment #16
chx commentedCurrent state and aidan promised to help with the task conversion.
Comment #17
bjaspan commentedsubscribe
Comment #18
aidanlis commentedI'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!
Comment #19
aidanlis commentedI'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?
Comment #20
chx commentedI 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.
Comment #21
pwolanin commentedHaving 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')
...
Comment #22
chx commentedI 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.
Comment #23
aidanlis commentedCleaned up based on comments and fixed another bug in menu.inc
Comment #24
aidanlis commentedModules 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
Comment #25
aidanlis commented- Ensure tabs are not shown if there's only 1 tab for that level
- Convert system.module
Comment #26
chx commentedaidanlis, thanks. You can post one patch , no need to post per file. Some remarks. I see constructs like
you can definitely merge these two lines into one if
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!
Comment #27
sunHoly. 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... ?
Comment #28
chx commentedduplicate 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.
Comment #29
chx commentedYes. We should remove access control from the tabs (sorry aidan) and run menu_get_item on them in menu.inc:
Comment #30
chx commentedAfter 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)
Comment #31
pwolanin commented@chx:
I don't really like this - see above suggesiton re: menu substrings vs alwys needing to test against each arg.
Comment #32
aidanlis commentedI'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
Comment #33
chx commentedaidanlis, please wait before continue. I am working on this a bit.
Comment #34
aidanlis commentedI'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.
Comment #35
merlinofchaos commentedOk, I'm largely in agreement with Peter. I really dislike the count() stuff. However, what about this:
This:
Becomes:
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:
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.
Comment #36
merlinofchaos commentedI 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.
Comment #37
sunI 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...?
Comment #38
chx commentedComment #39
sunComment #40
chx commentedsun, 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...
Comment #41
chx commentedThe 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.
Comment #42
chx commentedContinuing 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.
Comment #43
chx commentedContinuing 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.
Comment #44
merlinofchaos commentedI 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'.
Now, along comes project.module which wants to add some subtabs under edit. No problem:
Dynamic tabs should work just fine with a third hook, which allows us to do the realtime stuff:
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:
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.
Comment #45
chx commentedA 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.
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.
Comment #46
pwolanin commentedIf 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.
Comment #47
merlinofchaos commentedIf 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.
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.
Comment #48
chx commentedSo 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.
Comment #49
chx commented*however* we still have a problem here -- what if you have a second level tabset under node/%?
Comment #50
merlinofchaos commentedThat 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).
Comment #51
chx commentedyeah 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?
Comment #52
chx commentedSo today we came up with this
Comment #53
merlinofchaos commentedHmm. 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:
Comment #54
merlinofchaos commentedOk, 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.
Comment #55
kika commentedCan 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.
Comment #56
Anonymous (not verified) commentedsubscribe
Comment #57
EvanDonovan commentedIs there a current patch to review on here? (Also, subscribe - very much excited about advancements to the menu system... :)
Comment #58
gábor hojtsyI 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.
Comment #59
chx commentedComment #60
sunComment #61
sunTagging related issues.
Comment #62
pwolanin commentedDiscussing 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.
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:
Comment #63
sunSorry, 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:
node/%/edit/foospecify thatnode/%is a remote parent. It's a tab onnode/%/edit, and that's all we need to know.Also:
tab callback....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:
Comment #64
jibranDo we still need this? While we are shifting to routes.
Comment #65
hass commentedThis case sounds very outdated. I guess this has been solved already in core.