diff --git a/src/Plugin/Derivative/ToolsMenuDeriver.php b/src/Plugin/Derivative/ToolsMenuDeriver.php index 48b866e1ff3d855d403eef7a27eb11bc6a9a0c51..d96e7cb9c505233e7940e7d5acd8fe39287aa2cc 100644 --- a/src/Plugin/Derivative/ToolsMenuDeriver.php +++ b/src/Plugin/Derivative/ToolsMenuDeriver.php @@ -47,100 +47,294 @@ class ToolsMenuDeriver extends DeriverBase implements ContainerDeriverInterface ); } + protected function buildEntityMenus(array $base_plugin_definition): array { + + $entityLinks = []; + + $entity_types = $this->entityTypeManager->getDefinitions(); + $content_entities = []; + foreach ($entity_types as $key => $entity_type) { + if ($entity_type->getBundleEntityType() && ($entity_type->get('field_ui_base_route') != '')) { + $content_entities[$key] = [ + 'content_entity' => $key, + 'content_entity_bundle' => $entity_type->getBundleEntityType(), + ]; + } + } + + // Adds common links to entities. + foreach ($content_entities as $entities) { + $content_entity_bundle = $entities['content_entity_bundle']; + $content_entity = $entities['content_entity']; + $content_entity_bundle_storage = $this->entityTypeManager->getStorage($content_entity_bundle); + $bundles_ids = $content_entity_bundle_storage->getQuery() + ->accessCheck() + ->sort('weight') + ->sort($this->entityTypeManager->getDefinition($content_entity_bundle)->getKey('label')) + ->execute(); + $bundles = $this->entityTypeManager->getStorage($content_entity_bundle)->loadMultiple($bundles_ids); + // if (count($bundles) == $max_bundle_number && $this->routeExists('entity.' . $content_entity_bundle . '.collection')) { + // $entityLinks[$content_entity_bundle . '.collection'] = [ + // 'title' => $this->t('All types'), + // 'route_name' => 'entity.' . $content_entity_bundle . '.collection', + // 'parent' => 'entity.' . $content_entity_bundle . '.collection', + // 'weight' => -999, + // ] + $base_plugin_definition; + // } + foreach ($bundles as $machine_name => $bundle) { + // Normally, the edit form for the bundle would be its root link. + $content_entity_bundle_root = NULL; + if ($this->routeExists('entity.' . $content_entity_bundle . '.overview_form')) { + // Some bundles have an overview/list form that make a better root + // link. + $content_entity_bundle_root = 'entity.' . $content_entity_bundle . '.overview_form.' . $machine_name; + $entityLinks[$content_entity_bundle_root] = [ + 'route_name' => 'entity.' . $content_entity_bundle . '.overview_form', + 'parent' => 'entity.' . $content_entity_bundle . '.collection', + 'route_parameters' => [$content_entity_bundle => $machine_name], + 'class' => 'Drupal\navigation_extra_tools\Plugin\Menu\MenuLinkItemEntity', + 'metadata' => [ + 'entity_type' => $bundle->getEntityTypeId(), + 'entity_id' => $bundle->id(), + ], + ] + $base_plugin_definition; + $weight = $bundles[$machine_name]->get('weight'); + if (isset($weight) && is_numeric($weight)) { + $entityLinks[$content_entity_bundle_root]['weight'] = $weight; + } + } + if ($this->routeExists('entity.' . $content_entity_bundle . '.edit_form')) { + $key = 'entity.' . $content_entity_bundle . '.edit_form.' . $machine_name; + $entityLinks[$key] = [ + 'route_name' => 'entity.' . $content_entity_bundle . '.edit_form', + 'parent' => 'entity.' . $content_entity_bundle . '.collection', + 'route_parameters' => [$content_entity_bundle => $machine_name], + ] + $base_plugin_definition; + if (empty($content_entity_bundle_root)) { + $content_entity_bundle_root = $key; + $entityLinks[$key]['parent'] = 'entity.' . $content_entity_bundle . '.collection'; + + // When not grouped by bundle, use bundle name as title. + $entityLinks[$key]['class'] = 'Drupal\navigation_extra_tools\Plugin\Menu\MenuLinkItemEntity'; + $entityLinks[$key]['metadata'] = [ + 'entity_type' => $bundle->getEntityTypeId(), + 'entity_id' => $bundle->id(), + ]; + } + else { + $entityLinks[$key]['parent'] = $base_plugin_definition['id'] . ':' . $content_entity_bundle_root; + $entityLinks[$key]['title'] = $this->t('Edit'); + } + } + // @todo Add back field UI menus when level 3 Navigation supported. + } + } + + return $entityLinks; + + } + /** - * {@inheritdoc} + * Build the user sub-menu options. + * + * @param array $base_plugin_definition + * The plugin details to add to each menu item. + * + * @return array + * Array of menu items to be added to tools menu. */ - public function getDerivativeDefinitions($base_plugin_definition) { - $links = []; - - // If module Devel is enabled. - if ($this->moduleHandler->moduleExists('devel')) { - $links['devel'] = [ - 'title' => $this->t('Development'), - 'route_name' => 'system.admin_config_development', - 'parent' => 'navigation_extra_tools.help', - 'weight' => '-8', - ] + $base_plugin_definition; - $links['devel.admin_settings'] = [ - 'title' => $this->t('Devel settings'), - 'route_name' => 'devel.admin_settings', - 'parent' => $base_plugin_definition['id'] . ':devel', - 'weight' => '-31', - ] + $base_plugin_definition; - $links['devel.configs_list'] = [ - 'title' => $this->t('Config editor'), - 'route_name' => 'devel.configs_list', - 'parent' => $base_plugin_definition['id'] . ':devel', - 'weight' => '-30', - ] + $base_plugin_definition; - $links['devel.reinstall'] = [ - 'title' => $this->t('Reinstall modules'), - 'route_name' => 'devel.reinstall', - 'parent' => $base_plugin_definition['id'] . ':devel', - 'weight' => '-29', - ] + $base_plugin_definition; - $links['devel.menu_rebuild'] = [ - 'title' => $this->t('Rebuild menu'), - 'route_name' => 'devel.menu_rebuild', - 'parent' => $base_plugin_definition['id'] . ':devel', - 'weight' => '-28', + protected function buildUserMenu(array $base_plugin_definition): array { + + $userLinks = []; + + // Adds user links. + $userLinks['user.admin_create'] = [ + 'title' => $this->t('Add user'), + 'route_name' => 'user.admin_create', + 'parent' => 'entity.user.collection', + 'weight' => -10, + ] + $base_plugin_definition; + $userLinks['user.admin_permissions'] = [ + 'title' => $this->t('Permissions'), + 'route_name' => 'user.admin_permissions', + 'parent' => 'entity.user.collection', + 'weight' => -9, + ] + $base_plugin_definition; + $userLinks['entity.user_role.collection'] = [ + 'title' => $this->t('Roles'), + 'route_name' => 'entity.user_role.collection', + 'parent' => 'entity.user.collection', + 'weight' => -8, + ] + $base_plugin_definition; + $userLinks['entity.user_role.admin'] = [ + 'title' => $this->t('Administer Roles'), + 'route_name' => 'entity.user_role.collection', + 'parent' => $base_plugin_definition['id'] . ':entity.user_role.collection', + 'weight' => -50, + ] + $base_plugin_definition; + $userLinks['user.role_add'] = [ + 'title' => $this->t('Add role'), + 'route_name' => 'user.role_add', + 'parent' => $base_plugin_definition['id'] . ':entity.user_role.collection', + 'weight' => -49, + ] + $base_plugin_definition; + // Adds sub-links to Account settings link. + if ($this->moduleHandler->moduleExists('field_ui')) { + // @todo When Navigation allows 4 levels, move under Admin->People->Account Settings + $userLinks['entity.user.field_ui_fields_'] = [ + 'title' => $this->t('Manage user fields'), + 'route_name' => 'entity.user.field_ui_fields', + 'parent' => 'user.admin_index', + 'weight' => 1, ] + $base_plugin_definition; - $links['devel.state_system_page'] = [ - 'title' => $this->t('State editor'), - 'route_name' => 'devel.state_system_page', - 'parent' => $base_plugin_definition['id'] . ':devel', - 'weight' => '-27', + $userLinks['entity.entity_form_display.user.default_'] = [ + 'title' => $this->t('Manage user form display'), + 'route_name' => 'entity.entity_form_display.user.default', + 'parent' => 'user.admin_index', + 'weight' => 2, ] + $base_plugin_definition; - $links['devel.theme_registry'] = [ - 'title' => $this->t('Theme registry'), - 'route_name' => 'devel.theme_registry', - 'parent' => $base_plugin_definition['id'] . ':devel', - 'weight' => '-26', + $userLinks['entity.entity_view_display.user.default_'] = [ + 'title' => $this->t('Manage user display'), + 'route_name' => 'entity.entity_view_display.user.default', + 'parent' => 'user.admin_index', + 'weight' => 3, ] + $base_plugin_definition; - $links['devel.entity_info_page'] = [ - 'title' => $this->t('Entity info'), - 'route_name' => 'devel.entity_info_page', - 'parent' => $base_plugin_definition['id'] . ':devel', - 'weight' => '-25', - ] + $base_plugin_definition; - $links['devel.session'] = [ - 'title' => $this->t('Session viewer'), - 'route_name' => 'devel.session', - 'parent' => $base_plugin_definition['id'] . ':devel', - 'weight' => '-24', + } + + foreach ($this->entityTypeManager->getStorage('user_role')->loadMultiple() as $role) { + $userLinks['entity.user_role.edit_form.' . $role->id()] = [ + 'route_name' => 'entity.user_role.edit_form', + 'parent' => $base_plugin_definition['id'] . ':entity.user_role.collection', + 'weight' => $role->getWeight(), + 'route_parameters' => ['user_role' => $role->id()], + 'class' => 'Drupal\navigation_extra_tools\Plugin\Menu\MenuLinkItemEntity', + 'metadata' => [ + 'entity_type' => $role->getEntityTypeId(), + 'entity_id' => $role->id(), + ], ] + $base_plugin_definition; - $links['devel.element_info'] = [ - 'title' => $this->t('Element Info'), - 'route_name' => 'devel.elements_page', + // @todo Add Permission, Delete, and Devel submenus when 4th level supported. + } + return $userLinks; + + } + + /** + * Build the development sub-menu to add to the tools menu. + * + * @param array $base_plugin_definition + * The plugin details to add to each menu item. + * + * @return array + * Array of menu items to be added to tools menu. + */ + protected function buildDevelMenu(array $base_plugin_definition): array { + + // If module Devel not enabled, return empty array. + if (!$this->moduleHandler->moduleExists('devel')) { + return []; + } + + $develLinks = []; + $develLinks['devel'] = [ + 'title' => $this->t('Development'), + 'route_name' => 'system.admin_config_development', + 'parent' => 'navigation_extra_tools.help', + 'weight' => '-8', + ] + $base_plugin_definition; + $develLinks['devel.admin_settings'] = [ + 'title' => $this->t('Devel settings'), + 'route_name' => 'devel.admin_settings', + 'parent' => $base_plugin_definition['id'] . ':devel', + 'weight' => '-31', + ] + $base_plugin_definition; + $develLinks['devel.configs_list'] = [ + 'title' => $this->t('Config editor'), + 'route_name' => 'devel.configs_list', + 'parent' => $base_plugin_definition['id'] . ':devel', + 'weight' => '-30', + ] + $base_plugin_definition; + $develLinks['devel.reinstall'] = [ + 'title' => $this->t('Reinstall modules'), + 'route_name' => 'devel.reinstall', + 'parent' => $base_plugin_definition['id'] . ':devel', + 'weight' => '-29', + ] + $base_plugin_definition; + $develLinks['devel.menu_rebuild'] = [ + 'title' => $this->t('Rebuild menu'), + 'route_name' => 'devel.menu_rebuild', + 'parent' => $base_plugin_definition['id'] . ':devel', + 'weight' => '-28', + ] + $base_plugin_definition; + $develLinks['devel.state_system_page'] = [ + 'title' => $this->t('State editor'), + 'route_name' => 'devel.state_system_page', + 'parent' => $base_plugin_definition['id'] . ':devel', + 'weight' => '-27', + ] + $base_plugin_definition; + $develLinks['devel.theme_registry'] = [ + 'title' => $this->t('Theme registry'), + 'route_name' => 'devel.theme_registry', + 'parent' => $base_plugin_definition['id'] . ':devel', + 'weight' => '-26', + ] + $base_plugin_definition; + $develLinks['devel.entity_info_page'] = [ + 'title' => $this->t('Entity info'), + 'route_name' => 'devel.entity_info_page', + 'parent' => $base_plugin_definition['id'] . ':devel', + 'weight' => '-25', + ] + $base_plugin_definition; + $develLinks['devel.session'] = [ + 'title' => $this->t('Session viewer'), + 'route_name' => 'devel.session', + 'parent' => $base_plugin_definition['id'] . ':devel', + 'weight' => '-24', + ] + $base_plugin_definition; + $develLinks['devel.element_info'] = [ + 'title' => $this->t('Element Info'), + 'route_name' => 'devel.elements_page', + 'parent' => $base_plugin_definition['id'] . ':devel', + 'weight' => '-23', + ] + $base_plugin_definition; + // Menu link for the Toolbar module. + $develLinks['devel.toolbar.settings'] = [ + 'title' => $this->t('Devel Toolbar Settings'), + 'route_name' => 'devel.toolbar.settings_form', + 'parent' => $base_plugin_definition['id'] . ':devel', + 'weight' => '-22', + ] + $base_plugin_definition; + if ($this->moduleHandler->moduleExists('webprofiler')) { + $develLinks['devel.webprofiler'] = [ + 'title' => $this->t('Webprofiler settings'), + 'route_name' => 'webprofiler.settings', 'parent' => $base_plugin_definition['id'] . ':devel', - 'weight' => '-23', + 'weight' => '-21', ] + $base_plugin_definition; - // Menu link for the Toolbar module. - $links['devel.toolbar.settings'] = [ - 'title' => $this->t('Devel Toolbar Settings'), - 'route_name' => 'devel.toolbar.settings_form', + } + // If module Devel PHP is enabled. + if ($this->moduleHandler->moduleExists('devel_php') && $this->routeExists('devel_php.execute_php')) { + $develLinks['devel.devel_php.execute_php'] = [ + 'title' => $this->t('Execute PHP Code'), + 'route_name' => 'devel_php.execute_php', 'parent' => $base_plugin_definition['id'] . ':devel', - 'weight' => '-22', ] + $base_plugin_definition; - if ($this->moduleHandler->moduleExists('webprofiler')) { - $links['devel.webprofiler'] = [ - 'title' => $this->t('Webprofiler settings'), - 'route_name' => 'webprofiler.settings', - 'parent' => $base_plugin_definition['id'] . ':devel', - 'weight' => '-21', - ] + $base_plugin_definition; - } - // If module Devel PHP is enabled. - if ($this->moduleHandler->moduleExists('devel_php') && $this->routeExists('devel_php.execute_php')) { - $links['devel.devel_php.execute_php'] = [ - 'title' => $this->t('Execute PHP Code'), - 'route_name' => 'devel_php.execute_php', - 'parent' => $base_plugin_definition['id'] . ':devel', - ] + $base_plugin_definition; - } } + return $develLinks; + + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + + // Call each of the menu builders and combine into a single array. + return [ + ...$this->buildEntityMenus($base_plugin_definition), + ...$this->buildUserMenu($base_plugin_definition), + ...$this->buildDevelMenu($base_plugin_definition), + ]; - return $links; } /** diff --git a/src/Plugin/Menu/MenuLinkItemEntity.php b/src/Plugin/Menu/MenuLinkItemEntity.php new file mode 100644 index 0000000000000000000000000000000000000000..79f34538a226bea01fb4e1057295396a02e66917 --- /dev/null +++ b/src/Plugin/Menu/MenuLinkItemEntity.php @@ -0,0 +1,104 @@ +<?php + +namespace Drupal\navigation_extra_tools\Plugin\Menu; + +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Menu\MenuLinkDefault; +use Drupal\Core\Menu\StaticMenuLinkOverridesInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides a menu link plugins for configuration entities. + */ +class MenuLinkItemEntity extends MenuLinkDefault { + + /** + * Constructs a new MenuLinkItemEntity. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $static_override + * The static override storage. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The menu item entity. + */ + public function __construct( + array $configuration, + $plugin_id, + $plugin_definition, + StaticMenuLinkOverridesInterface $static_override, + protected EntityInterface $entity, + ) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $static_override); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */ + $entity_type_manager = $container->get('entity_type.manager'); + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('menu_link.static.overrides'), + $entity_type_manager->getStorage($plugin_definition['metadata']['entity_type'])->load($plugin_definition['metadata']['entity_id']), + ); + } + + /** + * {@inheritdoc} + */ + public function getTitle() { + if ($this->entity) { + return (string) $this->entity->label(); + } + return $this->pluginDefinition['title'] ?: $this->t('Missing'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + if ($this->entity && method_exists($this->entity, 'getDescription')) { + $description = $this->entity->getDescription(); + } + return $description ?? parent::getDescription(); + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + if ($this->entity) { + return $this->entity->getCacheContexts(); + } + return parent::getCacheContexts(); + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + if ($this->entity) { + return $this->entity->getCacheTags(); + } + return parent::getCacheTags(); + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + if ($this->entity) { + return $this->entity->getCacheMaxAge(); + } + return parent::getCacheMaxAge(); + } + +} diff --git a/tests/src/Functional/NavigationExtraToolsUserMenuTest.php b/tests/src/Functional/NavigationExtraToolsUserMenuTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c0d3799b7c1cd98a23a3d1f25d16793133f30b5a --- /dev/null +++ b/tests/src/Functional/NavigationExtraToolsUserMenuTest.php @@ -0,0 +1,93 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\navigation_extra_tools\Functional; + +use Drupal\Tests\BrowserTestBase; +use Drupal\user\UserInterface; + +// cSpell:ignore entityusercollection, systemadmin + +/** + * Test description. + * + * @group navigation_extra_tools + */ +final class NavigationExtraToolsUserMenuTest extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'navigation', + 'navigation_extra_tools', + 'field_ui', + ]; + + /** + * A test user with permission to access the administrative toolbar. + * + * @var \Drupal\user\UserInterface + */ + protected UserInterface $adminUser; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + // Create and log in an administrative user. + $this->adminUser = $this->drupalCreateUser([ + 'access navigation', + 'access administration pages', + 'administer account settings', + 'administer user fields', + 'administer user form display', + 'administer user display', + 'administer users', + 'administer permissions', + ]); + $this->drupalLogin($this->adminUser); + } + + /** + * Test the People menu. + */ + public function testPeopleMenu(): void { + $this->drupalGet('admin'); + // Test that Roles menu under People now has a button. + $this->assertSession()->elementExists('xpath', '//li[@id="navigation-link-entityusercollection"]/div/ul/li[contains(@class, "toolbar-menu__item--level-1")]/button[contains(@class, "toolbar-button")]/span[text() = "Roles"]'); + // Test that "Administer Roles" exists as level 2 menu under People. + $this->assertSession()->elementExists('xpath', '//li[@id="navigation-link-entityusercollection"]/div/ul/li/ul/li[contains(@class, "toolbar-menu__item--level-2")]/a[contains(@class, "toolbar-menu__link--2") and text() = "Administer Roles"]'); + // Test that "Add role" exists as level 2 menu under People. + $this->assertSession()->elementExists('xpath', '//li[@id="navigation-link-entityusercollection"]/div/ul/li/ul/li[contains(@class, "toolbar-menu__item--level-2")]/a[contains(@class, "toolbar-menu__link--2") and text() = "Add role"]'); + // Test that "Anonymous user" exists as level 2 menu under People. + $this->assertSession()->elementExists('xpath', '//li[@id="navigation-link-entityusercollection"]/div/ul/li/ul/li[contains(@class, "toolbar-menu__item--level-2")]/a[contains(@class, "toolbar-menu__link--2") and text() = "Anonymous user"]'); + // Test that "Authenticated user" exists as level 2 menu under People. + $this->assertSession()->elementExists('xpath', '//li[@id="navigation-link-entityusercollection"]/div/ul/li/ul/li[contains(@class, "toolbar-menu__item--level-2")]/a[contains(@class, "toolbar-menu__link--2") and text() = "Authenticated user"]'); + } + + /** + * Test the Config->People menu. + */ + public function testConfigPeopleMenu(): void { + $this->drupalGet('admin'); + // Test that Roles menu under People now has a button. + $this->assertSession()->elementExists('xpath', '//li[@id="navigation-link-systemadmin-config"]/div/ul/li[contains(@class, "toolbar-menu__item--level-1")]/button[contains(@class, "toolbar-button")]/span[text() = "People"]'); + // Test that "Administer Roles" exists as level 2 menu under People. + $this->assertSession()->elementExists('xpath', '//li[@id="navigation-link-systemadmin-config"]/div/ul/li/ul/li[contains(@class, "toolbar-menu__item--level-2")]/a[contains(@class, "toolbar-menu__link--2") and text() = "Account settings"]'); + // Test that "Add role" exists as level 2 menu under People. + $this->assertSession()->elementExists('xpath', '//li[@id="navigation-link-systemadmin-config"]/div/ul/li/ul/li[contains(@class, "toolbar-menu__item--level-2")]/a[contains(@class, "toolbar-menu__link--2") and text() = "Manage user fields"]'); + // Test that "Anonymous user" exists as level 2 menu under People. + $this->assertSession()->elementExists('xpath', '//li[@id="navigation-link-systemadmin-config"]/div/ul/li/ul/li[contains(@class, "toolbar-menu__item--level-2")]/a[contains(@class, "toolbar-menu__link--2") and text() = "Manage user form display"]'); + // Test that "Authenticated user" exists as level 2 menu under People. + $this->assertSession()->elementExists('xpath', '//li[@id="navigation-link-systemadmin-config"]/div/ul/li/ul/li[contains(@class, "toolbar-menu__item--level-2")]/a[contains(@class, "toolbar-menu__link--2") and text() = "Manage user display"]'); + } + +}