diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 94d0e57..83134c7 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -2117,6 +2117,15 @@ function menu_local_tasks($level = 0) { // Remove the depth, we are interested only in their relative placement. $tabs = array_values($tabs); $data['tabs'] = $tabs; + // Look for route-based tabs. + $route_name = Drupal::request()->attributes->get('_route'); + if (!empty($route_name)) { + $manager = Drupal::service('plugin.manager.system.menu_local_task'); + $local_tasks = $manager->getTasksBuild($route_name); + foreach ($local_tasks as $level => $items) { + $data['tabs'][$level] = empty($data['tabs'][$level]) ? $items : array_merge($data['tabs'][$level], $items); + } + } // Allow modules to dynamically add further tasks. $module_handler = Drupal::moduleHandler(); @@ -2617,7 +2626,7 @@ function menu_get_active_breadcrumb() { // Don't show a link to the current page in the breadcrumb trail. $end = end($active_trail); - if ($item['href'] == $end['href']) { + if (Drupal::request()->attributes->get('system_path') == $end['href']) { array_pop($active_trail); } diff --git a/core/modules/system/lib/Drupal/system/Annotation/MenuLocalTask.php b/core/modules/system/lib/Drupal/system/Annotation/MenuLocalTask.php new file mode 100644 index 0000000..59e5e42 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Annotation/MenuLocalTask.php @@ -0,0 +1,70 @@ +t = $string_translation; + $this->generator = $generator; + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('string_translation'), + $container->get('url_generator') + ); + } + + /** + * {@inheritdoc} + */ + public function getRouteName() { + return $this->pluginDefinition['route_name']; + } + + /** + * {@inheritdoc} + */ + public function getTitle() { + // Subclasses may pull in the request or specific attributes as parameters. + return $this->pluginDefinition['title']; + } + + /** + * {@inheritdoc} + */ + public function getPath() { + // Subclasses may set a request into the generator or + // use any desired method to generate the path. + // @todo - use the new method from https://drupal.org/node/2031353 + $path = $this->generator->generate($this->getRouteName()); + // In order to get the Drupal path the base URL has + // to be stripped off. + $base_url = $this->generator->getContext()->getBaseUrl(); + if (!empty($base_url) && strpos($path, $base_url) === 0) { + $path = substr($path, strlen($base_url)); + } + return trim($path, '/'); + } + + /** + * Returns the weight of the local task. + * + * @return int + * The weight of the task. If not defined in the annotation returns 0 by + * default or -10 for the root tab. + */ + public function getWeight() { + // By default the weight is 0, or -10 for the root tab. + if (!isset($this->pluginDefinition['weight'])) { + if ($this->pluginDefinition['tab_root_id'] == $this->pluginDefinition['id']) { + $this->pluginDefinition['weight'] = -10; + } + else { + $this->pluginDefinition['weight'] = 0; + } + } + return (int) $this->pluginDefinition['weight']; + } + + /** + * {@inheritdoc} + */ + public function getOptions() { + $options = $this->pluginDefinition['options']; + if ($this->active) { + if (empty($options['attributes']['class']) || !in_array('active', $options['attributes']['class'])) { + $options['attributes']['class'][] = 'active'; + } + } + return (array) $options; + } + + /** + * {@inheritdoc} + */ + public function setActive($active = TRUE) { + $this->active = $active; + } + + /** + * {@inheritdoc} + */ + public function getActive() { + return $this->active; + } + +} diff --git a/core/modules/system/lib/Drupal/system/Plugin/MenuLocalTaskInterface.php b/core/modules/system/lib/Drupal/system/Plugin/MenuLocalTaskInterface.php new file mode 100644 index 0000000..ce564c5 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Plugin/MenuLocalTaskInterface.php @@ -0,0 +1,73 @@ + $namespaces['Drupal\system']); + parent::__construct('Menu', $namespaces, $annotation_namespaces, 'Drupal\system\Annotation\MenuLocalTask'); + $this->controllerResolver = $controller_resolver; + $this->request = $request; + $this->routeProvider = $route_provider; + } + + /** + * Gets the title for a local task. + * + * @param \Drupal\system\Plugin\MenuLocalTaskInterface $local_task + * An object to get the title from. + * + * @return string + * The title (already localized). + */ + public function getTitle(MenuLocalTaskInterface $local_task) { + + $controller = array($local_task, 'getTitle'); + $arguments = $this->controllerResolver->getArguments($this->request, $controller); + + return call_user_func_array($controller, $arguments); + } + + /** + * Gets the Drupal path for a local task. + * + * @param \Drupal\system\Plugin\MenuLocalTaskInterface $local_task + * An object to get the path from. + * + * @return string + * The path. + */ + public function getPath(MenuLocalTaskInterface $local_task) { + $controller = array($local_task, 'getPath'); + $arguments = $this->controllerResolver->getArguments($this->request, $controller); + + return call_user_func_array($controller, $arguments); + } + + /** + * Find all local tasks that appear on a named route. + * + * @param string $route_name + * The route for which to find local tasks. + * + * @return array + * Returns an array of task levels. Each task level contains instances + * of local tasks (MenuLocalTaskInterface) which appear on the tab route. + * The array keys are the depth, and at each depth is an array of + * instances. + */ + public function getLocalTasksForRoute($route_name) { + if (!isset($this->instances[$route_name])) { + $this->instances[$route_name] = array(); + // @todo - optimize this lookup by compiling or caching. + $definitions = $this->getDefinitions(); + // We build the hierarchy by finding all tabs that should + // appear on the current route. + $tab_root_ids = array(); + $parents = array(); + foreach ($definitions as $plugin_id => $task_info) { + if ($route_name == $task_info['route_name']) { + $tab_root_ids[$task_info['tab_root_id']] = TRUE; + // Tabs that link to the current route are viable parents + // and their parent and children should be visible also. + // @todo - this only works for 2 levels of tabs. + // instead need to iterate up. + $parents[$plugin_id] = TRUE; + if (!empty($task_info['tab_parent_id'])) { + $parents[$task_info['tab_parent_id']] = TRUE; + } + } + } + if ($tab_root_ids) { + // Find all the plugins with the same root and that are at the top + // level or that have a visible parent. + $children = array(); + foreach ($definitions as $plugin_id => $task_info) { + if (!empty($tab_root_ids[$task_info['tab_root_id']]) && (empty($task_info['tab_parent_id']) || !empty($parents[$task_info['tab_parent_id']]))) { + // Concat '> ' with root ID for the parent of top-level tabs. + $parent = empty($task_info['tab_parent_id']) ? '> ' . $task_info['tab_root_id'] : $task_info['tab_parent_id']; + $children[$parent][$plugin_id] = $task_info; + } + } + foreach (array_keys($tab_root_ids) as $root_id) { + // Convert the tree keyed by plugin IDs into a simple one with + // integer depth. Create instances for each plugin along the way. + $level = 0; + // We used this above as the top-level parent array key. + $next_parent = '> ' . $root_id; + do { + $parent = $next_parent; + $next_parent = FALSE; + foreach ($children[$parent] as $plugin_id => $task_info) { + $plugin = $this->createInstance($plugin_id); + $this->instances[$route_name][$level][$plugin_id] = $plugin; + // Normally, l() compares the href of every link with the current + // path and sets the active class accordingly. But the parents of + // the current local task may be on a different route in which + // case we have to set the class manually by flagging it active. + if (!empty($parents[$plugin_id]) && $route_name != $task_info['route_name']) { + $plugin->setActive(); + } + if (isset($children[$plugin_id])) { + // This tab has visible children + $next_parent = $plugin_id; + } + } + $level++; + } while ($next_parent); + } + } + } + return $this->instances[$route_name]; + } + + /** + * Get the render array for all local tasks. + * + * @param string $route_name + * The route for which to make renderable local tasks. + * + * @return array + * A render array as expected by theme_menu_local_tasks. + */ + public function getTasksBuild($route_name) { + $tree = $this->getLocalTasksForRoute($route_name); + $build = array(); + foreach ($tree as $level => $instances) { + foreach ($instances as $child) { + $path = $this->getPath($child); + // Find out whether the user has access to the task. + $route = $this->routeProvider->getRouteByName($child->getRouteName()); + $map = array(); + // @todo - replace this call when we have a real service for it. + $access = menu_item_route_access($route, $path, $map); + if ($access) { + // Need to flag the list element as active for a tab for the current + // route or if the plugin is set active (i.e. the parent tab). + $active = ($route_name == $child->getRouteName() || $child->getActive()); + // @todo It might make sense to use menu link entities instead of + // arrays. + $menu_link = array( + 'title' => $this->getTitle($child), + 'href' => $path, + 'localized_options' => $child->getOptions(), + ); + $build[$level][$path] = array( + '#theme' => 'menu_local_task', + '#link' => $menu_link, + '#active' => $active, + '#weight' => $child->getWeight(), + '#access' => $access, + ); + } + } + } + return $build; + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/LocalTasksTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/LocalTasksTest.php index 5f695c0..34034d6 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/LocalTasksTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/LocalTasksTest.php @@ -140,4 +140,48 @@ protected function assertLocalTasks(array $hrefs, $level = 0) { } } + /** + * Tests the plugin based local tasks. + */ + public function testPluginLocalTask() { + // Verify that local tasks appear as defined in the router. + $this->drupalGet('menu-local-task-test/tasks'); + + $this->drupalGet('menu-local-task-test/tasks/view'); + $this->assertLocalTasks(array( + 'menu-local-task-test/tasks/view', + 'menu-local-task-test/tasks/settings', + 'menu-local-task-test/tasks/edit', + )); + + // Ensure the view tab is active. + $result = $this->xpath('//ul[contains(@class, "tabs")]//a[contains(@class, "active")]'); + $this->assertEqual(1, count($result), 'There is just a single active tab.'); + $this->assertEqual('View', (string) $result[0], 'The view tab is active.'); + + // Verify that local tasks in the second level appear. + + $this->drupalGet('menu-local-task-test/tasks/settings'); + $this->assertLocalTasks(array( + 'menu-local-task-test/tasks/settings/sub1', + 'menu-local-task-test/tasks/settings/sub2', + ), 1); + + $result = $this->xpath('//ul[contains(@class, "tabs")]//a[contains(@class, "active")]'); + $this->assertEqual(1, count($result), 'There is just a single active tab.'); + $this->assertEqual('Settings', (string) $result[0], 'The settings tab is active.'); + + + $this->drupalGet('menu-local-task-test/tasks/settings/sub1'); + $this->assertLocalTasks(array( + 'menu-local-task-test/tasks/settings/sub1', + 'menu-local-task-test/tasks/settings/sub2', + ), 1); + + $result = $this->xpath('//ul[contains(@class, "tabs")]//a[contains(@class, "active")]'); + $this->assertEqual(2, count($result), 'There are tabs active on both levels.'); + $this->assertEqual('Settings', (string) $result[0], 'The settings tab is active.'); + $this->assertEqual('sub1', (string) $result[1], 'The sub1 tab is active.'); + } + } diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml index 6aefa00..7cd11de 100644 --- a/core/modules/system/system.services.yml +++ b/core/modules/system/system.services.yml @@ -6,6 +6,9 @@ services: plugin.manager.system.plugin_ui: class: Drupal\system\Plugin\Type\PluginUIManager arguments: ['@container.namespaces'] + plugin.manager.system.menu_local_task: + class: Drupal\system\Plugin\Type\MenuLocalTaskManager + arguments: ['@container.namespaces', '@controller_resolver', '@request', '@router.route_provider'] system.manager: class: Drupal\system\SystemManager arguments: ['@module_handler', '@database'] diff --git a/core/modules/system/tests/Drupal/system/Tests/Plugin/Type/MenuLocalTaskManagerTest.php b/core/modules/system/tests/Drupal/system/Tests/Plugin/Type/MenuLocalTaskManagerTest.php new file mode 100644 index 0000000..caf0598 --- /dev/null +++ b/core/modules/system/tests/Drupal/system/Tests/Plugin/Type/MenuLocalTaskManagerTest.php @@ -0,0 +1,208 @@ + 'Local tasks manager.', + 'description' => 'Tests local tasks manager.', + 'group' => 'Menu', + ); + } + + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->controllerResolver = $this->getMock('Symfony\Component\HttpKernel\Controller\ControllerResolverInterface'); + $this->request = new Request(); + $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface'); + $this->pluginDiscovery = $this->getMock('Drupal\Component\Plugin\Discovery\DiscoveryInterface'); + $this->factory = $this->getMock('Drupal\Component\Plugin\Factory\FactoryInterface'); + + $this->setupLocalTaskManager(); + } + + /** + * Tests the getLocalTasksForRoute method. + * + * @see \Drupal\system\Plugin\Type\MenuLocalTaskManager::getLocalTasksForRoute() + */ + public function testGetLocalTasksForRouteSingleLevelTitle() { + $definitions = array(); + $definitions['menu_local_task_test_tasks_settings'] = array( + 'id' => 'menu_local_task_test_tasks_settings', + 'route_name' => 'menu_local_task_test_tasks_settings', + 'title' => 'Settings', + 'tab_root_id' => 'menu_local_task_test_tasks_view', + 'class' => 'Drupal\menu_test\Plugin\Menu\MenuLocalTasksTestTasksSettings', + ); + $definitions['menu_local_task_test_tasks_edit'] = array( + 'id' => 'menu_local_task_test_tasks_edit', + 'route_name' => 'menu_local_task_test_tasks_edit', + 'title' => 'Settings', + 'tab_root_id' => 'menu_local_task_test_tasks_view', + 'class' => 'Drupal\menu_test\Plugin\Menu\MenuLocalTasksTestTasksEdit', + 'weight' => 20, + ); + $definitions['menu_local_task_test_tasks_view'] = array( + 'id' => 'menu_local_task_test_tasks_view', + 'route_name' => 'menu_local_task_test_tasks_view', + 'title' => 'Settings', + 'tab_root_id' => 'menu_local_task_test_tasks_view', + 'class' => 'Drupal\menu_test\Plugin\Menu\MenuLocalTasksTestTasksView', + ); + + $this->pluginDiscovery->expects($this->any()) + ->method('getDefinitions') + ->will($this->returnValue($definitions)); + + $mock_plugin = $this->getMock('Drupal\system\Plugin\MenuLocalTaskInterface'); + + $map = array( + array('menu_local_task_test_tasks_settings', array(), $mock_plugin), + array('menu_local_task_test_tasks_edit', array(), $mock_plugin), + array('menu_local_task_test_tasks_view', array(), $mock_plugin), + ); + $this->factory->expects($this->any()) + ->method('createInstance') + ->will($this->returnValueMap($map)); + + $this->setupLocalTaskManager(); + + $local_tasks = $this->manager->getLocalTasksForRoute('menu_local_task_test_tasks_view'); + $this->assertEquals(array(0 => array( + 'menu_local_task_test_tasks_settings' => $mock_plugin, + 'menu_local_task_test_tasks_view' => $mock_plugin, + 'menu_local_task_test_tasks_edit' => $mock_plugin, + )), $local_tasks); + } + + /** + * Tests the getTitle method. + * + * @see \Drupal\system\Plugin\Type\MenuLocalTaskManager::getTitle() + */ + public function testGetTitle() { + $menu_local_task = $this->getMock('Drupal\system\Plugin\MenuLocalTaskInterface'); + $menu_local_task->expects($this->once()) + ->method('getTitle'); + + $this->controllerResolver->expects($this->once()) + ->method('getArguments') + ->with($this->request, array($menu_local_task, 'getTitle')) + ->will($this->returnValue(array())); + + $this->manager->getTitle($menu_local_task); + } + + /** + * Tests the getPath method. + * + * @see \Drupal\system\Plugin\Type\MenuLocalTaskManager::getPath() + */ + public function testGetPath() { + $menu_local_task = $this->getMock('Drupal\system\Plugin\MenuLocalTaskInterface'); + $menu_local_task->expects($this->once()) + ->method('getPath'); + + $this->controllerResolver->expects($this->once()) + ->method('getArguments') + ->with($this->request, array($menu_local_task, 'getPath')) + ->will($this->returnValue(array())); + + $this->manager->getPath($menu_local_task); + } + + /** + * Setups the local task manager for the test. + */ + protected function setupLocalTaskManager() { + $this->manager = $this + ->getMockBuilder('Drupal\system\Plugin\Type\MenuLocalTaskManager') + ->disableOriginalConstructor() + ->setMethods(NULL) + ->getMock(); + + $property = new \ReflectionProperty('Drupal\system\Plugin\Type\MenuLocalTaskManager', 'controllerResolver'); + $property->setAccessible(TRUE); + $property->setValue($this->manager, $this->controllerResolver); + + $property = new \ReflectionProperty('Drupal\system\Plugin\Type\MenuLocalTaskManager', 'request'); + $property->setAccessible(TRUE); + $property->setValue($this->manager, $this->request); + + $property = new \ReflectionProperty('Drupal\system\Plugin\Type\MenuLocalTaskManager', 'discovery'); + $property->setAccessible(TRUE); + $property->setValue($this->manager, $this->pluginDiscovery); + + $property = new \ReflectionProperty('Drupal\system\Plugin\Type\MenuLocalTaskManager', 'factory'); + $property->setAccessible(TRUE); + $property->setValue($this->manager, $this->factory); + } + +} + diff --git a/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Plugin/Menu/MenuLocalTasksTestTasksEdit.php b/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Plugin/Menu/MenuLocalTasksTestTasksEdit.php new file mode 100644 index 0000000..5d24fcd --- /dev/null +++ b/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Plugin/Menu/MenuLocalTasksTestTasksEdit.php @@ -0,0 +1,25 @@ + MENU_LOCAL_ACTION, ); + $items['menu-local-task-test/tasks'] = array( + 'title' => 'Local tasks', + 'route_name' => 'menu_local_task_test_tasks', + ); + return $items; } diff --git a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml index 3154626..32e1ea2 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml +++ b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml @@ -44,3 +44,52 @@ menu_test_local_action4: _content: '\Drupal\menu_test\TestControllers::test2' requirements: _access: 'TRUE' + +menu_local_task_test_tasks: + pattern: '/menu-local-task-test/tasks' + defaults: + _content: '\Drupal\menu_test\TestControllers::test1' + requirements: + _access: 'TRUE' + +menu_local_task_test_tasks_tasks: + pattern: '/menu-local-task-test/tasks/tasks' + defaults: + _content: '\Drupal\menu_test\TestControllers::test1' + requirements: + _access: 'TRUE' + +menu_local_task_test_tasks_view: + pattern: '/menu-local-task-test/tasks/view' + defaults: + _content: '\Drupal\menu_test\TestControllers::test1' + requirements: + _access: 'TRUE' + +menu_local_task_test_tasks_edit: + pattern: '/menu-local-task-test/tasks/edit' + defaults: + _content: '\Drupal\menu_test\TestControllers::test1' + requirements: + _access: 'TRUE' + +menu_local_task_test_tasks_settings: + pattern: '/menu-local-task-test/tasks/settings' + defaults: + _content: '\Drupal\menu_test\TestControllers::test1' + requirements: + _access: 'TRUE' + +menu_local_task_test_tasks_settings_sub1: + pattern: '/menu-local-task-test/tasks/settings/sub1' + defaults: + _content: '\Drupal\menu_test\TestControllers::test1' + requirements: + _access: 'TRUE' + +menu_local_task_test_tasks_settings_sub2: + pattern: '/menu-local-task-test/tasks/settings/sub2' + defaults: + _content: '\Drupal\menu_test\TestControllers::test1' + requirements: + _access: 'TRUE' diff --git a/core/modules/views_ui/lib/Drupal/views_ui/Plugin/Menu/ViewsListTask.php b/core/modules/views_ui/lib/Drupal/views_ui/Plugin/Menu/ViewsListTask.php new file mode 100644 index 0000000..188a6e2 --- /dev/null +++ b/core/modules/views_ui/lib/Drupal/views_ui/Plugin/Menu/ViewsListTask.php @@ -0,0 +1,25 @@ + 'views_ui.list', ); - $items['admin/structure/views/list'] = array( - 'title' => 'List', - 'type' => MENU_DEFAULT_LOCAL_TASK, - ); - $items['admin/structure/views/add'] = array( 'route_name' => 'views_ui.add', 'type' => MENU_SIBLING_LOCAL_TASK, ); - $items['admin/structure/views/settings'] = array( - 'title' => 'Settings', - 'route_name' => 'views_ui.settings.basic', - 'type' => MENU_LOCAL_TASK, - ); - $items['admin/structure/views/settings/basic'] = array( - 'title' => 'Basic', - 'type' => MENU_DEFAULT_LOCAL_TASK, - ); - $items['admin/structure/views/settings/advanced'] = array( - 'title' => 'Advanced', - 'route_name' => 'views_ui.settings.advanced', - 'type' => MENU_LOCAL_TASK, - ); - // The primary Edit View page. Secondary tabs for each Display are added in // views_ui_menu_local_tasks_alter(). $items['admin/structure/views/view/%'] = array(