diff --git a/core/core.services.yml b/core/core.services.yml index 12f3a3c..22e60be 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -197,6 +197,9 @@ services: class: Drupal\Core\Menu\LocalTaskManager arguments: ['@controller_resolver', '@request', '@router.route_provider', '@module_handler', '@cache.cache', '@language_manager', '@access_manager', '@current_user'] scope: request + plugin.manager.menu.contextual_link: + class: Drupal\Core\Menu\ContextualLinkManager + arguments: ['@controller_resolver', '@module_handler', '@cache.cache', '@language_manager', '@access_manager'] request: class: Symfony\Component\HttpFoundation\Request # @TODO the synthetic setting must be uncommented whenever drupal_session_initialize() diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 78d6367..417d7ef 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -251,11 +251,6 @@ const MENU_CONTEXT_PAGE = 0x0001; /** - * Internal menu flag: Local task should be displayed inline. - */ -const MENU_CONTEXT_INLINE = 0x0002; - -/** * @} End of "defgroup menu_context_types". */ @@ -2055,7 +2050,6 @@ function _menu_get_legacy_tasks($router_item, &$data, &$root_path) { $result = db_select('menu_router', NULL, array('fetch' => PDO::FETCH_ASSOC)) ->fields('menu_router') ->condition('tab_root', $router_item['tab_root']) - ->condition('context', MENU_CONTEXT_INLINE, '<>') ->orderBy('weight') ->orderBy('title') ->execute(); @@ -2224,114 +2218,6 @@ function _menu_get_legacy_tasks($router_item, &$data, &$root_path) { } /** - * Retrieves contextual links for a path based on registered local tasks. - * - * This leverages the menu system to retrieve the first layer of registered - * local tasks for a given system path. All local tasks of the tab type - * MENU_CONTEXT_INLINE are taken into account. - * - * For example, when considering the following registered local tasks: - * - node/%node/view (default local task) with no 'context' defined - * - node/%node/edit with context: MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE - * - node/%node/revisions with context: MENU_CONTEXT_PAGE - * - node/%node/report-as-spam with context: MENU_CONTEXT_INLINE - * - * If the path "node/123" is passed to this function, then it will return the - * links for 'edit' and 'report-as-spam'. - * - * @param $module - * The name of the implementing module. This is used to prefix the key for - * each contextual link, which is transformed into a CSS class during - * rendering by theme_links(). For example, if $module is 'block' and the - * retrieved local task path argument is 'edit', then the resulting CSS class - * will be 'block-edit'. - * @param $parent_path - * The static menu router path of the object to retrieve local tasks for, for - * example 'node' or 'admin/structure/block/manage'. - * @param $args - * A list of dynamic path arguments to append to $parent_path to form the - * fully-qualified menu router path; for example, array(123) for a certain - * node or array('system', 'tools') for a certain block. - * - * @return - * A list of menu router items that are local tasks for the passed-in path. - * - * @see contextual_links_preprocess() - * @see hook_menu() - */ -function menu_contextual_links($module, $parent_path, $args) { - static $path_empty = array(); - - $links = array(); - // Performance: In case a previous invocation for the same parent path did not - // return any links, we immediately return here. - if (isset($path_empty[$parent_path]) && strpos($parent_path, '%') !== FALSE) { - return $links; - } - // Construct the item-specific parent path. - $path = $parent_path . '/' . implode('/', $args); - - // Get the router item for the given parent link path. - $router_item = menu_get_item($path); - if (!$router_item || !$router_item['access']) { - $path_empty[$parent_path] = TRUE; - return $links; - } - $data = &drupal_static(__FUNCTION__, array()); - $root_path = $router_item['path']; - - // Performance: For a single, normalized path (such as 'node/%') we only query - // available tasks once per request. - if (!isset($data[$root_path])) { - // Get all contextual links that are direct children of the router item and - // not of the tab type 'view'. - $data[$root_path] = db_select('menu_router', 'm') - ->fields('m') - ->condition('tab_parent', $router_item['tab_root']) - ->condition('context', MENU_CONTEXT_NONE, '<>') - ->condition('context', MENU_CONTEXT_PAGE, '<>') - ->orderBy('weight') - ->orderBy('title') - ->execute() - ->fetchAllAssoc('path', PDO::FETCH_ASSOC); - } - $parent_length = drupal_strlen($root_path) + 1; - $map = $router_item['original_map']; - foreach ($data[$root_path] as $item) { - // Extract the actual "task" string from the path argument. - $key = drupal_substr($item['path'], $parent_length); - - // Denormalize and translate the contextual link. - _menu_translate($item, $map, TRUE); - if (!$item['access']) { - continue; - } - - // If this item is a default local task, rewrite the href to link to its - // parent item. - if ($item['type'] == MENU_DEFAULT_LOCAL_TASK) { - $item['href'] = $item['tab_parent_href']; - } - - // All contextual links are keyed by the actual "task" path argument, - // prefixed with the name of the implementing module. - $links[$module . '-' . $key] = $item; - } - - // Allow modules to alter contextual links. - drupal_alter('menu_contextual_links', $links, $router_item, $root_path); - - // Performance: If the current user does not have access to any links for this - // router path and no other module added further links, we assign FALSE here - // to skip the entire process the next time the same router path is requested. - if (empty($links)) { - $path_empty[$parent_path] = TRUE; - } - - return $links; -} - -/** * Returns the rendered local tasks at the top level. */ function menu_primary_local_tasks() { diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 9a0d713..45c794a 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1693,8 +1693,12 @@ function theme_links($variables) { $class[] = 'last'; } + $link += array( + 'href' => NULL, + ); + // Handle links. - if (isset($link['href'])) { + if (isset($link['href']) || isset($link['route_name'])) { $is_current_path = ($link['href'] == current_path() || ($link['href'] == '' && drupal_is_front_page())); $is_current_language = (empty($link['language']) || $link['language']->id == $language_url->id); if ($is_current_path && $is_current_language) { @@ -1715,8 +1719,17 @@ function theme_links($variables) { $item = drupal_render($link_element); } else { - // Pass in $link as $options, they share the same keys. - $item = l($link['title'], $link['href'], $link); + // @todo theme_links() should *really* use the same parameters as l(), + // and just take an array of '#type' => 'link' elements, see + // https://drupal.org/node/2102777. + // Pass in $link as $options, as they share the same keys. + if (isset($link['href'])) { + $item = l($link['title'], $link['href'], $link); + } + else { + $link += array('route_parameters' => array()); + $item = \Drupal::l($link['title'], $link['route_name'], $link['route_parameters'], $link); + } } } // Handle title-only text items. diff --git a/core/lib/Drupal/Core/Menu/ContextualLinkDefault.php b/core/lib/Drupal/Core/Menu/ContextualLinkDefault.php new file mode 100644 index 0000000..79c451f --- /dev/null +++ b/core/lib/Drupal/Core/Menu/ContextualLinkDefault.php @@ -0,0 +1,56 @@ +pluginDefinition['title_context'])) { + $options['context'] = $this->pluginDefinition['title_context']; + } + return $this->t($this->pluginDefinition['title'], array(), $options); + } + + /** + * {@inheritdoc} + */ + public function getRouteName() { + return $this->pluginDefinition['route_name']; + } + + /** + * {@inheritdoc} + */ + public function getGroup() { + return $this->pluginDefinition['group']; + } + + /** + * {@inheritdoc} + */ + public function getOptions() { + return $this->pluginDefinition['options']; + } + + /** + * {@inheritdoc} + */ + public function getWeight() { + return $this->pluginDefinition['weight']; + } + +} diff --git a/core/lib/Drupal/Core/Menu/ContextualLinkInterface.php b/core/lib/Drupal/Core/Menu/ContextualLinkInterface.php new file mode 100644 index 0000000..e5eecec --- /dev/null +++ b/core/lib/Drupal/Core/Menu/ContextualLinkInterface.php @@ -0,0 +1,69 @@ + '', + // (required) The contextual links group. + 'group' => '', + // The static title text for the link. + 'title' => '', + // The default link options. + 'options' => array(), + // The weight of the link. + 'weight' => NULL, + // Default class for contextual link implementations. + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + // The plugin id. Set by the plugin system based on the top-level YAML key. + 'id' => '', + ); + + /** + * A controller resolver object. + * + * @var \Symfony\Component\HttpKernel\Controller\ControllerResolverInterface + */ + protected $controllerResolver; + + /** + * The access manager. + * + * @var \Drupal\Core\Access\AccessManager + */ + protected $accessManager; + + /** + * A static cache of all the contextual link plugins by group name. + * + * @var array + */ + protected $pluginsByGroup; + + /** + * Constructs a new ContextualLinkManager instance. + * + * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver + * The controller resolver. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * The cache backend. + * @param \Drupal\Core\Language\LanguageManager $language_manager + * The language manager. + * @param \Drupal\Core\Access\AccessManager $access_manager + * The access manager. + */ + public function __construct(ControllerResolverInterface $controller_resolver, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, LanguageManager $language_manager, AccessManager $access_manager) { + $this->discovery = new YamlDiscovery('contextual_links', $module_handler->getModuleDirectories()); + $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery); + $this->factory = new ContainerFactory($this); + + $this->controllerResolver = $controller_resolver; + $this->accessManager = $access_manager; + $this->alterInfo($module_handler, 'contextual_links_plugins'); + $this->setCacheBackend($cache_backend, $language_manager, 'contextual_links_plugins'); + } + + /** + * {@inheritdoc} + */ + public function processDefinition(&$definition, $plugin_id) { + parent::processDefinition($definition, $plugin_id); + + // If there is no route name, this is a broken definition. + if (empty($definition['route_name'])) { + throw new PluginException(sprintf('Contextual link plugin (%s) definition must include "route_name".', $plugin_id)); + } + // If there is no group name, this is a broken definition. + if (empty($definition['group'])) { + throw new PluginException(sprintf('Contextual link plugin (%s) definition must include "group".', $plugin_id)); + } + } + + /** + * {@inheritdoc} + */ + public function getContextualLinkPluginsByGroup($group_name) { + if (isset($this->pluginsByGroup[$group_name])) { + $contextual_links = $this->pluginsByGroup[$group_name]; + } + elseif ($cache = $this->cacheBackend->get($this->cacheKey . ':' . $group_name)) { + $contextual_links = $cache->data; + $this->pluginsByGroup[$group_name] = $contextual_links; + } + else { + $contextual_links = array(); + foreach ($this->getDefinitions() as $plugin_id => $plugin_definition) { + if ($plugin_definition['group'] == $group_name) { + $contextual_links[$plugin_id] = $plugin_definition; + } + } + $this->cacheBackend->set($this->cacheKey . ':' . $group_name, $contextual_links); + $this->pluginsByGroup[$group_name] = $contextual_links; + } + return $contextual_links; + } + + /** + * {@inheritdoc} + */ + public function getContextualLinksArrayByGroup($group_name, array $route_parameters, array $metadata = array()) { + $links = array(); + foreach ($this->getContextualLinkPluginsByGroup($group_name) as $plugin_id => $plugin_definition) { + /** @var $plugin \Drupal\Core\Menu\ContextualLinkInterface */ + $plugin = $this->createInstance($plugin_id); + $route_name = $plugin->getRouteName(); + + // Check access. + if (!$this->accessManager->checkNamedRoute($route_name, $route_parameters)) { + continue; + } + + $links[$plugin_id] = array( + 'route_name' => $route_name, + 'route_parameters' => $route_parameters, + 'title' => $plugin->getTitle(), + 'weight' => $plugin->getWeight(), + 'localized_options' => $plugin->getOptions(), + 'metadata' => $metadata, + ); + } + + $this->moduleHandler->alter('contextual_links', $links, $group_name, $route_parameters); + + return $links; + } + +} diff --git a/core/lib/Drupal/Core/Menu/ContextualLinkManagerInterface.php b/core/lib/Drupal/Core/Menu/ContextualLinkManagerInterface.php new file mode 100644 index 0000000..801990b --- /dev/null +++ b/core/lib/Drupal/Core/Menu/ContextualLinkManagerInterface.php @@ -0,0 +1,51 @@ + 'Configure block', 'route_name' => 'block.admin_edit', ); - $items['admin/structure/block/manage/%block/configure'] = array( - 'title' => 'Configure block', - 'type' => MENU_DEFAULT_LOCAL_TASK, - 'context' => MENU_CONTEXT_INLINE, - ); $items['admin/structure/block/add/%/%'] = array( 'title' => 'Place block', 'type' => MENU_VISIBLE_IN_BREADCRUMB, @@ -268,7 +263,9 @@ function _block_get_renderable_region($list = array()) { // to perform contextual actions on the help block, and the links needlessly // draw attention on it. if (isset($build[$key]) && !in_array($block->get('plugin'), array('system_help_block', 'system_main_block'))) { - $build[$key]['#contextual_links']['block'] = array('admin/structure/block/manage', array($key)); + $build[$key]['#contextual_links']['block'] = array( + 'route_parameters' => array('block' => $key), + ); // If there are any nested contextual links, move them to the top level. if (isset($build[$key]['content']['#contextual_links'])) { diff --git a/core/modules/block/custom_block/custom_block.contextual_links.yml b/core/modules/block/custom_block/custom_block.contextual_links.yml new file mode 100644 index 0000000..acd117f --- /dev/null +++ b/core/modules/block/custom_block/custom_block.contextual_links.yml @@ -0,0 +1,10 @@ +custom_block.block_edit: + title: 'Edit' + group: custom_block + route_name: 'custom_block.edit' + +custom_block.block_delete: + title: 'Delete' + group: custom_block + route_name: 'custom_block.delete' + weight: 1 diff --git a/core/modules/block/custom_block/custom_block.module b/core/modules/block/custom_block/custom_block.module index 2e44de6..4cf2e68 100644 --- a/core/modules/block/custom_block/custom_block.module +++ b/core/modules/block/custom_block/custom_block.module @@ -80,24 +80,6 @@ function custom_block_menu() { 'route_name' => 'custom_block.add_page', ); - // There has to be a base-item in order for contextual links to work. - $items['block/%custom_block'] = array( - 'title' => 'Edit', - 'route_name' => 'custom_block.edit', - ); - $items['block/%custom_block/edit'] = array( - 'title' => 'Edit', - 'weight' => 0, - 'type' => MENU_DEFAULT_LOCAL_TASK, - 'context' => MENU_CONTEXT_INLINE, - ); - $items['block/%custom_block/delete'] = array( - 'title' => 'Delete', - 'weight' => 1, - 'type' => MENU_LOCAL_TASK, - 'context' => MENU_CONTEXT_INLINE, - 'route_name' => 'custom_block.delete', - ); return $items; } diff --git a/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockViewBuilder.php b/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockViewBuilder.php index 6be784e..1477345 100644 --- a/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockViewBuilder.php +++ b/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockViewBuilder.php @@ -23,7 +23,9 @@ protected function alterBuild(array &$build, EntityInterface $entity, EntityDisp parent::alterBuild($build, $entity, $display, $view_mode, $langcode); // Add contextual links for this custom block. if (!empty($entity->id->value) && $view_mode == 'full') { - $build['#contextual_links']['custom_block'] = array('block', array($entity->id())); + $build['#contextual_links']['custom_block'] = array( + 'route_parameters' => array('custom_block' => $entity->id()), + ); } } diff --git a/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php b/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php index d759c57..9a4680c 100644 --- a/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php +++ b/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php @@ -230,7 +230,7 @@ public function testBlockContextualLinks() { $block = $this->drupalPlaceBlock('views_block:test_view_block-block_1'); $this->drupalGet('test-page'); - $id = 'block:admin/structure/block/manage:' . $block->id() . ':|views_ui:admin/structure/views/view:test_view_block:location=block&name=test_view_block&display_id=block_1'; + $id = 'block:block=' . $block->id() . ':|views_ui_edit:view=test_view_block:location=block&name=test_view_block&display_id=block_1'; // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder() $this->assertRaw('
', format_string('Contextual link placeholder with id @id exists.', array('@id' => $id))); @@ -240,7 +240,7 @@ public function testBlockContextualLinks() { $response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page'))); $this->assertResponse(200); $json = drupal_json_decode($response); - $this->assertIdentical($json[$id], ''); + $this->assertIdentical($json[$id], ''); } } diff --git a/core/modules/content_translation/content_translation.contextual_links.yml b/core/modules/content_translation/content_translation.contextual_links.yml new file mode 100644 index 0000000..2d2818a --- /dev/null +++ b/core/modules/content_translation/content_translation.contextual_links.yml @@ -0,0 +1,3 @@ +content_translation.contextual_links: + derivative: 'Drupal\content_translation\Plugin\Derivative\ContentTranslationContextualLinks' + weight: 2 diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index b31b8d8..83b8a3b 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -180,7 +180,7 @@ function content_translation_menu() { 'title' => 'Translate', 'route_name' => "content_translation.translation_overview_$entity_type", 'type' => MENU_LOCAL_TASK, - 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, + 'context' => MENU_CONTEXT_PAGE, 'weight' => 2, ) + $item; diff --git a/core/modules/content_translation/lib/Drupal/content_translation/Plugin/Derivative/ContentTranslationContextualLinks.php b/core/modules/content_translation/lib/Drupal/content_translation/Plugin/Derivative/ContentTranslationContextualLinks.php new file mode 100644 index 0000000..c6cc8ea --- /dev/null +++ b/core/modules/content_translation/lib/Drupal/content_translation/Plugin/Derivative/ContentTranslationContextualLinks.php @@ -0,0 +1,64 @@ +entityManager = $entity_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('entity.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions(array $base_plugin_definition) { + // Create contextual links for all possible entity types. + foreach ($this->entityManager->getDefinitions() as $entity_type => $entity_info) { + if ($entity_info['translatable'] && isset($entity_info['translation'])) { + $this->derivatives[$entity_type]['title'] = t('Translate'); + $this->derivatives[$entity_type]['route_name'] = "content_translation.translation_overview_$entity_type"; + $this->derivatives[$entity_type]['group'] = $entity_type; + } + } + return parent::getDerivativeDefinitions($base_plugin_definition); + } + +} diff --git a/core/modules/contextual/contextual.api.php b/core/modules/contextual/contextual.api.php index e8f33ee..d972045 100644 --- a/core/modules/contextual/contextual.api.php +++ b/core/modules/contextual/contextual.api.php @@ -22,10 +22,12 @@ * A renderable array representing the contextual links. * @param $items * An associative array containing the original contextual link items, as - * generated by menu_contextual_links(), which were used to build - * $element['#links']. + * generated by + * \Drupal\Core\Menu\ContextualLinkManagerInterface::getContextualLinksArrayByGroup(), + * which were used to build $element['#links']. * - * @see hook_menu_contextual_links_alter() + * @see hook_contextual_links_alter() + * @see hook_contextual_links_plugins_alter() * @see contextual_pre_render_links() * @see contextual_element_info() */ diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module index 529fa5c..e405f20 100644 --- a/core/modules/contextual/contextual.module +++ b/core/modules/contextual/contextual.module @@ -1,4 +1,5 @@ array( - * 'block' => array('admin/structure/block/manage', array('system', 'menu-tools')), - * 'menu' => array('admin/structure/menu/manage', array('tools')), + * 'block' => array( + * 'route_parameters' => array('block' => 'system.menu-tools'), + * ), + * 'menu' => array( + * 'route_parameters' => array('menu' => 'tools'), + * ), * )) * @endcode * * @return * A renderable array representing contextual links. * - * @see menu_contextual_links() * @see contextual_element_info() */ function contextual_pre_render_links($element) { // Retrieve contextual menu links. $items = array(); - foreach ($element['#contextual_links'] as $module => $args) { - $items += menu_contextual_links($module, $args[0], $args[1]); + + /** @var $contextual_links_manager \Drupal\Core\Menu\ContextualLinkManager */ + $contextual_links_manager = \Drupal::service('plugin.manager.menu.contextual_link'); + foreach ($element['#contextual_links'] as $group => $args) { + $args += array( + 'route_parameters' => array(), + 'metadata' => array(), + ); + $items += $contextual_links_manager->getContextualLinksArrayByGroup($group, $args['route_parameters'], $args['metadata']); } // Transform contextual links into parameters suitable for theme_links(). @@ -266,9 +280,9 @@ function contextual_pre_render_links($element) { $class = drupal_html_class($class); $links[$class] = array( 'title' => $item['title'], - 'href' => $item['href'], + 'route_name' => isset($item['route_name']) ? $item['route_name'] : '', + 'route_parameters' => isset($item['route_parameters']) ? $item['route_parameters'] : array(), ); - // @todo theme_links() should *really* use the same parameters as l(). $item['localized_options'] += array('query' => array()); $item['localized_options']['query'] += drupal_get_destination(); $links[$class] += $item['localized_options']; @@ -293,7 +307,7 @@ function contextual_pre_render_links($element) { */ function contextual_contextual_links_view_alter(&$element, $items) { if (isset($element['#contextual_links']['contextual'])) { - $encoded_links = $element['#contextual_links']['contextual'][2]['contextual-views-field-links']; + $encoded_links = $element['#contextual_links']['contextual']['metadata']['contextual-views-field-links']; $element['#links'] = drupal_json_decode(rawurldecode($encoded_links)); } } @@ -302,15 +316,14 @@ function contextual_contextual_links_view_alter(&$element, $items) { * Serializes #contextual_links property value array to a string. * * Examples: - * - node:node:1: - * - views_ui:admin/structure/views/view:frontpage:location=page&view_name=frontpage&view_display_id=page_1 - * - menu:admin/structure/menu/manage:tools:|block:admin/structure/block/manage:bartik.tools: + * - node:node=1: + * - views_ui_edit:view=frontpage:location=page&view_name=frontpage&view_display_id=page_1 + * - menu:menu=tools:|block:block=bartik.tools: * * So, expressed in a pattern: - * ::: + * :: * - * The (dynamic) path args are joined with slashes. The options are encoded as a - * query string. + * The route parameters and options are encoded as query strings. * * @param array $contextual_links * The $element['#contextual_links'] value for some render element. @@ -320,18 +333,13 @@ function contextual_contextual_links_view_alter(&$element, $items) { * use in a data- attribute. */ function _contextual_links_to_id($contextual_links) { - $id = ''; - foreach ($contextual_links as $module => $args) { - $parent_path = $args[0]; - $path_args = implode('/', $args[1]); - $metadata = drupal_http_build_query((isset($args[2])) ? $args[2] : array()); - - if (drupal_strlen($id) > 0) { - $id .= '|'; - } - $id .= $module . ':' . $parent_path . ':' . $path_args . ':' . $metadata; + $ids = array(); + foreach ($contextual_links as $group => $args) { + $route_parameters = Url::buildQuery($args['route_parameters']); + $metadata = Url::buildQuery((isset($args['metadata'])) ? $args['metadata'] : array()); + $ids[] = "{$group}:{$route_parameters}:{$metadata}"; } - return $id; + return implode('|', $ids); } /** @@ -349,11 +357,14 @@ function _contextual_id_to_links($id) { $contextual_links = array(); $contexts = explode('|', $id); foreach ($contexts as $context) { - list($module, $parent_path, $path_args, $metadata_raw) = explode(':', $context); - $path_args = explode('/', $path_args); + list($group, $route_parameters_raw, $metadata_raw) = explode(':', $context); + parse_str($route_parameters_raw, $route_parameters); $metadata = array(); parse_str($metadata_raw, $metadata); - $contextual_links[$module] = array($parent_path, $path_args, $metadata); + $contextual_links[$group] = array( + 'route_parameters' => $route_parameters, + 'metadata' => $metadata, + ); } return $contextual_links; } diff --git a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php index 638ce15..4cbbca3 100644 --- a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php +++ b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php @@ -60,10 +60,10 @@ function testDifferentPermissions() { // Now, on the front page, all article nodes should have contextual links // placeholders, as should the view that contains them. $ids = array( - 'node:node:' . $node1->id() . ':', - 'node:node:' . $node2->id() . ':', - 'node:node:' . $node3->id() . ':', - 'views_ui:admin/structure/views/view:frontpage:location=page&name=frontpage&display_id=page_1', + 'node:node=' . $node1->id() . ':', + 'node:node=' . $node2->id() . ':', + 'node:node=' . $node3->id() . ':', + 'views_ui_edit:view=frontpage:location=page&name=frontpage&display_id=page_1', ); // Editor user: can access contextual links and can edit articles. @@ -77,9 +77,9 @@ function testDifferentPermissions() { $response = $this->renderContextualLinks($ids, 'node'); $this->assertResponse(200); $json = drupal_json_decode($response); - $this->assertIdentical($json[$ids[0]], ''); + $this->assertIdentical($json[$ids[0]], ''); $this->assertIdentical($json[$ids[1]], ''); - $this->assertIdentical($json[$ids[2]], ''); + $this->assertIdentical($json[$ids[2]], ''); $this->assertIdentical($json[$ids[3]], ''); // Authenticated user: can access contextual links, cannot edit articles. diff --git a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualUnitTest.php b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualUnitTest.php index d2018ac..738f047 100644 --- a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualUnitTest.php +++ b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualUnitTest.php @@ -26,75 +26,83 @@ public static function getInfo() { */ function _contextual_links_id_testcases() { // Test branch conditions: - // - one module. + // - one group. // - one dynamic path argument. // - no metadata. $tests[] = array( 'links' => array( 'node' => array( - 'node', - array('14031991'), - array() + 'route_parameters' => array( + 'node' => '14031991', + ), + 'metadata' => array() ), ), - 'id' => 'node:node:14031991:', + 'id' => 'node:node=14031991:', ); // Test branch conditions: - // - one module. + // - one group. // - multiple dynamic path arguments. // - no metadata. $tests[] = array( 'links' => array( 'foo' => array( - 'baz/in/ga', - array('bar', 'baz', 'qux'), - array() + 'route_parameters'=> array( + 'bar', + 'key' => 'baz', + 'qux', + ), + 'metadata' => array(), ), ), - 'id' => 'foo:baz/in/ga:bar/baz/qux:', + 'id' => 'foo:0=bar&key=baz&1=qux:', ); // Test branch conditions: - // - one module. + // - one group. // - one dynamic path argument. // - metadata. $tests[] = array( 'links' => array( - 'views_ui' => array( - 'admin/structure/views/view', - array('frontpage'), - array( + 'views_ui_edit' => array( + 'route_parameters' => array( + 'view' => 'frontpage' + ), + 'metadata' => array( 'location' => 'page', 'display' => 'page_1', - ) + ), ), ), - 'id' => 'views_ui:admin/structure/views/view:frontpage:location=page&display=page_1', + 'id' => 'views_ui_edit:view=frontpage:location=page&display=page_1', ); // Test branch conditions: - // - multiple modules. + // - multiple groups. // - multiple dynamic path arguments. $tests[] = array( 'links' => array( 'node' => array( - 'node', - array('14031991'), - array() + 'route_parameters' => array( + 'node' => '14031991', + ), + 'metadata' => array(), ), 'foo' => array( - 'baz/in/ga', - array('bar', 'baz', 'qux'), - array() + 'route_parameters' => array( + 'bar', + 'key' => 'baz', + 'qux', + ), + 'metadata' => array(), ), 'edge' => array( - 'edge', - array('20011988'), - array() + 'route_parameters' => array('20011988'), + 'metadata' => array(), ), ), - 'id' => 'node:node:14031991:|foo:baz/in/ga:bar/baz/qux:|edge:edge:20011988:', + 'id' => 'node:node=14031991:|foo:0=bar&key=baz&1=qux:|edge:0=20011988:', ); return $tests; diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php index 92c23cd..f4f9e53 100644 --- a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php +++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php @@ -408,7 +408,7 @@ public function testBlockContextualLinks() { $block = $this->drupalPlaceBlock('system_menu_block:tools', array('label' => 'Tools', 'module' => 'system')); $this->drupalGet('test-page'); - $id = 'block:admin/structure/block/manage:' . $block->id() . ':|menu:admin/structure/menu/manage:tools:'; + $id = 'block:block=' . $block->id() . ':|menu:menu=tools:'; // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder() $this->assertRaw('
', format_string('Contextual link placeholder with id @id exists.', array('@id' => $id))); diff --git a/core/modules/menu/menu.contextual_links.yml b/core/modules/menu/menu.contextual_links.yml new file mode 100644 index 0000000..5af427e --- /dev/null +++ b/core/modules/menu/menu.contextual_links.yml @@ -0,0 +1,4 @@ +menu_edit: + title: 'Edit menu' + route_name: 'menu.menu_edit' + group: menu diff --git a/core/modules/menu/menu.module b/core/modules/menu/menu.module index fe52570..c48bb46 100644 --- a/core/modules/menu/menu.module +++ b/core/modules/menu/menu.module @@ -94,7 +94,7 @@ function menu_menu() { $items['admin/structure/menu/manage/%menu/edit'] = array( 'title' => 'Edit menu', 'type' => MENU_DEFAULT_LOCAL_TASK, - 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, + 'context' => MENU_CONTEXT_PAGE, ); $items['admin/structure/menu/item/%menu_link/edit'] = array( 'title' => 'Edit menu link', @@ -338,7 +338,9 @@ function menu_block_view_system_menu_block_alter(array &$build, BlockPluginInter list(, $menu_name) = explode(':', $block->getPluginId()); if (isset($menus[$menu_name]) && isset($build['content'])) { foreach (element_children($build['content']) as $key) { - $build['content']['#contextual_links']['menu'] = array('admin/structure/menu/manage', array($build['content'][$key]['#original_link']['menu_name'])); + $build['content']['#contextual_links']['menu'] = array( + 'route_parameters' => array('menu' => $build['content'][$key]['#original_link']['menu_name']), + ); } } } diff --git a/core/modules/node/lib/Drupal/node/NodeViewBuilder.php b/core/modules/node/lib/Drupal/node/NodeViewBuilder.php index 8551158..92be3ad 100644 --- a/core/modules/node/lib/Drupal/node/NodeViewBuilder.php +++ b/core/modules/node/lib/Drupal/node/NodeViewBuilder.php @@ -83,7 +83,9 @@ public function buildContent(array $entities, array $displays, $view_mode, $lang protected function alterBuild(array &$build, EntityInterface $entity, EntityDisplay $display, $view_mode, $langcode = NULL) { parent::alterBuild($build, $entity, $display, $view_mode, $langcode); if ($entity->id()) { - $build['#contextual_links']['node'] = array('node', array($entity->id())); + $build['#contextual_links']['node'] = array( + 'route_parameters' =>array('node' => $entity->id()), + ); } // The node 'submitted' info is not rendered in a standard way (renderable diff --git a/core/modules/node/lib/Drupal/node/Tests/Views/NodeContextualLinksTest.php b/core/modules/node/lib/Drupal/node/Tests/Views/NodeContextualLinksTest.php index cf24cba..ff390e6 100644 --- a/core/modules/node/lib/Drupal/node/Tests/Views/NodeContextualLinksTest.php +++ b/core/modules/node/lib/Drupal/node/Tests/Views/NodeContextualLinksTest.php @@ -47,13 +47,15 @@ public function testNodeContextualLinks() { $user = $this->drupalCreateUser(array('administer nodes', 'access contextual links')); $this->drupalLogin($user); - $response = $this->renderContextualLinks(array('node:node:1:'), 'node'); + $response = $this->renderContextualLinks(array('node:node=1:'), 'node'); $this->assertResponse(200); $json = Json::decode($response); - $this->drupalSetContent($json['node:node:1:']); + $this->drupalSetContent($json['node:node=1:']); - $this->assertLinkByHref('node/1/contextual-links', 0, 'The contextual link to the view was found.'); - $this->assertLink('Test contextual link', 0, 'The contextual link to the view was found.'); + // @todo Add these back when the functionality for making Views displays + // appear in contextual links is working again. + // $this->assertLinkByHref('node/1/contextual-links', 0, 'The contextual link to the view was found.'); + // $this->assertLink('Test contextual link', 0, 'The contextual link to the view was found.'); } /** diff --git a/core/modules/node/node.contextual_links.yml b/core/modules/node/node.contextual_links.yml new file mode 100644 index 0000000..91159bd --- /dev/null +++ b/core/modules/node/node.contextual_links.yml @@ -0,0 +1,10 @@ +node.page_edit: + route_name: node.page_edit + group: node + title: Edit + +node.delete_confirm: + route_name: node.delete_confirm + group: node + title: Delete + weight: 10 diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 5c10fbb..440d403 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -984,19 +984,6 @@ function node_menu() { // overridden by a menu link. 'route_name' => 'node.view', ); - $items['node/%node/edit'] = array( - 'title' => 'Edit', - 'route_name' => 'node.page_edit', - 'type' => MENU_LOCAL_TASK, - 'context' => MENU_CONTEXT_INLINE, - ); - $items['node/%node/delete'] = array( - 'title' => 'Delete', - 'route_name' => 'node.delete_confirm', - 'weight' => 10, - 'type' => MENU_LOCAL_TASK, - 'context' => MENU_CONTEXT_INLINE, - ); $items['node/%node/revisions/%node_revision/view'] = array( 'title' => 'Revisions', 'route_name' => 'node.revision_show', diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php index f341681..4b69e98 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php @@ -296,10 +296,6 @@ protected function doTestMenuHidden() { $depth = $parent['depth'] + 1; $plid = $parent['mlid']; - $link = $links['menu-test/hidden/block/manage/%/%/configure']; - $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); - $link = $links['menu-test/hidden/block/manage/%/%/delete']; $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index c6f7d2c..69e872c 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -677,48 +677,62 @@ function hook_local_task_alter(&$local_tasks) { /** * Alter contextual links before they are rendered. * - * This hook is invoked by menu_contextual_links(). The system-determined - * contextual links are passed in by reference. Additional links may be added - * or existing links can be altered. + * This hook is invoked by + * \Drupal\Core\Menu\ContextualLinkManager::getContextualLinkPluginsByGroup(). + * The system-determined contextual links are passed in by reference. Additional + * links may be added and existing links can be altered. * - * Each contextual link must at least contain: + * Each contextual link contains the following entries: * - title: The localized title of the link. - * - href: The system path to link to. + * - route_name: The route name of the link. + * - route_parameters: The route parameters of the link. * - localized_options: An array of options to pass to url(). + * - (optional) weight: The weight of the link, which is used to sort the links. * - * @param $links - * An associative array containing contextual links for the given $root_path, + * + * @param array $links + * An associative array containing contextual links for the given $group, * as described above. The array keys are used to build CSS class names for * contextual links and must therefore be unique for each set of contextual * links. - * @param $router_item - * The menu router item belonging to the $root_path being requested. - * @param $root_path - * The (parent) path that has been requested to build contextual links for. - * This is a normalized path, which means that an originally passed path of - * 'node/123' became 'node/%'. - * - * @see hook_contextual_links_view_alter() - * @see menu_contextual_links() - * @see hook_menu() - * @see contextual_preprocess() - */ -function hook_menu_contextual_links_alter(&$links, $router_item, $root_path) { - // Add a link to all contextual links for nodes. - if ($root_path == 'node/%') { - $links['foo'] = array( - 'title' => t('Do fu'), - 'href' => 'foo/do', - 'localized_options' => array( - 'query' => array( - 'foo' => 'bar', - ), - ), - ); + * @param string $group + * The group of contextual links being rendered. + * @param array $route_parameters. + * The route parameters passed to each route_name of the contextual links. + * For example: + * @code + * array('node' => $node->id()) + * @endcode + * + * @see \Drupal\Core\Menu\ContextualLinkManager + */ +function hook_contextual_links_alter(array &$links, $group, array $route_parameters) { + if ($group == 'menu') { + // Dynamically use the menu name for the title of the menu_edit contextual + // link. + $menu = \Drupal::entityManager()->getStorageController('menu')->load($route_parameters['menu']); + $links['menu_edit']['title'] = t('Edit menu: !label', array('!label' => $menu->label())); } } /** + * Alter the plugin definition of contextual links. + * + * @param array $contextual_links + * An array of contextual_links plugin definitions, keyed by contextual link + * ID. Each entry contains the following keys: + * - title: The displayed title of the link + * - route_name: The route_name of the contextual link to be displayed + * - group: The group under which the contextual links should be added to. + * Possible values are e.g. 'node' or 'menu'. + * + * @see \Drupal\Core\Menu\ContextualLinkManager + */ +function hook_contextual_links_plugins_alter(array &$contextual_links) { + $contextual_links['menu_edit']['title'] = 'Edit the menu'; +} + +/** * Perform alterations before a page is rendered. * * Use this hook when you want to remove or alter elements at the page diff --git a/core/modules/system/tests/modules/menu_test/menu_test.contextual_links.yml b/core/modules/system/tests/modules/menu_test/menu_test.contextual_links.yml new file mode 100644 index 0000000..b2d8e47 --- /dev/null +++ b/core/modules/system/tests/modules/menu_test/menu_test.contextual_links.yml @@ -0,0 +1,14 @@ +menu_test.hidden_manage: + title: 'List links' + group: menu_test_menu + route_name: menu_test.hidden_manage + +menu_test.hidden_manage_edit: + title: 'Edit menu' + group: menu_test_menu + route_name: menu_test.hidden_manage_edit + +menu_test.hidden_block_configure: + title: 'Configure block' + group: menu_test_block + route_name: menu_test.hidden_block_configure diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module index 566a620..c42e705 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.module +++ b/core/modules/system/tests/modules/menu_test/menu_test.module @@ -126,7 +126,7 @@ function menu_test_menu() { $items['menu-test/hidden/menu/manage/%menu/list'] = array( 'title' => 'List links', 'type' => MENU_DEFAULT_LOCAL_TASK, - 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, + 'context' => MENU_CONTEXT_PAGE, ); $items['menu-test/hidden/menu/manage/%menu/add'] = array( 'title' => 'Add link', @@ -137,7 +137,7 @@ function menu_test_menu() { 'title' => 'Edit menu', 'route_name' => 'menu_test.hidden_manage_edit', 'type' => MENU_LOCAL_TASK, - 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, + 'context' => MENU_CONTEXT_PAGE, ); $items['menu-test/hidden/menu/manage/%menu/delete'] = array( 'title' => 'Delete menu', @@ -162,11 +162,6 @@ function menu_test_menu() { 'title' => 'Configure block', 'route_name' => 'menu_test.hidden_block_configure', ); - $items['menu-test/hidden/block/manage/%/%/configure'] = array( - 'title' => 'Configure block', - 'type' => MENU_DEFAULT_LOCAL_TASK, - 'context' => MENU_CONTEXT_INLINE, - ); $items['menu-test/hidden/block/manage/%/%/delete'] = array( 'title' => 'Delete block', 'route_name' => 'menu_test.hidden_block_delete', diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TermViewBuilder.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermViewBuilder.php index 6e91524..b33dbfd 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/TermViewBuilder.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermViewBuilder.php @@ -54,7 +54,9 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco protected function alterBuild(array &$build, EntityInterface $entity, EntityDisplay $display, $view_mode, $langcode = NULL) { parent::alterBuild($build, $entity, $display, $view_mode, $langcode); $build['#attached']['css'][] = drupal_get_path('module', 'taxonomy') . '/css/taxonomy.module.css'; - $build['#contextual_links']['taxonomy'] = array('taxonomy/term', array($entity->id())); + $build['#contextual_links']['taxonomy_term'] = array( + 'route_parameters' => array('taxonomy_term' => $entity->id()), + ); } } diff --git a/core/modules/taxonomy/taxonomy.contextual_links.yml b/core/modules/taxonomy/taxonomy.contextual_links.yml new file mode 100644 index 0000000..3fafba7 --- /dev/null +++ b/core/modules/taxonomy/taxonomy.contextual_links.yml @@ -0,0 +1,17 @@ +taxonomy.term_edit: + title: Edit + group: taxonomy_term + route_name: taxonomy.term_edit + weight: 10 + +taxonomy.term_delete: + title: Delete + group: taxonomy_term + route_name: taxonomy.term_delete + weight: 20 + +taxonomy.vocabulary_delete: + title: Delete + group: taxonomy_vocabulary + route_name: taxonomy.vocabulary_delete + weight: 20 diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index 9c89610..3a57153 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -255,20 +255,6 @@ function taxonomy_menu() { 'title arguments' => array(2), 'route_name' => 'taxonomy.term_page', ); - $items['taxonomy/term/%taxonomy_term/edit'] = array( - 'title' => 'Edit', - 'route_name' => 'taxonomy.term_edit', - 'type' => MENU_LOCAL_TASK, - 'context' => MENU_CONTEXT_INLINE, - 'weight' => 10, - ); - $items['taxonomy/term/%taxonomy_term/delete'] = array( - 'title' => 'Delete', - 'type' => MENU_LOCAL_TASK, - 'context' => MENU_CONTEXT_INLINE, - 'weight' => 20, - 'route_name' => 'taxonomy.term_delete', - ); $items['taxonomy/term/%taxonomy_term/feed'] = array( 'title' => 'Taxonomy term', 'title callback' => 'taxonomy_term_title', @@ -291,13 +277,6 @@ function taxonomy_menu() { 'route_name' => 'taxonomy.vocabulary_edit', 'type' => MENU_LOCAL_TASK, ); - $items['admin/structure/taxonomy/%taxonomy_vocabulary/delete'] = array( - 'title' => 'Delete', - 'type' => MENU_LOCAL_TASK, - 'context' => MENU_CONTEXT_INLINE, - 'weight' => 20, - 'route_name' => 'taxonomy.vocabulary_delete', - ); $items['admin/structure/taxonomy/manage/%taxonomy_vocabulary/add'] = array( 'title' => 'Add term', diff --git a/core/modules/user/user.contextual_links.yml b/core/modules/user/user.contextual_links.yml new file mode 100644 index 0000000..1e0df52 --- /dev/null +++ b/core/modules/user/user.contextual_links.yml @@ -0,0 +1,5 @@ +user.role_delete: + title: 'Delete role' + group: role + weight: 10 + route_name: 'user.role_delete' diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 8b528d3..83baa54 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -795,12 +795,6 @@ function user_menu() { 'title' => 'Edit', 'type' => MENU_DEFAULT_LOCAL_TASK, ); - $items['admin/people/roles/manage/%user_role/delete'] = array( - 'title' => 'Delete role', - 'route_name' => 'user.role_delete', - 'weight' => 10, - 'context' => MENU_CONTEXT_INLINE, - ); // Administration pages. $items['admin/config/people'] = array( diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/PathPluginBase.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/PathPluginBase.php index 731f604..ac3cb9e 100644 --- a/core/modules/views/lib/Drupal/views/Plugin/views/display/PathPluginBase.php +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/PathPluginBase.php @@ -302,9 +302,9 @@ public function executeHookMenu($callbacks) { } // Add context for contextual links. - // @see menu_contextual_links() if (!empty($menu['context'])) { - $items[$path]['context'] = MENU_CONTEXT_INLINE; + // @todo Make this work with the new contextual links system. + $items[$path]['context'] = TRUE; } // If this is a 'default' tab, check to see if we have to create the diff --git a/core/modules/views/views.module b/core/modules/views/views.module index 279f14c..b23bbca 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -552,11 +552,11 @@ function views_add_contextual_links(&$render_element, $location, ViewExecutable $plugin['contextual_links_locations'][] = 'exposed_filter'; $has_links = !empty($plugin['contextual links']) && !empty($plugin['contextual_links_locations']); if ($has_links && in_array($location, $plugin['contextual_links_locations'])) { - foreach ($plugin['contextual links'] as $module => $link) { + foreach ($plugin['contextual links'] as $group => $link) { $args = array(); $valid = TRUE; - if (!empty($link['argument properties'])) { - foreach ($link['argument properties'] as $property) { + if (!empty($link['route_parameters_names'])) { + foreach ($link['route_parameters_names'] as $parameter_name => $property) { // If the plugin is trying to create an invalid contextual link // (for example, "path/to/{$view->storage->property}", where // $view->storage->{property} does not exist), we cannot construct @@ -566,7 +566,7 @@ function views_add_contextual_links(&$render_element, $location, ViewExecutable break; } else { - $args[] = $view->storage->{$property}; + $args[$parameter_name] = $view->storage->{$property}; } } } @@ -574,14 +574,13 @@ function views_add_contextual_links(&$render_element, $location, ViewExecutable // array. if ($valid) { $render_element['#views_contextual_links'] = TRUE; - $render_element['#contextual_links'][$module] = array( - $link['parent path'], - $args, - array( + $render_element['#contextual_links'][$group] = array( + 'route_parameters' => $args, + 'metadata' => array( 'location' => $location, 'name' => $view->storage->id(), 'display_id' => $display_id, - ) + ), ); } } diff --git a/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php b/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php index 4f700cf..905095a 100644 --- a/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php +++ b/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php @@ -219,13 +219,13 @@ public function testDisplayPluginsAlter() { $definitions = Views::pluginManager('display')->getDefinitions(); $expected = array( - 'parent path' => 'admin/structure/views/view', - 'argument properties' => array('id'), + 'route_name' => 'views_ui.edit', + 'route_parameters_names' => array('view' => 'id'), ); // Test the expected views_ui array exists on each definition. foreach ($definitions as $definition) { - $this->assertIdentical($definition['contextual links']['views_ui'], $expected, 'Expected views_ui array found in plugin definition.'); + $this->assertIdentical($definition['contextual links']['views_ui_edit'], $expected, 'Expected views_ui array found in plugin definition.'); } } @@ -288,7 +288,7 @@ public function testPageContextualLinks() { $view->enable()->save(); $this->drupalGet('test-display'); - $id = 'views_ui:admin/structure/views/view:test_display:location=page&name=test_display&display_id=page_1'; + $id = 'views_ui_edit:view=test_display:location=page&name=test_display&display_id=page_1'; // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder() $this->assertRaw('
', format_string('Contextual link placeholder with id @id exists.', array('@id' => $id))); @@ -298,7 +298,7 @@ public function testPageContextualLinks() { $response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-display'))); $this->assertResponse(200); $json = drupal_json_decode($response); - $this->assertIdentical($json[$id], ''); + $this->assertIdentical($json[$id], ''); } /** diff --git a/core/modules/views_ui/views_ui.contextual_links.yml b/core/modules/views_ui/views_ui.contextual_links.yml new file mode 100644 index 0000000..20e9ead --- /dev/null +++ b/core/modules/views_ui/views_ui.contextual_links.yml @@ -0,0 +1,9 @@ +views_ui.edit: + title: 'Edit view' + route_name: views_ui.edit + group: views_ui_edit + +views_ui.preview: + title: 'Preview view' + route_name: views_ui.preview + group: views_ui_preview diff --git a/core/modules/views_ui/views_ui.module b/core/modules/views_ui/views_ui.module index 93742a4..850f8cf 100644 --- a/core/modules/views_ui/views_ui.module +++ b/core/modules/views_ui/views_ui.module @@ -30,11 +30,6 @@ function views_ui_menu() { $items['admin/structure/views/view/%'] = array( 'route_name' => 'views_ui.edit', ); - $items['admin/structure/views/view/%/edit'] = array( - 'title' => 'Edit view', - 'type' => MENU_DEFAULT_LOCAL_TASK, - 'context' => MENU_CONTEXT_INLINE, - ); // A page in the Reports section to show usage of plugins in all views. $items['admin/reports/views-plugins'] = array( @@ -277,9 +272,9 @@ function views_ui_views_plugins_display_alter(&$plugins) { // paths underneath "admin/structure/views/view/{$view->id()}" (i.e., paths // for editing and performing other contextual actions on the view). foreach ($plugins as &$display) { - $display['contextual links']['views_ui'] = array( - 'parent path' => 'admin/structure/views/view', - 'argument properties' => array('id'), + $display['contextual links']['views_ui_edit'] = array( + 'route_name' => 'views_ui.edit', + 'route_parameters_names' => array('view' => 'id'), ); } } @@ -296,9 +291,10 @@ function views_ui_contextual_links_view_alter(&$element, $items) { // Append the display ID to the Views UI edit links, so that clicking on the // contextual link takes you directly to the correct display tab on the edit // screen. - elseif (!empty($element['#links']['views-ui-edit'])) { - $display_id = $element['#contextual_links']['views_ui'][2]['display_id']; - $element['#links']['views-ui-edit']['href'] .= '/edit/' . $display_id; + elseif (!empty($element['#links']['views-uiedit'])) { + $display_id = $items['views_ui.edit']['metadata']['display_id']; + $element['#links']['views-uiedit']['route_parameters']['display_id'] = $display_id; + $element['#links']['views-uiedit']['route_name'] = 'views_ui.edit_display'; } } diff --git a/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php new file mode 100644 index 0000000..e93b127 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkDefaultTest.php @@ -0,0 +1,158 @@ + 'contextual_link_default', + ); + + /** + * The mocked translator. + * + * @var \Drupal\Core\StringTranslation\TranslationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $stringTranslation; + + public static function getInfo() { + return array( + 'name' => 'Contextual links default.', + 'description' => 'Tests the contextual link default class.', + 'group' => 'Menu', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->stringTranslation = $this->getMock('Drupal\Core\StringTranslation\TranslationInterface'); + } + + protected function setupContextualLinkDefault() { + $this->contextualLinkDefault = new ContextualLinkDefault($this->config, $this->pluginId, $this->pluginDefinition); + $this->contextualLinkDefault->setTranslationManager($this->stringTranslation); + } + + /** + * Tests the getTitle method without a translation context. + * + * @see \Drupal\Core\Menu\LocalTaskDefault::getTitle() + */ + public function testGetTitle($title = 'Example') { + $this->pluginDefinition['title'] = $title; + $this->stringTranslation->expects($this->once()) + ->method('translate') + ->with($this->pluginDefinition['title'], array(), array()) + ->will($this->returnValue('Example translated')); + + $this->setupContextualLinkDefault(); + $this->assertEquals('Example translated', $this->contextualLinkDefault->getTitle()); + } + + /** + * Tests the getTitle method with a translation context. + * + * @see \Drupal\Core\Menu\LocalTaskDefault::getTitle() + */ + public function testGetTitleWithContext() { + $this->pluginDefinition['title'] = 'Example'; + $this->pluginDefinition['title_context'] = 'context'; + $this->stringTranslation->expects($this->once()) + ->method('translate') + ->with($this->pluginDefinition['title'], array(), array('context' => $this->pluginDefinition['title_context'])) + ->will($this->returnValue('Example translated with context')); + + $this->setupContextualLinkDefault(); + $this->assertEquals('Example translated with context', $this->contextualLinkDefault->getTitle()); + } + + /** + * Tests the getRouteName() method. + * + * @covers \Drupal\Core\Menu\ContextualLinkDefault::getRouteName() + */ + public function testGetRouteName($route_name = 'test_route_name') { + $this->pluginDefinition['route_name'] = $route_name; + $this->setupContextualLinkDefault(); + + $this->assertEquals($route_name, $this->contextualLinkDefault->getRouteName()); + } + + /** + * Tests the getGroup() method. + * + * @covers \Drupal\Core\Menu\ContextualLinkDefault::getGroup() + */ + public function testGetGroup($group_name = 'test_group') { + $this->pluginDefinition['group'] = $group_name; + $this->setupContextualLinkDefault(); + + $this->assertEquals($group_name, $this->contextualLinkDefault->getGroup()); + } + + /** + * Tests the getOptions() method. + * + * @covers \Drupal\Core\Menu\ContextualLinkDefault::getOptions() + */ + public function testGetOptions($options = array('key' => 'value')) { + $this->pluginDefinition['options'] = $options; + $this->setupContextualLinkDefault(); + + $this->assertEquals($options, $this->contextualLinkDefault->getOptions()); + } + + /** + * Tests the getWeight() method. + * + * @covers \Drupal\Core\Menu\ContextualLinkDefault::getWeight() + */ + public function testGetWeight($weight = 5) { + $this->pluginDefinition['weight'] = $weight; + $this->setupContextualLinkDefault(); + + $this->assertEquals($weight, $this->contextualLinkDefault->getWeight()); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Menu/ContextualLinkManagerTest.php b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkManagerTest.php new file mode 100644 index 0000000..9455724 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Menu/ContextualLinkManagerTest.php @@ -0,0 +1,406 @@ + 'Contextual links manager.', + 'description' => 'Tests the contextual links manager.', + 'group' => 'Menu', + ); + } + + protected function setUp() { + $this->contextualLinkManager = $this + ->getMockBuilder('Drupal\Core\Menu\ContextualLinkManager') + ->disableOriginalConstructor() + ->setMethods(NULL) + ->getMock(); + + $this->controllerResolver = $this->getMock('Symfony\Component\HttpKernel\Controller\ControllerResolverInterface'); + $this->pluginDiscovery = $this->getMock('Drupal\Component\Plugin\Discovery\DiscoveryInterface'); + $this->factory = $this->getMock('Drupal\Component\Plugin\Factory\FactoryInterface'); + $this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); + $this->accessManager = $this->getMockBuilder('Drupal\Core\Access\AccessManager') + ->disableOriginalConstructor() + ->getMock(); + + $property = new \ReflectionProperty('Drupal\Core\Menu\ContextualLinkManager', 'controllerResolver'); + $property->setAccessible(TRUE); + $property->setValue($this->contextualLinkManager, $this->controllerResolver); + + $property = new \ReflectionProperty('Drupal\Core\Menu\ContextualLinkManager', 'discovery'); + $property->setAccessible(TRUE); + $property->setValue($this->contextualLinkManager, $this->pluginDiscovery); + + $property = new \ReflectionProperty('Drupal\Core\Menu\ContextualLinkManager', 'factory'); + $property->setAccessible(TRUE); + $property->setValue($this->contextualLinkManager, $this->factory); + + + $property = new \ReflectionProperty('Drupal\Core\Menu\ContextualLinkManager', 'accessManager'); + $property->setAccessible(TRUE); + $property->setValue($this->contextualLinkManager, $this->accessManager); + + $language_manager = $this->getMockBuilder('Drupal\Core\Language\LanguageManager') + ->disableOriginalConstructor() + ->getMock(); + $language_manager->expects($this->any()) + ->method('getLanguage') + ->will($this->returnValue(new Language(array('id' => 'en')))); + + $this->moduleHandler = $this->getMock('\Drupal\Core\Extension\ModuleHandlerInterface'); + + $method = new \ReflectionMethod('Drupal\Core\Menu\ContextualLinkManager', 'alterInfo'); + $method->setAccessible(TRUE); + $method->invoke($this->contextualLinkManager, $this->moduleHandler, 'contextual_links_plugins'); + + $this->contextualLinkManager->setCacheBackend($this->cacheBackend, $language_manager, 'contextual_links_plugins'); + } + + /** + * Tests the getContextualLinkPluginsByGroup method. + * + * @see \Drupal\Core\Menu\ContextualLinkManager::getContextualLinkPluginsByGroup() + */ + public function testGetContextualLinkPluginsByGroup() { + $definitions = array( + 'test_plugin1' => array( + 'id' => 'test_plugin1', + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'group' => 'group1', + 'route_name' => 'test_route', + ), + 'test_plugin2' => array( + 'id' => 'test_plugin2', + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'group' => 'group1', + 'route_name' => 'test_route2', + ), + 'test_plugin3' => array( + 'id' => 'test_plugin3', + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'group' => 'group2', + 'route_name' => 'test_router3', + ), + ); + $this->pluginDiscovery->expects($this->once()) + ->method('getDefinitions') + ->will($this->returnValue($definitions)); + + // Test with a non existing group. + $result = $this->contextualLinkManager->getContextualLinkPluginsByGroup('group_non_existing'); + $this->assertEmpty($result); + + $result = $this->contextualLinkManager->getContextualLinkPluginsByGroup('group1'); + $this->assertEquals(array('test_plugin1', 'test_plugin2'), array_keys($result)); + + $result = $this->contextualLinkManager->getContextualLinkPluginsByGroup('group2'); + $this->assertEquals(array('test_plugin3'), array_keys($result)); + } + + /** + * Tests the getContextualLinkPluginsByGroup method with a prefilled cache. + */ + public function testGetContextualLinkPluginsByGroupWithCache() { + $definitions = array( + 'test_plugin1' => array( + 'id' => 'test_plugin1', + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'group' => 'group1', + 'route_name' => 'test_route', + ), + 'test_plugin2' => array( + 'id' => 'test_plugin2', + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'group' => 'group1', + 'route_name' => 'test_route2', + ), + ); + + $this->cacheBackend->expects($this->once()) + ->method('get') + ->with('contextual_links_plugins:en:group1') + ->will($this->returnValue((object) array('data' => $definitions))); + + $result = $this->contextualLinkManager->getContextualLinkPluginsByGroup('group1'); + $this->assertEquals($definitions, $result); + + // Ensure that the static cache works, so no second cache get is executed. + + $result = $this->contextualLinkManager->getContextualLinkPluginsByGroup('group1'); + $this->assertEquals($definitions, $result); + } + + /** + * Tests processDefinition() by passing a plugin definition without a route. + * + * @see \Drupal\Core\Menu\ContextualLinkManager::processDefinition() + * + * @expectedException \Drupal\Component\Plugin\Exception\PluginException + */ + public function testProcessDefinitionWithoutRoute() { + $definition = array( + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'group' => 'example', + 'id' => 'test_plugin', + ); + $this->contextualLinkManager->processDefinition($definition, 'test_plugin'); + } + + /** + * Tests processDefinition() by passing a plugin definition without a group. + * + * @see \Drupal\Core\Menu\ContextualLinkManager::processDefinition() + * + * @expectedException \Drupal\Component\Plugin\Exception\PluginException + */ + public function testProcessDefinitionWithoutGroup() { + $definition = array( + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'route_name' => 'example', + 'id' => 'test_plugin', + ); + $this->contextualLinkManager->processDefinition($definition, 'test_plugin'); + } + + /** + * Tests the getContextualLinksArrayByGroup method. + * + * @see \Drupal\Core\Menu\ContextualLinkManager::getContextualLinksArrayByGroup() + */ + public function testGetContextualLinksArrayByGroup() { + $definitions = array( + 'test_plugin1' => array( + 'id' => 'test_plugin1', + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'title' => 'Plugin 1', + 'weight' => 0, + 'group' => 'group1', + 'route_name' => 'test_route', + 'options' => array(), + ), + 'test_plugin2' => array( + 'id' => 'test_plugin2', + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'title' => 'Plugin 2', + 'weight' => 2, + 'group' => 'group1', + 'route_name' => 'test_route2', + 'options' => array('key' => 'value'), + ), + 'test_plugin3' => array( + 'id' => 'test_plugin3', + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'title' => 'Plugin 3', + 'weight' => 5, + 'group' => 'group2', + 'route_name' => 'test_router3', + 'options' => array(), + ), + ); + + $this->pluginDiscovery->expects($this->once()) + ->method('getDefinitions') + ->will($this->returnValue($definitions)); + + $this->accessManager->expects($this->any()) + ->method('checkNamedRoute') + ->will($this->returnValue(TRUE)); + + // Set up mocking of the plugin factory. + $map = array(); + foreach ($definitions as $plugin_id => $definition) { + $plugin = $this->getMock('Drupal\Core\Menu\ContextualLinkInterface'); + $plugin->expects($this->any()) + ->method('getRouteName') + ->will($this->returnValue($definition['route_name'])); + $plugin->expects($this->any()) + ->method('getTitle') + ->will($this->returnValue($definition['title'])); + $plugin->expects($this->any()) + ->method('getWeight') + ->will($this->returnValue($definition['weight'])); + $plugin->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($definition['options'])); + $map[] = array($plugin_id, array(), $plugin); + } + $this->factory->expects($this->any()) + ->method('createInstance') + ->will($this->returnValueMap($map)); + + $this->moduleHandler->expects($this->at(1)) + ->method('alter') + ->with($this->equalTo('contextual_links'), new \PHPUnit_Framework_Constraint_Count(2), $this->equalTo('group1'), $this->equalTo(array('key' => 'value'))); + + $result = $this->contextualLinkManager->getContextualLinksArrayByGroup('group1', array('key' => 'value')); + $this->assertCount(2, $result); + foreach (array('test_plugin1', 'test_plugin2') as $plugin_id) { + $definition = $definitions[$plugin_id]; + $this->assertEquals($definition['weight'], $result[$plugin_id]['weight']); + $this->assertEquals($definition['title'], $result[$plugin_id]['title']); + $this->assertEquals($definition['route_name'], $result[$plugin_id]['route_name']); + } + } + + /** + * Tests the access checking of the getContextualLinksArrayByGroup method. + * + * @see \Drupal\Core\Menu\ContextualLinkManager::getContextualLinksArrayByGroup() + */ + public function testGetContextualLinksArrayByGroupAccessCheck() { + $definitions = array( + 'test_plugin1' => array( + 'id' => 'test_plugin1', + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'title' => 'Plugin 1', + 'weight' => 0, + 'group' => 'group1', + 'route_name' => 'test_route', + 'options' => array(), + ), + 'test_plugin2' => array( + 'id' => 'test_plugin2', + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'title' => 'Plugin 2', + 'weight' => 2, + 'group' => 'group1', + 'route_name' => 'test_route2', + 'options' => array('key' => 'value'), + ), + ); + + $this->pluginDiscovery->expects($this->once()) + ->method('getDefinitions') + ->will($this->returnValue($definitions)); + + $this->accessManager->expects($this->any()) + ->method('checkNamedRoute') + ->will($this->returnValueMap(array( + array('test_route', array('key' => 'value'), NULL, TRUE), + array('test_route2', array('key' => 'value'), NULL, FALSE), + ))); + + // Set up mocking of the plugin factory. + $map = array(); + foreach ($definitions as $plugin_id => $definition) { + $plugin = $this->getMock('Drupal\Core\Menu\ContextualLinkInterface'); + $plugin->expects($this->any()) + ->method('getRouteName') + ->will($this->returnValue($definition['route_name'])); + $plugin->expects($this->any()) + ->method('getTitle') + ->will($this->returnValue($definition['title'])); + $plugin->expects($this->any()) + ->method('getWeight') + ->will($this->returnValue($definition['weight'])); + $plugin->expects($this->any()) + ->method('getOptions') + ->will($this->returnValue($definition['options'])); + $map[] = array($plugin_id, array(), $plugin); + } + $this->factory->expects($this->any()) + ->method('createInstance') + ->will($this->returnValueMap($map)); + + $result = $this->contextualLinkManager->getContextualLinksArrayByGroup('group1', array('key' => 'value')); + + // Ensure that access checking was respected. + $this->assertTrue(isset($result['test_plugin1'])); + $this->assertFalse(isset($result['test_plugin2'])); + } + + /** + * Tests the plugins alter hook. + */ + public function testPluginDefinitionAlter() { + $definitions['test_plugin'] = array( + 'id' => 'test_plugin', + 'class' => '\Drupal\Core\Menu\ContextualLinkDefault', + 'title' => 'Plugin', + 'weight' => 2, + 'group' => 'group1', + 'route_name' => 'test_route', + 'options' => array('key' => 'value'), + ); + + $this->pluginDiscovery->expects($this->once()) + ->method('getDefinitions') + ->will($this->returnValue($definitions)); + + $this->moduleHandler->expects($this->once()) + ->method('alter') + ->with('contextual_links_plugins', $definitions); + + $this->contextualLinkManager->getDefinition('test_plugin'); + } + +}