From 02975bd7d29b8291bc908bb1edab7677bd724f16 Mon Sep 17 00:00:00 2001
From: Yash Rode <57207-yash.rode@users.noreply.drupalcode.org>
Date: Wed, 21 Aug 2024 16:17:08 +0000
Subject: [PATCH] Issue #3466307 by yash.rode, phenaproxima, chrisfromredfin,
 prashant.c: Expose the project browser as a render element

---
 src/Controller/BrowserController.php          | 102 +---------
 src/Element/ProjectBrowser.php                | 192 ++++++++++++++++++
 .../ProjectBrowserUiTestJsonApi.php           |   2 +-
 3 files changed, 196 insertions(+), 100 deletions(-)
 create mode 100644 src/Element/ProjectBrowser.php

diff --git a/src/Controller/BrowserController.php b/src/Controller/BrowserController.php
index a17dcbe2f..73941c43b 100644
--- a/src/Controller/BrowserController.php
+++ b/src/Controller/BrowserController.php
@@ -3,12 +3,6 @@
 namespace Drupal\project_browser\Controller;
 
 use Drupal\Core\Controller\ControllerBase;
-use Drupal\project_browser\DevelopmentStatus;
-use Drupal\project_browser\EnabledSourceHandler;
-use Drupal\project_browser\InstallReadiness;
-use Drupal\project_browser\MaintenanceStatus;
-use Drupal\project_browser\SecurityStatus;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 
 // cspell:ignore ctools
 
@@ -20,21 +14,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
  */
 class BrowserController extends ControllerBase {
 
-  public function __construct(
-    private readonly EnabledSourceHandler $enabledSource,
-    private readonly ?InstallReadiness $installReadiness,
-  ) {}
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get(EnabledSourceHandler::class),
-      $container->get(InstallReadiness::class, ContainerInterface::NULL_ON_INVALID_REFERENCE),
-    );
-  }
-
   /**
    * Builds the browse page and the individual module page.
    *
@@ -55,85 +34,10 @@ class BrowserController extends ControllerBase {
    *   A render array.
    */
   public function browse(?string $source, ?string $id): array {
-    $current_sources = $this->enabledSource->getCurrentSources();
-    $ui_install_enabled = (bool) $this->config('project_browser.admin_settings')->get('allow_ui_install') && is_object($this->installReadiness);
-
-    $package_manager = [
-      'available' => $ui_install_enabled,
-      'errors' => [],
-      'warnings' => [],
-      // This is just for testing and can probably be removed in
-      // https://www.drupal.org/project/project_browser/issues/3457682.
-      // @see \Drupal\Tests\project_browser\FunctionalJavascript\ProjectBrowserInstallerUiTest::testPackageManagerErrorPreventsDownload()
-      'status_checked' => FALSE,
-    ];
-
-    if (empty($source) || empty($id)) {
-      // If Package Manager is available, only run status checks if we're not
-      // looking at a particular project.
-      if ($package_manager['available']) {
-        $package_manager = array_merge($package_manager, $this->installReadiness->validatePackageManager());
-        $package_manager['status_checked'] = TRUE;
-      }
-    }
-
-    $current_sources_keys = array_keys($current_sources);
-    // To get common data from single source plugin.
-    $current_source = reset($current_sources);
-
-    $sort_options = $active_plugins = [];
-    foreach ($current_sources as $source) {
-      $sort_options[$source->getPluginId()] = array_values($source->getSortOptions());
-      $active_plugins[$source->getPluginId()] = $source->getPluginDefinition()['label'];
-    }
-
-    return [
-      '#theme' => 'project_browser_main_app',
-      '#attached' => [
-        'library' => [
-          'project_browser/svelte',
-        ],
-        'drupalSettings' => [
-          'project_browser' => [
-            'active_plugins' => $active_plugins,
-            'module_path' => $this->moduleHandler()->getModule('project_browser')->getPath(),
-            'special_ids' => $this->getSpecialIds(),
-            'sort_options' => $sort_options,
-            'maintenance_options' => MaintenanceStatus::asOptions(),
-            'security_options' => SecurityStatus::asOptions(),
-            'development_options' => DevelopmentStatus::asOptions(),
-            'default_plugin_id' => $current_source->getPluginId(),
-            'current_sources_keys' => $current_sources_keys,
-            'package_manager' => $package_manager,
-          ],
-        ],
-      ],
-    ];
-  }
-
-  /**
-   * Return special IDs for some vocabularies.
-   *
-   * This is needed because these two vocabularies have a special term
-   * in them that shows an icon next to the label, so we need to be
-   * explicit about these special cases.
-   *
-   * @return array
-   *   List of special IDs per vocabulary.
-   */
-  protected function getSpecialIds(): array {
-    $maintained = MaintenanceStatus::Maintained;
-    $covered = SecurityStatus::Covered;
     return [
-      'maintenance_status' => [
-        'id' => $maintained->value,
-        'name' => $maintained->label(),
-      ],
-      'security_coverage' => [
-        'id' => $covered->value,
-        'name' => $covered->label(),
-      ],
-      'all_values' => MaintenanceStatus::All->value,
+      '#type' => 'project_browser',
+      '#source' => $source,
+      '#id' => $id,
     ];
   }
 
diff --git a/src/Element/ProjectBrowser.php b/src/Element/ProjectBrowser.php
new file mode 100644
index 000000000..2646cf1b0
--- /dev/null
+++ b/src/Element/ProjectBrowser.php
@@ -0,0 +1,192 @@
+<?php
+
+namespace Drupal\project_browser\Element;
+
+use Drupal\Component\Utility\DeprecationHelper;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Render\Attribute\RenderElement;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Render\Element\ElementInterface;
+use Drupal\Core\Render\Element\RenderElementBase;
+use Drupal\project_browser\DevelopmentStatus;
+use Drupal\project_browser\EnabledSourceHandler;
+use Drupal\project_browser\InstallReadiness;
+use Drupal\project_browser\MaintenanceStatus;
+use Drupal\project_browser\SecurityStatus;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a render element for the Project Browser.
+ *
+ * @RenderElement("project_browser")
+ */
+#[RenderElement('project_browser')]
+final class ProjectBrowser implements ElementInterface, ContainerFactoryPluginInterface {
+
+  use DependencySerializationTrait;
+
+  public function __construct(
+    private readonly string $pluginId,
+    private readonly mixed $pluginDefinition,
+    private readonly EnabledSourceHandler $enabledSourceHandler,
+    private readonly ?InstallReadiness $installReadiness,
+    private readonly ModuleHandlerInterface $moduleHandler,
+    private readonly ConfigFactoryInterface $configFactory,
+  ) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginId(): string {
+    return $this->pluginId;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginDefinition(): mixed {
+    return $this->pluginDefinition;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
+    return new static(
+      $plugin_id,
+      $plugin_definition,
+      $container->get(EnabledSourceHandler::class),
+      $container->get(InstallReadiness::class, ContainerInterface::NULL_ON_INVALID_REFERENCE),
+      $container->get(ModuleHandlerInterface::class),
+      $container->get(ConfigFactoryInterface::class),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo(): array {
+    return [
+      '#theme' => 'project_browser_main_app',
+      '#attached' => [
+        'library' => [
+          'project_browser/svelte',
+        ],
+        'drupalSettings' => [
+          'project_browser' => [],
+        ],
+      ],
+      '#pre_render' => [
+        [$this, 'attachProjectBrowserSettings'],
+      ],
+    ];
+  }
+
+  /**
+   * Prepares a render element for Project Browser.
+   *
+   * @param array $element
+   *   A render element array.
+   *
+   * @return array
+   *   The render element array.
+   */
+  public function attachProjectBrowserSettings(array $element): array {
+    $element['#attached']['drupalSettings']['project_browser'] = $this->getDrupalSettings(
+      $element['#source'] ?? NULL,
+      $element['#id'] ?? NULL
+    );
+    return $element;
+  }
+
+  /**
+   * Gets the Drupal settings for the Project Browser.
+   *
+   * @param string|null $source
+   *   If viewing a specific project, the ID of its source plugin.
+   * @param string|null $id
+   *   If viewing a specific project, the project's local ID (as known to the
+   *   source plugin).
+   *
+   * @return array
+   *   An array of Drupal settings.
+   */
+  private function getDrupalSettings(?string $source, ?string $id): array {
+    $current_sources = $this->enabledSourceHandler->getCurrentSources();
+    $ui_install_enabled = (bool) $this->configFactory->get('project_browser.admin_settings')->get('allow_ui_install') && is_object($this->installReadiness);
+
+    $package_manager = [
+      'available' => $ui_install_enabled,
+      'errors' => [],
+      'warnings' => [],
+      'status_checked' => FALSE,
+    ];
+
+    if (empty($source) || empty($id)) {
+      if ($package_manager['available']) {
+        $package_manager = array_merge($package_manager, $this->installReadiness->validatePackageManager());
+        $package_manager['status_checked'] = TRUE;
+      }
+    }
+
+    $current_sources_keys = array_keys($current_sources);
+    $current_source = reset($current_sources);
+
+    $sort_options = $active_plugins = [];
+    foreach ($current_sources as $source) {
+      $sort_options[$source->getPluginId()] = array_values($source->getSortOptions());
+      $active_plugins[$source->getPluginId()] = $source->getPluginDefinition()['label'];
+    }
+
+    return [
+      'active_plugins' => $active_plugins,
+      'module_path' => $this->moduleHandler->getModule('project_browser')->getPath(),
+      'special_ids' => static::getSpecialIds(),
+      'sort_options' => $sort_options,
+      'maintenance_options' => MaintenanceStatus::asOptions(),
+      'security_options' => SecurityStatus::asOptions(),
+      'development_options' => DevelopmentStatus::asOptions(),
+      'default_plugin_id' => $current_source->getPluginId(),
+      'current_sources_keys' => $current_sources_keys,
+      'package_manager' => $package_manager,
+    ];
+  }
+
+  /**
+   * Return special IDs for some vocabularies.
+   *
+   * @return array
+   *   List of special IDs per vocabulary.
+   */
+  private static function getSpecialIds(): array {
+    $maintained = MaintenanceStatus::Maintained;
+    $covered = SecurityStatus::Covered;
+    return [
+      'maintenance_status' => [
+        'id' => $maintained->value,
+        'name' => $maintained->label(),
+      ],
+      'security_coverage' => [
+        'id' => $covered->value,
+        'name' => $covered->label(),
+      ],
+      'all_values' => MaintenanceStatus::All->value,
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function setAttributes(&$element, $class = []): void {
+    DeprecationHelper::backwardsCompatibleCall(
+      \Drupal::VERSION,
+      '10.3',
+      static fn () => RenderElementBase::setAttributes($element, $class),
+      static fn () => Element::setAttributes($element, $class)
+    );
+  }
+
+}
diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php
index 02343fdea..97371ef55 100644
--- a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php
+++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php
@@ -227,7 +227,7 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase {
     $this->clickWithWait('#acc38507-ac85-43e6-8f32-beb3febea93f', bypass_wait: TRUE);
 
     // Click 'Utility' checkbox.
-    $this->clickWithWait('#fddb4569-cb89-42f5-8699-182b10234dfa', '557 Results');
+    $this->clickWithWait('#fddb4569-cb89-42f5-8699-182b10234dfa', '557 Results', TRUE);
     $this->assertPagerItems(['1', '2', '3', '4', '5', '…', 'Next', 'Last']);
   }
 
-- 
GitLab