From fd027c7068b3da3e5594bb0c8abf8e8f75a465b8 Mon Sep 17 00:00:00 2001
From: lostcarpark <james@lostcarpark.com>
Date: Sat, 23 Nov 2024 17:59:02 +0000
Subject: [PATCH 1/6] Move development menu into separate function to keep
 class readable.

---
 src/Plugin/Derivative/ToolsMenuDeriver.php | 189 ++++++++++++---------
 1 file changed, 104 insertions(+), 85 deletions(-)

diff --git a/src/Plugin/Derivative/ToolsMenuDeriver.php b/src/Plugin/Derivative/ToolsMenuDeriver.php
index 48b866e..6a36d11 100644
--- a/src/Plugin/Derivative/ToolsMenuDeriver.php
+++ b/src/Plugin/Derivative/ToolsMenuDeriver.php
@@ -48,99 +48,118 @@ class ToolsMenuDeriver extends DeriverBase implements ContainerDeriverInterface
   }
 
   /**
-   * {@inheritdoc}
+   * 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.
    */
-  public function getDerivativeDefinitions($base_plugin_definition) {
-    $links = [];
+  protected function buildDevelMenu(array $base_plugin_definition): array {
 
-    // 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',
-      ] + $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',
-      ] + $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',
-      ] + $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',
-      ] + $base_plugin_definition;
-      $links['devel.element_info'] = [
-        'title' => $this->t('Element Info'),
-        'route_name' => 'devel.elements_page',
+    // 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;
+    $lindevelLinksks['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) {
+
+    return [
+      ...$this->buildDevelMenu($base_plugin_definition),
+    ];
 
-    return $links;
   }
 
   /**
-- 
GitLab


From d8b2d01f0de4a0c61d58e58a1d6eb05f98a27d4a Mon Sep 17 00:00:00 2001
From: lostcarpark <james@lostcarpark.com>
Date: Sun, 24 Nov 2024 09:42:03 +0000
Subject: [PATCH 2/6] Accidental type 'lindevelLinksks'.

---
 src/Plugin/Derivative/ToolsMenuDeriver.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Plugin/Derivative/ToolsMenuDeriver.php b/src/Plugin/Derivative/ToolsMenuDeriver.php
index 6a36d11..7e99dca 100644
--- a/src/Plugin/Derivative/ToolsMenuDeriver.php
+++ b/src/Plugin/Derivative/ToolsMenuDeriver.php
@@ -82,7 +82,7 @@ class ToolsMenuDeriver extends DeriverBase implements ContainerDeriverInterface
       'parent' => $base_plugin_definition['id'] . ':devel',
       'weight' => '-30',
     ] + $base_plugin_definition;
-    $lindevelLinksks['devel.reinstall'] = [
+    $develLinks['devel.reinstall'] = [
       'title' => $this->t('Reinstall modules'),
       'route_name' => 'devel.reinstall',
       'parent' => $base_plugin_definition['id'] . ':devel',
-- 
GitLab


From 7b9081a8789801a001d0a6b73c5f48a257064876 Mon Sep 17 00:00:00 2001
From: lostcarpark <james@lostcarpark.com>
Date: Sun, 24 Nov 2024 22:18:57 +0000
Subject: [PATCH 3/6] Add people menu options.

---
 src/Plugin/Derivative/ToolsMenuDeriver.php    | 86 +++++++++++++++++
 .../NavigationExtraToolsUserMenuTest.php      | 93 +++++++++++++++++++
 2 files changed, 179 insertions(+)
 create mode 100644 tests/src/Functional/NavigationExtraToolsUserMenuTest.php

diff --git a/src/Plugin/Derivative/ToolsMenuDeriver.php b/src/Plugin/Derivative/ToolsMenuDeriver.php
index 7e99dca..2222e27 100644
--- a/src/Plugin/Derivative/ToolsMenuDeriver.php
+++ b/src/Plugin/Derivative/ToolsMenuDeriver.php
@@ -47,6 +47,91 @@ class ToolsMenuDeriver extends DeriverBase implements ContainerDeriverInterface
     );
   }
 
+  /**
+   * 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.
+   */
+  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 Navigarion 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;
+      $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;
+      $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;
+    }
+
+    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;
+      /** @todo Add Permission, Delete, and Devel submenus when 4th level supported. */
+    }
+    return $userLinks;
+
+  }
+
   /**
    * Build the development sub-menu to add to the tools menu.
    *
@@ -157,6 +242,7 @@ class ToolsMenuDeriver extends DeriverBase implements ContainerDeriverInterface
   public function getDerivativeDefinitions($base_plugin_definition) {
 
     return [
+      ...$this->buildUserMenu($base_plugin_definition),
       ...$this->buildDevelMenu($base_plugin_definition),
     ];
 
diff --git a/tests/src/Functional/NavigationExtraToolsUserMenuTest.php b/tests/src/Functional/NavigationExtraToolsUserMenuTest.php
new file mode 100644
index 0000000..e40b6b1
--- /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 toolshelp
+
+/**
+ * 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"]');
+  }
+
+}
-- 
GitLab


From e676ab473245c3cfa1caee2e0a58ad35dae7705a Mon Sep 17 00:00:00 2001
From: DDEV User <nobody@example.com>
Date: Sun, 24 Nov 2024 22:40:12 +0000
Subject: [PATCH 4/6] Missed plugin and CSpell fixes.

---
 src/Plugin/Derivative/ToolsMenuDeriver.php |   6 +-
 src/Plugin/Menu/MenuLinkItemEntity.php     | 104 +++++++++++++++++++++
 2 files changed, 108 insertions(+), 2 deletions(-)
 create mode 100644 src/Plugin/Menu/MenuLinkItemEntity.php

diff --git a/src/Plugin/Derivative/ToolsMenuDeriver.php b/src/Plugin/Derivative/ToolsMenuDeriver.php
index 2222e27..94984e6 100644
--- a/src/Plugin/Derivative/ToolsMenuDeriver.php
+++ b/src/Plugin/Derivative/ToolsMenuDeriver.php
@@ -13,6 +13,8 @@ use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
+// cSpell:ignore entityusercollection, systemadmin
+
 /**
  * Deriver class to add extra links to the navigation menus.
  */
@@ -93,7 +95,7 @@ class ToolsMenuDeriver extends DeriverBase implements ContainerDeriverInterface
     ] + $base_plugin_definition;
     // Adds sub-links to Account settings link.
     if ($this->moduleHandler->moduleExists('field_ui')) {
-      /** @todo When Navigarion allows 4 levels, move under Admin->People->Account Settings */
+      // @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',
@@ -126,7 +128,7 @@ class ToolsMenuDeriver extends DeriverBase implements ContainerDeriverInterface
           'entity_id' => $role->id(),
         ],
       ] + $base_plugin_definition;
-      /** @todo Add Permission, Delete, and Devel submenus when 4th level supported. */
+      // @todo Add Permission, Delete, and Devel submenus when 4th level supported.
     }
     return $userLinks;
 
diff --git a/src/Plugin/Menu/MenuLinkItemEntity.php b/src/Plugin/Menu/MenuLinkItemEntity.php
new file mode 100644
index 0000000..79f3453
--- /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();
+  }
+
+}
-- 
GitLab


From a42fee39f0153e2b1fbdade6e6353c376db9b4c6 Mon Sep 17 00:00:00 2001
From: lostcarpark <james@lostcarpark.com>
Date: Sun, 24 Nov 2024 22:56:10 +0000
Subject: [PATCH 5/6] CSpell fix.

---
 src/Plugin/Derivative/ToolsMenuDeriver.php                | 2 --
 tests/src/Functional/NavigationExtraToolsUserMenuTest.php | 2 +-
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/Plugin/Derivative/ToolsMenuDeriver.php b/src/Plugin/Derivative/ToolsMenuDeriver.php
index 94984e6..7d32a55 100644
--- a/src/Plugin/Derivative/ToolsMenuDeriver.php
+++ b/src/Plugin/Derivative/ToolsMenuDeriver.php
@@ -13,8 +13,6 @@ use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
-// cSpell:ignore entityusercollection, systemadmin
-
 /**
  * Deriver class to add extra links to the navigation menus.
  */
diff --git a/tests/src/Functional/NavigationExtraToolsUserMenuTest.php b/tests/src/Functional/NavigationExtraToolsUserMenuTest.php
index e40b6b1..c0d3799 100644
--- a/tests/src/Functional/NavigationExtraToolsUserMenuTest.php
+++ b/tests/src/Functional/NavigationExtraToolsUserMenuTest.php
@@ -7,7 +7,7 @@ namespace Drupal\Tests\navigation_extra_tools\Functional;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\user\UserInterface;
 
-// cSpell:ignore toolshelp
+// cSpell:ignore entityusercollection, systemadmin
 
 /**
  * Test description.
-- 
GitLab


From 4a22b5b1d071ddfcfb9d7341a1adcc7d8a6e5a4e Mon Sep 17 00:00:00 2001
From: lostcarpark <james@lostcarpark.com>
Date: Mon, 25 Nov 2024 00:02:47 +0000
Subject: [PATCH 6/6] Add entity menu items.

---
 src/Plugin/Derivative/ToolsMenuDeriver.php | 89 ++++++++++++++++++++++
 1 file changed, 89 insertions(+)

diff --git a/src/Plugin/Derivative/ToolsMenuDeriver.php b/src/Plugin/Derivative/ToolsMenuDeriver.php
index 7d32a55..d96e7cb 100644
--- a/src/Plugin/Derivative/ToolsMenuDeriver.php
+++ b/src/Plugin/Derivative/ToolsMenuDeriver.php
@@ -47,6 +47,93 @@ 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;
+
+  }
+
   /**
    * Build the user sub-menu options.
    *
@@ -241,7 +328,9 @@ class ToolsMenuDeriver extends DeriverBase implements ContainerDeriverInterface
    */
   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),
     ];
-- 
GitLab