diff --git a/core/modules/views/lib/Drupal/views/Plugin/Derivative/ViewsLocalTask.php b/core/modules/views/lib/Drupal/views/Plugin/Derivative/ViewsLocalTask.php new file mode 100644 index 0000000..bda9051 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/Derivative/ViewsLocalTask.php @@ -0,0 +1,170 @@ +routeProvider = $route_provider; + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('router.route_provider'), + $container->get('state') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions(array $base_plugin_definition) { + $this->derivatives = array(); + + $view_route_names = $this->state->get('views.view_route_names'); + foreach ($this->getApplicableMenuViews() as $pair) { + /** @var $executable \Drupal\views\ViewExecutable */ + list($executable, $display_id) = $pair; + + $executable->setDisplay($display_id); + $menu = $executable->display_handler->getOption('menu'); + if (in_array($menu['type'], array('tab', 'default tab'))) { + $plugin_id = 'view.' . $executable->storage->id() . '.' . $display_id; + $route_name = $view_route_names[$executable->storage->id() . '.' . $display_id]; + + // Don't add a local task for views which override existing routes. + if ($route_name != $plugin_id) { + continue; + } + + $this->derivatives[$plugin_id] = array( + 'route_name' => $route_name, + 'weight' => $menu['weight'], + 'title' => $menu['title'], + ) + $base_plugin_definition; + + // Default local tasks have themselves as tab root id. + if ($menu['type'] == 'default tab') { + $this->derivatives[$plugin_id]['tab_root_id'] = 'views_view:' . $plugin_id; + } + } + } + return $this->derivatives; + } + + /** + * Alters tab_root_id and tab_parent_id into the views local tasks. + */ + public function alterLocalTasks(&$local_tasks) { + $view_route_names = $this->state->get('views.view_route_names'); + + foreach ($this->getApplicableMenuViews() as $pair) { + /** @var $executable \Drupal\views\ViewExecutable */ + list($executable, $display_id) = $pair; + + $executable->setDisplay($display_id); + $menu = $executable->display_handler->getOption('menu'); + + // We already have set the tab_root_id for default tabs. + if (in_array($menu['type'], array('tab'))) { + $plugin_id = 'view.' . $executable->storage->id() . '.' . $display_id; + $view_route_name = $view_route_names[$executable->storage->id() . '.' . $display_id]; + + // Don't add a local task for views which override existing routes. + if ($view_route_name != $plugin_id) { + unset($local_tasks[$plugin_id]); + continue; + } + + // Find out the parent route. + // @todo Find out how to find both the root and parent tab. + $path = $executable->display_handler->getPath(); + $split = explode('/', $path); + array_pop($split); + $path = implode('/', $split); + + $pattern = '/' . str_replace('%', '{}', $path); + if ($routes = $this->routeProvider->getRoutesByPattern($pattern)) { + foreach ($routes->all() as $name => $route) { + if ($parent_task = $this->getTaskFromRoute($name, $local_tasks)) { + $local_tasks['views_view:' . $plugin_id]['tab_root_id'] = $parent_task; + } + // Skip after the first found route. + break; + } + } + } + } + } + + /** + * Find the local task ID of the parent route given the route name. + * + * @param string $route_name + * The route name of the parent local task. + * @param array $local_tasks + * An array of all local task definitions. + * + * @return bool|string + * Returns the local task ID of the parent task, otherwise return FALSE. + */ + protected function getTaskFromRoute($route_name, &$local_tasks) { + $local_task = FALSE; + foreach ($local_tasks as $plugin_id => $local_task) { + if ($local_task['route_name'] == $route_name) { + $local_task = $plugin_id; + break; + } + } + + return $local_task; + } + + /** + * @return array + */ + protected function getApplicableMenuViews() { + return views_get_applicable_views('uses_hook_menu'); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/Menu/LocalTask/ViewsLocalTask.php b/core/modules/views/lib/Drupal/views/Plugin/Menu/LocalTask/ViewsLocalTask.php new file mode 100644 index 0000000..8bf6684 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/Menu/LocalTask/ViewsLocalTask.php @@ -0,0 +1,16 @@ +drupalGet('test_route_without_arguments'); $this->assertResponse(200); $result = $this->xpath('//span[@class="field-content"]'); diff --git a/core/modules/views/tests/Drupal/views/Tests/Plugin/Derivative/ViewsLocalTaskTest.php b/core/modules/views/tests/Drupal/views/Tests/Plugin/Derivative/ViewsLocalTaskTest.php new file mode 100644 index 0000000..78d85a5 --- /dev/null +++ b/core/modules/views/tests/Drupal/views/Tests/Plugin/Derivative/ViewsLocalTaskTest.php @@ -0,0 +1,339 @@ + '\Drupal\views\Plugin\Menu\LocalTask\ViewsLocalTask', + 'derivative' => '\Drupal\views\Plugin\Derivative\ViewsLocalTask' + ); + + /** + * The tested local task derivative class. + * + * @var \Drupal\views\Plugin\Derivative\ViewsLocalTask + */ + protected $localTaskDerivative; + + public static function getInfo() { + return array( + 'name' => 'Views local task derivative', + 'description' => 'Tests the views local task derivative.', + 'group' => 'Views plugin', + ); + } + + protected function setUp() { + $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface'); + $this->state = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreInterface'); + + $this->localTaskDerivative = new TestViewsLocalTask($this->routeProvider, $this->state); + } + + /** + * Tests fetching the derivatives on no view with hook menu. + * + * @see \Drupal\views\Plugin\Derivative\ViewsLocalTask::getDerivativeDefinitions() + */ + public function testGetDerivativeDefinitionsWithoutHookMenuViews() { + $result = array(); + $this->localTaskDerivative->setApplicableMenuViews($result); + + $definitions = $this->localTaskDerivative->getDerivativeDefinitions($this->baseDefinition); + $this->assertEquals(array(), $definitions); + } + + /** + * Tests fetching the derivatives on a view with without a local task. + */ + public function testGetDerivativeDefinitionsWithoutLocalTask() { + $executable = $this->getMockBuilder('Drupal\views\ViewExecutable') + ->disableOriginalConstructor() + ->getMock(); + $display_plugin = $this->getMockBuilder('Drupal\views\Plugin\views\display\PathPluginBase') + ->setMethods(array('getOption')) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $display_plugin->expects($this->once()) + ->method('getOption') + ->with('menu') + ->will($this->returnValue(array('type' => 'normal'))); + $executable->display_handler = $display_plugin; + + $result = array(array($executable, 'page_1')); + $this->localTaskDerivative->setApplicableMenuViews($result); + + $definitions = $this->localTaskDerivative->getDerivativeDefinitions($this->baseDefinition); + $this->assertEquals(array(), $definitions); + } + + /** + * Tests fetching the derivatives on a view with a default local task. + */ + public function testGetDerivativeDefinitionsWithLocalTask() { + $executable = $this->getMockBuilder('Drupal\views\ViewExecutable') + ->disableOriginalConstructor() + ->getMock(); + $storage = $this->getMockBuilder('Drupal\views\Entity\View') + ->disableOriginalConstructor() + ->getMock(); + $storage->expects($this->any()) + ->method('id') + ->will($this->returnValue('example_view')); + $executable->storage = $storage; + + $display_plugin = $this->getMockBuilder('Drupal\views\Plugin\views\display\PathPluginBase') + ->setMethods(array('getOption')) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $display_plugin->expects($this->once()) + ->method('getOption') + ->with('menu') + ->will($this->returnValue(array('type' => 'tab', 'weight' => 12, 'title' => 'Example title'))); + $executable->display_handler = $display_plugin; + + $result = array(array($executable, 'page_1')); + $this->localTaskDerivative->setApplicableMenuViews($result); + + // Mock the view route names state. + $view_route_names = array(); + $view_route_names['example_view.page_1'] = 'view.example_view.page_1'; + $this->state->expects($this->once()) + ->method('get') + ->with('views.view_route_names') + ->will($this->returnValue($view_route_names)); + + $definitions = $this->localTaskDerivative->getDerivativeDefinitions($this->baseDefinition); + $this->assertCount(1, $definitions); + $this->assertEquals('view.example_view.page_1', $definitions['view.example_view.page_1']['route_name']); + $this->assertEquals(12, $definitions['view.example_view.page_1']['weight']); + $this->assertEquals('Example title', $definitions['view.example_view.page_1']['title']); + $this->assertEquals($this->baseDefinition['class'], $definitions['view.example_view.page_1']['class']); + $this->assertTrue(empty($definitions['view.example_view.page_1']['tab_root_id'])); + } + + /** + * Tests fetching the derivatives on a view which overrides an existing route. + */ + public function testGetDerivativeDefinitionsWithOverrideRoute() { + $executable = $this->getMockBuilder('Drupal\views\ViewExecutable') + ->disableOriginalConstructor() + ->getMock(); + $storage = $this->getMockBuilder('Drupal\views\Entity\View') + ->disableOriginalConstructor() + ->getMock(); + $storage->expects($this->any()) + ->method('id') + ->will($this->returnValue('example_view')); + $executable->storage = $storage; + + $display_plugin = $this->getMockBuilder('Drupal\views\Plugin\views\display\PathPluginBase') + ->setMethods(array('getOption')) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $display_plugin->expects($this->once()) + ->method('getOption') + ->with('menu') + ->will($this->returnValue(array('type' => 'tab', 'weight' => 12))); + $executable->display_handler = $display_plugin; + + $result = array(array($executable, 'page_1')); + $this->localTaskDerivative->setApplicableMenuViews($result); + + // Mock the view route names state. + $view_route_names = array(); + // Setup a view which overrides an existing route. + $view_route_names['example_view.page_1'] = 'example_overridden_route'; + $this->state->expects($this->once()) + ->method('get') + ->with('views.view_route_names') + ->will($this->returnValue($view_route_names)); + + $definitions = $this->localTaskDerivative->getDerivativeDefinitions($this->baseDefinition); + $this->assertCount(0, $definitions); + } + + /** + * Tests fetching the derivatives on a view with a default local task. + */ + public function testGetDerivativeDefinitionsWithDefaultLocalTask() { + $executable = $this->getMockBuilder('Drupal\views\ViewExecutable') + ->disableOriginalConstructor() + ->getMock(); + $storage = $this->getMockBuilder('Drupal\views\Entity\View') + ->disableOriginalConstructor() + ->getMock(); + $storage->expects($this->any()) + ->method('id') + ->will($this->returnValue('example_view')); + $executable->storage = $storage; + + $display_plugin = $this->getMockBuilder('Drupal\views\Plugin\views\display\PathPluginBase') + ->setMethods(array('getOption')) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $display_plugin->expects($this->exactly(2)) + ->method('getOption') + ->with('menu') + ->will($this->returnValue(array('type' => 'default tab', 'weight' => 12, 'title' => 'Example title'))); + $executable->display_handler = $display_plugin; + + $result = array(array($executable, 'page_1')); + $this->localTaskDerivative->setApplicableMenuViews($result); + + // Mock the view route names state. + $view_route_names = array(); + $view_route_names['example_view.page_1'] = 'view.example_view.page_1'; + $this->state->expects($this->exactly(2)) + ->method('get') + ->with('views.view_route_names') + ->will($this->returnValue($view_route_names)); + + $definitions = $this->localTaskDerivative->getDerivativeDefinitions($this->baseDefinition); + $this->assertCount(1, $definitions); + $plugin = $definitions['view.example_view.page_1']; + $this->assertEquals('view.example_view.page_1', $plugin['route_name']); + $this->assertEquals(12, $plugin['weight']); + $this->assertEquals('Example title', $plugin['title']); + $this->assertEquals($this->baseDefinition['class'], $plugin['class']); + $this->assertEquals('views_view:view.example_view.page_1', $plugin['tab_root_id']); + + // Setup the prefix of the derivative. + $definitions['views_view:view.example_view.page_1'] = $definitions['view.example_view.page_1']; + unset($definitions['view.example_view.page_1']); + $this->localTaskDerivative->alterLocalTasks($definitions); + + $plugin = $definitions['views_view:view.example_view.page_1']; + $this->assertCount(1, $definitions); + $this->assertEquals('view.example_view.page_1', $plugin['route_name']); + $this->assertEquals(12, $plugin['weight']); + $this->assertEquals('Example title', $plugin['title']); + $this->assertEquals($this->baseDefinition['class'], $plugin['class']); + $this->assertEquals('views_view:view.example_view.page_1', $plugin['tab_root_id']); + } + + /** + * Tests fetching the derivatives on a view with a local task and a parent. + * + * The parent is defined by another module, not views. + */ + public function testGetDerivativeDefinitionsWithExistingLocalTask() { + $executable = $this->getMockBuilder('Drupal\views\ViewExecutable') + ->disableOriginalConstructor() + ->getMock(); + $storage = $this->getMockBuilder('Drupal\views\Entity\View') + ->disableOriginalConstructor() + ->getMock(); + $storage->expects($this->any()) + ->method('id') + ->will($this->returnValue('example_view')); + $executable->storage = $storage; + + $display_plugin = $this->getMockBuilder('Drupal\views\Plugin\views\display\PathPluginBase') + ->setMethods(array('getOption', 'getPath')) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $display_plugin->expects($this->exactly(2)) + ->method('getOption') + ->with('menu') + ->will($this->returnValue(array('type' => 'tab', 'weight' => 12, 'title' => 'Example title'))); + $display_plugin->expects($this->once()) + ->method('getPath') + ->will($this->returnValue('path/example')); + $executable->display_handler = $display_plugin; + + $result = array(array($executable, 'page_1')); + $this->localTaskDerivative->setApplicableMenuViews($result); + + // Mock the view route names state. + $view_route_names = array(); + $view_route_names['example_view.page_1'] = 'view.example_view.page_1'; + $this->state->expects($this->exactly(2)) + ->method('get') + ->with('views.view_route_names') + ->will($this->returnValue($view_route_names)); + + // Mock the route provider. + $route_collection = new RouteCollection(); + $route_collection->add('test_route', new Route('/path')); + $this->routeProvider->expects($this->any()) + ->method('getRoutesByPattern') + ->with('/path') + ->will($this->returnValue($route_collection)); + + // Setup the existing local task of the test_route. + $definitions['test_route_tab'] = $other_tab = array( + 'route_name' => 'test_route', + 'title' => 'Test route', + 'tab_root_id' => 'test_route_tab', + ); + + $definitions += $this->localTaskDerivative->getDerivativeDefinitions($this->baseDefinition); + + // Setup the prefix of the derivative. + $definitions['views_view:view.example_view.page_1'] = $definitions['view.example_view.page_1']; + unset($definitions['view.example_view.page_1']); + $this->localTaskDerivative->alterLocalTasks($definitions); + + $plugin = $definitions['views_view:view.example_view.page_1']; + $this->assertCount(2, $definitions); + + // Ensure the other local task was not changed. + $this->assertEquals($other_tab, $definitions['test_route_tab']); + + $this->assertEquals('view.example_view.page_1', $plugin['route_name']); + $this->assertEquals(12, $plugin['weight']); + $this->assertEquals('Example title', $plugin['title']); + $this->assertEquals($this->baseDefinition['class'], $plugin['class']); + $this->assertEquals('test_route_tab', $plugin['tab_root_id']); + } + +} + +/** + * Replaces the applicable views call for easier testability. + */ +class TestViewsLocalTask extends ViewsLocalTask { + + /** + * Set applicable views result. + */ + public function setApplicableMenuViews($result) { + $this->result = $result; + } + + protected function getApplicableMenuViews() { + return $this->result; + } + +} diff --git a/core/modules/views/views.local_tasks.yml b/core/modules/views/views.local_tasks.yml new file mode 100644 index 0000000..624aeb7 --- /dev/null +++ b/core/modules/views/views.local_tasks.yml @@ -0,0 +1,3 @@ +views_view: + class: \Drupal\views\Plugin\Menu\LocalTask\ViewsLocalTask + derivative: \Drupal\views\Plugin\Derivative\ViewsLocalTask diff --git a/core/modules/views/views.module b/core/modules/views/views.module index f8f9ff5..d25ae54 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -12,6 +12,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Database\Query\AlterableInterface; use Drupal\Core\Language\Language; +use Drupal\views\Plugin\Derivative\ViewsLocalTask; use Drupal\views\ViewExecutable; use Drupal\Component\Plugin\Exception\PluginException; use Drupal\views\Entity\View; @@ -1729,3 +1730,11 @@ function views_cache_get($cid, $use_language = FALSE) { return cache('views_info')->get($cid); } +/** + * Implements hook_local_tasks_alter(). + */ +function views_local_tasks_alter(&$local_tasks) { + $container = \Drupal::getContainer(); + $local_task = ViewsLocalTask::create($container, 'views_view'); + $local_task->alterLocalTasks($local_tasks); +}