Commit 70f62fd0 authored by willzyx's avatar willzyx

Issue #2839502 by willzyx: Add toolbar integration - make devel menu items more accessible

parent 8a6e8b06
toolbar_items:
- 'devel.admin_settings_link'
- 'devel.cache_clear'
- 'devel.container_info.service'
- 'devel.execute_php'
- 'devel.menu_rebuild'
- 'devel.reinstall'
- 'devel.route_info'
- 'devel.run_cron'
......@@ -28,6 +28,16 @@ devel.settings:
type: string
label: 'Devel variable dumper'
devel.toolbar.settings:
type: config_object
label: 'Devel Toolbar settings'
mapping:
toolbar_items:
type: sequence
label: 'Toolbar items'
sequence:
type: string
block.settings.devel_switch_user:
type: block_settings
label: 'Switch user block'
......
/**
* @file
* Styling for devel toolbar module.
*/
.toolbar .toolbar-tray-vertical .edit-devel-toolbar {
text-align: right; /* LTR */
padding: 1em;
}
[dir="rtl"] .toolbar .toolbar-tray-vertical .edit-devel-toolbar {
text-align: left;
}
.toolbar .toolbar-tray-horizontal .edit-devel-toolbar {
float: right; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-horizontal .edit-devel-toolbar {
float: left;
}
.toolbar .toolbar-tray-horizontal .menu {
float: left; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-horizontal .menu {
float: right;
}
.toolbar .toolbar-bar .toolbar-icon-devel:before{
background-image: url(../icons/bebebe/cog.svg);
}
.toolbar-bar .toolbar-icon-devel:active:before,
.toolbar-bar .toolbar-icon-devel.is-active:before {
background-image: url(../icons/ffffff/cog.svg);
}
.toolbar-horizontal .toolbar-horizontal-item-hidden {
display: none;
}
......@@ -3,3 +3,9 @@ devel:
css:
theme:
css/devel.css: {}
devel-toolbar:
version: VERSION
css:
component:
css/devel.toolbar.css: {}
......@@ -19,11 +19,13 @@ use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Menu\LocalTaskDefault;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\Core\Utility\Error;
use Drupal\devel\EntityTypeInfo;
use Drupal\devel\ToolbarHandler;
/**
* Implements hook_help().
......@@ -93,6 +95,30 @@ function devel_entity_operation(EntityInterface $entity) {
->entityOperation($entity);
}
/**
* Implements hook_toolbar().
*/
function devel_toolbar() {
return \Drupal::service('class_resolver')
->getInstanceFromDefinition(ToolbarHandler::class)
->toolbar();
}
/**
* Implements hook_local_tasks_alter().
*/
function devel_local_tasks_alter(&$local_tasks) {
if (\Drupal::moduleHandler()->moduleExists('toolbar')) {
$local_tasks['devel.toolbar.settings_form'] = [
'title' => 'Toolbar Settings',
'base_route' => 'devel.admin_settings',
'route_name' => 'devel.toolbar.settings_form',
'class' => LocalTaskDefault::class,
'options' => [],
];
}
}
/**
* Sets message.
*/
......
......@@ -6,6 +6,15 @@ devel.admin_settings:
requirements:
_permission: 'administer site configuration'
devel.toolbar.settings_form:
path: 'admin/config/development/devel/toolbar'
defaults:
_form: '\Drupal\devel\Form\ToolbarSettingsForm'
_title: 'Devel Toolbar Settings'
requirements:
_permission: 'administer site configuration'
_module_dependencies: 'toolbar'
devel.reinstall:
path: '/devel/reinstall'
defaults:
......
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#bebebe" d="M15.176 9.041c.045-.327.076-.658.076-.998 0-.36-.035-.71-.086-1.056l-2.275-.293c-.115-.426-.283-.827-.498-1.201l1.396-1.808c-.416-.551-.906-1.039-1.459-1.452l-1.807 1.391c-.373-.215-.774-.383-1.2-.499l-.292-2.252c-.338-.048-.677-.081-1.029-.081s-.694.033-1.032.082l-.291 2.251c-.426.116-.826.284-1.2.499l-1.805-1.391c-.552.413-1.044.901-1.459 1.452l1.395 1.808c-.215.374-.383.774-.499 1.2l-2.276.294c-.05.346-.085.696-.085 1.056 0 .34.031.671.077.998l2.285.295c.115.426.284.826.499 1.2l-1.417 1.836c.411.55.896 1.038 1.443 1.452l1.842-1.42c.374.215.774.383 1.2.498l.298 2.311c.337.047.677.08 1.025.08s.688-.033 1.021-.08l.299-2.311c.426-.115.826-.283 1.201-.498l1.842 1.42c.547-.414 1.031-.902 1.443-1.452l-1.416-1.837c.215-.373.383-.773.498-1.199l2.286-.295zm-7.174 1.514c-1.406 0-2.543-1.137-2.543-2.541 0-1.402 1.137-2.541 2.543-2.541 1.402 0 2.541 1.138 2.541 2.541 0 1.404-1.139 2.541-2.541 2.541z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ffffff" d="M15.176 9.041c.045-.327.076-.658.076-.998 0-.36-.035-.71-.086-1.056l-2.275-.293c-.115-.426-.283-.827-.498-1.201l1.396-1.808c-.416-.551-.906-1.039-1.459-1.452l-1.807 1.391c-.373-.215-.774-.383-1.2-.499l-.292-2.252c-.338-.048-.677-.081-1.029-.081s-.694.033-1.032.082l-.291 2.251c-.426.116-.826.284-1.2.499l-1.805-1.391c-.552.413-1.044.901-1.459 1.452l1.395 1.808c-.215.374-.383.774-.499 1.2l-2.276.294c-.05.346-.085.696-.085 1.056 0 .34.031.671.077.998l2.285.295c.115.426.284.826.499 1.2l-1.417 1.836c.411.55.896 1.038 1.443 1.452l1.842-1.42c.374.215.774.383 1.2.498l.298 2.311c.337.047.677.08 1.025.08s.688-.033 1.021-.08l.299-2.311c.426-.115.826-.283 1.201-.498l1.842 1.42c.547-.414 1.031-.902 1.443-1.452l-1.416-1.837c.215-.373.383-.773.498-1.199l2.286-.295zm-7.174 1.514c-1.406 0-2.543-1.137-2.543-2.541 0-1.402 1.137-2.541 2.543-2.541 1.402 0 2.541 1.138 2.541 2.541 0 1.404-1.139 2.541-2.541 2.541z"/></svg>
\ No newline at end of file
<?php
namespace Drupal\devel\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configures devel toolbar settings.
*/
class ToolbarSettingsForm extends ConfigFormBase {
/**
* The menu link tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTree
*/
protected $menuLinkTree;
/**
* ToolbarSettingsForm constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_link_tree
* The menu link tree service.
*/
public function __construct(ConfigFactoryInterface $config_factory, MenuLinkTreeInterface $menu_link_tree) {
parent::__construct($config_factory);
$this->menuLinkTree = $menu_link_tree;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('menu.link_tree')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'devel_toolbar_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
'devel.toolbar.settings',
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('devel.toolbar.settings');
$form['toolbar_items'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Menu items always visible'),
'#options' => $this->getLinkLabels(),
'#default_value' => $config->get('toolbar_items') ?: [],
'#required' => TRUE,
'#description' => $this->t('Select the menu items always visible in devel toolbar tray. All the items not selected in this list will be visible only when the toolbar orientation is vertical.'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$values = $form_state->getValues();
$toolbar_items = array_keys(array_filter($values['toolbar_items']));
$this->config('devel.toolbar.settings')
->set('toolbar_items', $toolbar_items)
->save();
parent::submitForm($form, $form_state);
}
/**
* Provides an array of available menu items.
*
* @return array
* Associative array of devel menu item labels keyed by plugin ID.
*/
protected function getLinkLabels() {
$options = [];
$parameters = new MenuTreeParameters();
$parameters->onlyEnabledLinks()->setTopLevelOnly();
$tree = $this->menuLinkTree->load('devel', $parameters);
foreach ($tree as $element) {
$link = $element->link;
$options[$link->getPluginId()] = $link->getTitle();
}
asort($options);
return $options;
}
}
<?php
namespace Drupal\devel;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Toolbar integration handler.
*/
class ToolbarHandler implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The menu link tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTreeInterface
*/
protected $menuLinkTree;
/**
* The devel toolbar config.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $account;
/**
* ToolbarHandler constructor.
*
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_link_tree
* The menu link tree service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Session\AccountProxyInterface $account
* The current user.
*/
public function __construct(MenuLinkTreeInterface $menu_link_tree, ConfigFactoryInterface $config_factory, AccountProxyInterface $account) {
$this->menuLinkTree = $menu_link_tree;
$this->config = $config_factory->get('devel.toolbar.settings');
$this->account = $account;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('menu.link_tree'),
$container->get('config.factory'),
$container->get('current_user')
);
}
/**
* Hook bridge.
*
* @return array
* The devel toolbar items render array.
*
* @see hook_toolbar()
*/
public function toolbar() {
$items['devel'] = [
'#cache' => [
'contexts' => ['user.permissions'],
],
];
if ($this->account->hasPermission('access devel information')) {
$items['devel'] += [
'#type' => 'toolbar_item',
'#weight' => 999,
'tab' => [
'#type' => 'link',
'#title' => $this->t('Devel'),
'#url' => Url::fromRoute('devel.admin_settings'),
'#attributes' => [
'title' => $this->t('Development menu'),
'class' => ['toolbar-icon', 'toolbar-icon-devel'],
],
],
'tray' => [
'#heading' => $this->t('Development menu'),
'devel_menu' => [
// Currently devel menu is uncacheable, so instead of poisoning the
// entire page cache we use a lazy builder.
// @see \Drupal\devel\Plugin\Menu\DestinationMenuLink
// @see \Drupal\devel\Plugin\Menu\RouteDetailMenuItem
'#lazy_builder' => [ToolbarHandler::class . ':lazyBuilder', []],
// Force the creation of the placeholder instead of rely on the
// automatical placeholdering or otherwise the page results
// uncacheable when max-age 0 is bubbled up.
'#create_placeholder' => TRUE,
],
'configuration' => [
'#type' => 'link',
'#title' => $this->t('Configure'),
'#url' => Url::fromRoute('devel.toolbar.settings_form'),
'#options' => [
'attributes' => ['class' => ['edit-devel-toolbar']],
],
],
],
'#attached' => [
'library' => 'devel/devel-toolbar',
],
];
}
return $items;
}
/**
* Lazy builder callback for the devel menu toolbar.
*
* @return array
* The renderable array rapresentation of the devel menu.
*/
public function lazyBuilder() {
$parameters = new MenuTreeParameters();
$parameters->onlyEnabledLinks()->setTopLevelOnly();
$tree = $this->menuLinkTree->load('devel', $parameters);
$manipulators = [
['callable' => 'menu.default_tree_manipulators:checkAccess'],
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
['callable' => ToolbarHandler::class . ':processTree'],
];
$tree = $this->menuLinkTree->transform($tree, $manipulators);
$build = $this->menuLinkTree->build($tree);
CacheableMetadata::createFromRenderArray($build)
->addCacheableDependency($this->config)
->applyTo($build);
return $build;
}
/**
* Adds toolbar-specific attributes to the menu link tree.
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu link tree to manipulate.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* The manipulated menu link tree.
*/
public function processTree(array $tree) {
$visible_items = $this->config->get('toolbar_items') ?: [];
foreach ($tree as $element) {
$plugin_id = $element->link->getPluginId();
if (!in_array($plugin_id, $visible_items)) {
// Add a class that allow to hide the non prioritized menu items when
// the toolbar has horizontal orientation.
$element->options['attributes']['class'][] = 'toolbar-horizontal-item-hidden';
}
}
return $tree;
}
}
<?php
namespace Drupal\Tests\devel\Functional;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Tests\BrowserTestBase;
/**
* Tests devel toolbar module functionality.
*
* @group devel
*/
class DevelToolbarTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['devel', 'toolbar', 'block'];
/**
* The user for tests.
*
* @var \Drupal\user\UserInterface
*/
protected $toolbarUser;
/**
* The user for tests.
*
* @var \Drupal\user\UserInterface
*/
protected $develUser;
/**
* The dafault toolbar items.
*
* @var array
*/
protected $defaultToolbarItems = [
'devel.cache_clear',
'devel.container_info.service',
'devel.admin_settings_link',
'devel.execute_php',
'devel.menu_rebuild',
'devel.reinstall',
'devel.route_info',
'devel.run_cron',
];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalPlaceBlock('page_title_block');
$this->develUser = $this->drupalCreateUser([
'administer site configuration',
'access devel information',
'execute php code',
'access toolbar',
]);
$this->toolbarUser = $this->drupalCreateUser([
'access toolbar',
]);
}
/**
* Tests configuration form.
*/
public function testConfigurationForm() {
// Ensures that the page is accessible ony to users with the adequate
// permissions.
$this->drupalGet('admin/config/development/devel/toolbar');
$this->assertSession()->statusCodeEquals(403);
// Ensures that the config page is accessible for users with the adequate
// permissions and exists the Devel toolbar local task.
$this->drupalLogin($this->develUser);
$this->drupalGet('admin/config/development/devel/toolbar');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->elementExists('css', '.tabs .primary a:contains("Toolbar Settings")');
$this->assertSession()->pageTextContains('Devel Toolbar Settings');
// Ensures and that all devel menu links are listed in the configuration
// page.
foreach ($this->getMenuLinkInfos() as $link) {
$this->assertSession()->fieldExists(sprintf('toolbar_items[%s]', $link['id']));
}
// Ensures and that the default configuration items are selected by
// default.
foreach ($this->defaultToolbarItems as $item) {
$this->assertSession()->checkboxChecked(sprintf('toolbar_items[%s]', $item));
}
// Ensures that the configuration save works as expected.
$edit = [
'toolbar_items[devel.event_info]' => 'devel.event_info',
'toolbar_items[devel.theme_registry]' => 'devel.theme_registry',
];
$this->drupalPostForm('admin/config/development/devel/toolbar', $edit, t('Save configuration'));
$this->assertSession()->pageTextContains('The configuration options have been saved.');
$expected_items = array_merge($this->defaultToolbarItems, ['devel.event_info', 'devel.theme_registry']);
sort($expected_items);
$config_items = \Drupal::config('devel.toolbar.settings')->get('toolbar_items');
sort($config_items);
$this->assertEquals($expected_items, $config_items);
}
/**
* Tests cache metadata headers.
*/
public function testCacheHeaders() {
// Disable user toolbar tab so we can test properly if the devel toolbar
// implementation interferes with the page cacheability.
\Drupal::service('module_installer')->install(['toolbar_disable_user_toolbar']);
// The menu is not loaded for users without the adequate permission,
// so no cache tags for configuration are added.
$this->drupalLogin($this->toolbarUser);
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'config:devel.toolbar.settings');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'config:system.menu.devel');
// Make sure that the configuration cache tags are present for users with
// the adequate permission.
$this->drupalLogin($this->develUser);
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:devel.toolbar.settings');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:system.menu.devel');
// The Devel toolbar implementation should not interfere with the page
// cacheability, so you expect a MISS value in the X-Drupal-Dynamic-Cache
// header the first time.
$this->assertSession()->responseHeaderContains('X-Drupal-Dynamic-Cache', 'MISS');
// Triggers a page reload and verify that the page is served from the
// cache.
$this->drupalGet('');
$this->assertSession()->responseHeaderContains('X-Drupal-Dynamic-Cache', 'HIT');
}
/**
* Tests toolbar integration.
*/
public function testToolbarIntegration() {
$library_css_url = 'devel/css/devel.toolbar.css';
$toolbar_selector = '#toolbar-bar .toolbar-tab';
$toolbar_tab_selector = '#toolbar-bar .toolbar-tab a.toolbar-icon-devel';
$toolbar_tray_selector = '#toolbar-bar .toolbar-tab #toolbar-item-devel-tray';
// Ensures that devel toolbar item is accessible only for user with the
// adequate permissions.
$this->drupalGet('');
$this->assertSession()->responseNotContains($library_css_url);
$this->assertSession()->elementNotExists('css', $toolbar_selector);
$this->assertSession()->elementNotExists('css', $toolbar_tab_selector);
$this->drupalLogin($this->toolbarUser);
$this->assertSession()->responseNotContains($library_css_url);
$this->assertSession()->elementExists('css', $toolbar_selector);
$this->assertSession()->elementNotExists('css', $toolbar_tab_selector);
$this->drupalLogin($this->develUser);
$this->assertSession()->responseContains($library_css_url);
$this->assertSession()->elementExists('css', $toolbar_selector);
$this->assertSession()->elementExists('css', $toolbar_tab_selector);
$this->assertSession()->elementTextContains('css', $toolbar_tab_selector, 'Devel');
// Ensures that the configure link in the toolbar is present and point to
// the correct page.
$this->clickLink('Configure');
$this->assertSession()->addressEquals('admin/config/development/devel/toolbar');
// Ensures that the toolbar tray contains the all the menu links. To the
// links not marked as always visible will be assigned a css class that
// allow to hide they when the toolbar has horizontal orientation.
$this->drupalGet('');
$toolbar_tray = $this->assertSession()->elementExists('css', $toolbar_tray_selector);
$devel_menu_items = $this->getMenuLinkInfos();
$toolbar_items = $toolbar_tray->findAll('css', 'ul.menu a');
$this->assertCount(count($devel_menu_items), $toolbar_items);
foreach ($devel_menu_items as $link) {
$item_selector = sprintf('ul.menu a:contains("%s")', $link['title']);
$item = $this->assertSession()->elementExists('css', $item_selector, $toolbar_tray);
// TODO: find a more correct way to test link url.
$this->assertContains(strtok($link['url'], '?'), $item->getAttribute('href'));
$not_visible = !in_array($link['id'], $this->defaultToolbarItems);
$this->assertTrue($not_visible === $item->hasClass('toolbar-horizontal-item-hidden'));
}
// Ensures that changing the toolbar settings configuration the changes are
// immediately visible.
$saved_items = $this->config('devel.toolbar.settings')->get('toolbar_items');
$saved_items[] = 'devel.event_info';
$this->config('devel.toolbar.settings')
->set('toolbar_items', $saved_items)
->save();
$this->drupalGet('');
$toolbar_tray = $this->assertSession()->elementExists('css', $toolbar_tray_selector);
$item = $this->assertSession()->elementExists('css', sprintf('ul.menu a:contains("%s")', 'Events Info'), $toolbar_tray);
$this->assertFalse($item->hasClass('toolbar-horizontal-item-hidden'));
// Ensures that disabling a menu link it will not more shown in the toolbar
// and that the changes are immediately visible.
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
$menu_link_manager->updateDefinition('devel.event_info', ['enabled' => FALSE]);
$this->drupalGet('');
$toolbar_tray = $this->assertSession()->elementExists('css', $toolbar_tray_selector);
$this->assertSession()->elementNotExists('css', sprintf('ul.menu a:contains("%s")', 'Events Info'), $toolbar_tray);
}
/**
* Tests devel when toolbar module is not installed.
*/
public function testToolbarModuleNotInstalled() {
// Ensures that when toolbar module is not installed all works properly.
\Drupal::service('module_installer')->uninstall(['toolbar']);
$this->drupalLogin($this->develUser);
// Toolbar settings page should respond with 404.
$this->drupalGet('admin/config/development/devel/toolbar');
$this->assertSession()->statusCodeEquals(404);
// Primary local task should not contains toolbar tab.
$this->drupalGet('admin/config/development/devel');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->elementNotExists('css', '.tabs .primary a:contains("Toolbar Settings")');
// Toolbar setting config and devel menu cache tags sholud not present.
$this->drupalGet('');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'config:devel.toolbar.settings');
$this->assertSession()->responseHeaderNotContains('X-Drupal-Cache-Tags', 'config:system.menu.devel');
}
/**
* Helper function for retrieve the menu link informations.
*
* @return array
* An array containing the menu link informations.
*/
protected function getMenuLinkInfos() {
$parameters = new MenuTreeParameters();
$parameters->onlyEnabledLinks()->setTopLevelOnly();
$tree = \Drupal::menuTree()->load('devel', $parameters);
$links = [];
foreach ($tree as $element) {
$links[] = [
'id' => $element->link->getPluginId(),
'title' => $element->link->getTitle(),
'url' => $element->link->getUrlObject()->toString(),
];
}
return $links;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment