Commit 0d263c64 authored by catch's avatar catch

Issue #2084463 by dawehner, pwolanin, David_Rothstein, tim.plunkett, Xano:...

Issue #2084463 by dawehner, pwolanin, David_Rothstein, tim.plunkett, Xano: Convert contextual links to a plugin system similar to local tasks/actions.
parent 952f57a1
......@@ -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', '@current_user']
request:
class: Symfony\Component\HttpFoundation\Request
# @TODO the synthetic setting must be uncommented whenever drupal_session_initialize()
......
......@@ -250,11 +250,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();
......@@ -2223,114 +2217,6 @@ function _menu_get_legacy_tasks($router_item, &$data, &$root_path) {
$data['tabs'] += $tabs;
}
/**
* 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.
*/
......
......@@ -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'] == '<front>' && drupal_is_front_page()));
$is_current_language = (empty($link['language']) || $link['language']->id == $language_url->id);
if ($is_current_path && $is_current_language) {
......@@ -1715,9 +1719,18 @@ function theme_links($variables) {
$item = drupal_render($link_element);
}
else {
// Pass in $link as $options, they share the same keys.
// @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.
else {
......
<?php
/**
* @file
* Contains \Drupal\Core\Menu\ContextualLinkDefault.
*/
namespace Drupal\Core\Menu;
use Drupal\Core\Plugin\PluginBase;
/**
* Provides a common base implementation of a contextual link.
*/
class ContextualLinkDefault extends PluginBase implements ContextualLinkInterface {
/**
* {@inheritdoc}
*/
public function getTitle() {
$options = array();
if (!empty($this->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'];
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Menu\ContextualLinkInterface.
*/
namespace Drupal\Core\Menu;
/**
* Defines a contextual link plugin.
*/
interface ContextualLinkInterface {
/**
* Returns the localized title to be shown for this contextual link.
*
* Subclasses may add optional arguments like NodeInterface $node = NULL that
* will be supplied by the ControllerResolver.
*
* @return string
* The title to be shown for this action.
*
* @see \Drupal\Core\Menu\ContextualLinksManager::getTitle()
*/
public function getTitle();
/**
* Returns the route name of the contextual link.
*
* @return string
* The name of the route this contextual link links to.
*/
public function getRouteName();
/**
* Returns the group this contextual link should be rendered in.
*
* A contextual link group is a set of contextual links that are displayed
* together on a certain page. For example, the 'block' group displays all
* links related to the block, such as the block instance edit link as well as
* the views edit link, if it is a view block.
*
* @return string
* The contextual links group name.
*/
public function getGroup();
/**
* Returns the link options passed to the link generator.
*
* @return array
* The options as expected by LinkGeneratorInterface::generate()
*
* @see \Drupal\Core\Utility\LinkGeneratorInterface::generate()
*/
public function getOptions();
/**
* Returns the weight of the contextual link.
*
* The contextual links in one group are sorted by weight for display.
*
* @return int
* The weight as positive/negative integer.
*/
public function getWeight();
}
<?php
/**
* @file
* Contains \Drupal\Core\Menu\ContextualLinkManager.
*/
namespace Drupal\Core\Menu;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Access\AccessManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManager;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
use Drupal\Core\Plugin\Factory\ContainerFactory;
use Drupal\Core\Session\AccountInterface;
/**
* Defines a contextual link plugin manager to deal with contextual links.
*
* @see \Drupal\Core\Menu\ContextualLinkInterface
*/
class ContextualLinkManager extends DefaultPluginManager implements ContextualLinkManagerInterface {
/**
* Provides default values for a contextual link definition.
*
* @var array
*/
protected $defaults = array(
// (required) The name of the route to link to.
'route_name' => '',
// (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;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* 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.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
*/
public function __construct(ControllerResolverInterface $controller_resolver, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, LanguageManager $language_manager, AccessManager $access_manager, AccountInterface $account) {
$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->account = $account;
$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, $this->account)) {
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;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Menu\ContextualLinkManagerInterface.
*/
namespace Drupal\Core\Menu;
/**
* Provides an object which returns the available contextual links.
*/
interface ContextualLinkManagerInterface {
/**
* Gets the contextual link plugins by contextual link group.
*
* @param string $group_name
* The group name.
*
* @return array
* A list of contextual links plugin definitions.
*/
public function getContextualLinkPluginsByGroup($group_name);
/**
* Gets the contextual links prepared as expected by theme_links().
*
* @param string $group_name
* The group name.
* @param array $route_parameters
* The incoming route parameters. The route parameters need to have the same
* name on all contextual link routes, e.g. you cannot use 'node' and
* 'entity' in parallel.
* @param array $metadata
* Additional metadata of contextual links, like the position (optional).
*
* @return array
* An array of link information, keyed by the plugin ID. Each entry is an
* associative array with the following keys:
* - route_name: The route name to link to.
* - route_parameters: The route parameters for the contextual link.
* - title: The title of the contextual link.
* - weight: The weight of the contextual link.
* - localized_options: The options of the link, which will be passed
* to the link generator.
* - metadata: The array of additional metadata that was passed in.
*/
public function getContextualLinksArrayByGroup($group_name, array $route_parameters, array $metadata = array());
}
block_configure:
title: 'Configure block'
route_name: 'block.admin_edit'
group: 'block'
......@@ -104,11 +104,6 @@ function block_menu() {
'title' => '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'])) {
......
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
......@@ -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;
}
......
......@@ -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()),
);
}
}
......
......@@ -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('<div data-contextual-id="'. $id . '"></div>', 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], '<ul class="contextual-links"><li class="block-configure odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '?destination=test-page">Configure block</a></li><li class="views-ui-edit even last"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1?destination=test-page">Edit view</a></li></ul>');
$this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '?destination=test-page">Configure block</a></li><li class="views-uiedit even last"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1?destination=test-page">Edit view</a></li></ul>');
}
}
content_translation.contextual_links:
derivative: 'Drupal\content_translation\Plugin\Derivative\ContentTranslationContextualLinks'
weight: 2
......@@ -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;
......
<?php
/**
* @file
* Contains \Drupal\content_translation\Plugin\Derivative\ContentTranslationContextualLinks.
*/
namespace Drupal\content_translation\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DerivativeBase;
use Drupal\Core\Entity\EntityManager;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides dynamic contextual links for content translation.
*
* @see \Drupal\content_translation\Plugin\Menu\ContextualLink\ContentTranslationContextualLinks
*/
class ContentTranslationContextualLinks extends DerivativeBase implements ContainerDerivativeInterface {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManager
*/
protected $entityManager;
/**
* Constructs a new ContentTranslationContextualLinks.
*
* @param \Drupal\Core\Entity\EntityManager $entity_manager
* The entity manager.
*/
public function __construct(EntityManager $entity_manager) {
$this->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) {
//