From efcfba6df1e029629276d0a0d61059ba8ce51f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 18 Feb 2025 18:23:17 -0500 Subject: [PATCH 01/45] Sketch in the normalizer --- project_browser.services.yml | 1 + .../ProjectBrowserEndpointController.php | 10 ++-- src/ProjectBrowser/Normalizer.php | 54 +++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 src/ProjectBrowser/Normalizer.php diff --git a/project_browser.services.yml b/project_browser.services.yml index 4e2e42215..05c3bcd98 100644 --- a/project_browser.services.yml +++ b/project_browser.services.yml @@ -41,3 +41,4 @@ services: public: false tags: - { name: paramconverter } + Drupal\project_browser\ProjectBrowser\Normalizer: ~ diff --git a/src/Controller/ProjectBrowserEndpointController.php b/src/Controller/ProjectBrowserEndpointController.php index c5e58fb2f..0f13431e1 100644 --- a/src/Controller/ProjectBrowserEndpointController.php +++ b/src/Controller/ProjectBrowserEndpointController.php @@ -3,14 +3,15 @@ namespace Drupal\project_browser\Controller; use Drupal\Core\Controller\ControllerBase; -use Drupal\project_browser\ActivationManager; use Drupal\project_browser\EnabledSourceHandler; +use Drupal\project_browser\ProjectBrowser\Normalizer; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; /** * Controller for the proxy layer. @@ -19,7 +20,7 @@ final class ProjectBrowserEndpointController extends ControllerBase { public function __construct( private readonly EnabledSourceHandler $enabledSource, - private readonly ActivationManager $activationManager, + private readonly NormalizerInterface $normalizer, ) {} /** @@ -28,7 +29,7 @@ final class ProjectBrowserEndpointController extends ControllerBase { public static function create(ContainerInterface $container): static { return new static( $container->get(EnabledSourceHandler::class), - $container->get(ActivationManager::class), + $container->get(Normalizer::class), ); } @@ -51,7 +52,7 @@ final class ProjectBrowserEndpointController extends ControllerBase { } $results = $this->enabledSource->getProjects($query['source'], $query); - return new JsonResponse($this->prepareResults($results)); + return new JsonResponse($this->normalizer->normalize($results)); } /** @@ -65,6 +66,7 @@ final class ProjectBrowserEndpointController extends ControllerBase { * all projects. */ private function prepareResults(ProjectsResultsPage $results): array { + // @todo Move all of this to the normalizer. $data = $results->toArray(); // Add activation info to all the projects in the result set, and fully diff --git a/src/ProjectBrowser/Normalizer.php b/src/ProjectBrowser/Normalizer.php new file mode 100644 index 000000000..f84c3a2b4 --- /dev/null +++ b/src/ProjectBrowser/Normalizer.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\project_browser\ProjectBrowser; + +use Drupal\project_browser\ActivationManager; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +final class Normalizer implements NormalizerInterface { + + public function __construct( + private readonly ActivationManager $activationManager, + ) {} + + /** + * {@inheritdoc} + */ + public function normalize(mixed $data, ?string $format = NULL, array $context = []): array { + if ($data instanceof Project) { + assert(array_key_exists('source', $context)); + $data = $this->activationManager->getActivationInfo($data) + $data->toArray(); + $data['id'] = $context['source'] . '/' . $data['id']; + } + elseif ($data instanceof ProjectsResultsPage) { + $context['source'] = $data->pluginId; + + $data = $data->toArray(); + $data['list'] = array_map( + fn (Project $project): array => $this->normalize($project, $format, $context), + $data['list'], + ); + } + return $data; + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization(mixed $data, ?string $format = NULL, array $context = []): bool { + return $data instanceof Project || $data instanceof ProjectsResultsPage; + } + + /** + * {@inheritdoc} + */ + public function getSupportedTypes(?string $format): array { + return [ + Project::class => TRUE, + ProjectsResultsPage::class => TRUE, + ]; + } + +} -- GitLab From 11115eae3f625b36e6f67bd4c80cf6e9d217317d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 18 Feb 2025 18:56:15 -0500 Subject: [PATCH 02/45] Remove prepareResults() --- .../ProjectBrowserEndpointController.php | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/src/Controller/ProjectBrowserEndpointController.php b/src/Controller/ProjectBrowserEndpointController.php index 0f13431e1..1c0dd781d 100644 --- a/src/Controller/ProjectBrowserEndpointController.php +++ b/src/Controller/ProjectBrowserEndpointController.php @@ -5,8 +5,6 @@ namespace Drupal\project_browser\Controller; use Drupal\Core\Controller\ControllerBase; use Drupal\project_browser\EnabledSourceHandler; use Drupal\project_browser\ProjectBrowser\Normalizer; -use Drupal\project_browser\ProjectBrowser\Project; -use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -55,33 +53,6 @@ final class ProjectBrowserEndpointController extends ControllerBase { return new JsonResponse($this->normalizer->normalize($results)); } - /** - * Prepares a set of results to be delivered to the front end. - * - * @param \Drupal\project_browser\ProjectBrowser\ProjectsResultsPage $results - * A page of query results. - * - * @return array - * The query results, with activation info and fully qualified IDs added to - * all projects. - */ - private function prepareResults(ProjectsResultsPage $results): array { - // @todo Move all of this to the normalizer. - $data = $results->toArray(); - - // Add activation info to all the projects in the result set, and fully - // qualify the project IDs by prefixing them with the source plugin ID. - $mapper = function (Project $project) use ($results): array { - $data = $this->activationManager->getActivationInfo($project) + $project->toArray(); - // Always send a fully qualified project ID to the front end. - $data['id'] = $results->pluginId . '/' . $project->id; - return $data; - }; - $data['list'] = array_map($mapper, $data['list']); - - return $data; - } - /** * Builds the query based on the current request. * -- GitLab From c7c50036d5f56a6f27d8ee79fadfc947f1bad2a4 Mon Sep 17 00:00:00 2001 From: narendra-drupal <87118318+narendra-drupal@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:02:46 +0530 Subject: [PATCH 03/45] getActivationInfo moved to Normalizer --- src/ActivationManager.php | 65 +------------------------------ src/ProjectBrowser/Normalizer.php | 65 ++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/src/ActivationManager.php b/src/ActivationManager.php index 91e78e68d..53adf27d0 100644 --- a/src/ActivationManager.php +++ b/src/ActivationManager.php @@ -4,13 +4,8 @@ declare(strict_types=1); namespace Drupal\project_browser; -use Drupal\Component\Utility\Xss; -use Drupal\Core\Link; -use Drupal\Core\Render\RendererInterface; use Drupal\project_browser\Activator\ActivationStatus; use Drupal\project_browser\Activator\ActivatorInterface; -use Drupal\project_browser\Activator\InstructionsInterface; -use Drupal\project_browser\Activator\TasksInterface; use Drupal\project_browser\ProjectBrowser\Project; use Symfony\Component\HttpFoundation\Response; @@ -29,10 +24,6 @@ final class ActivationManager { */ private array $activators = []; - public function __construct( - private readonly RendererInterface $renderer, - ) {} - /** * Registers an activator. * @@ -71,7 +62,7 @@ final class ActivationManager { * @throws \InvalidArgumentException * Thrown if none of the registered activators can handle the given project. */ - private function getActivatorForProject(Project $project): ActivatorInterface { + public function getActivatorForProject(Project $project): ActivatorInterface { foreach ($this->activators as $activator) { if ($activator->supports($project)) { return $activator; @@ -80,60 +71,6 @@ final class ActivationManager { throw new \InvalidArgumentException("The project '$project->machineName' is not supported by any registered activators."); } - /** - * Gets activation information for a project, for delivery to the front-end. - * - * @param \Drupal\project_browser\ProjectBrowser\Project $project - * A project object. - * - * @return array - * An array of activation information. Will consist of: - * - `status`: The activation status of the project on the current site. - * Will be the lowercase name of the one of the cases of - * \Drupal\project_browser\Activator\ActivationStatus. - * - `commands`: The instructions a human can take to activate the project - * manually, or a URL where they can do so. Will be NULL if the registered - * activator which supports the given project is not capable of generating - * instructions. - * - `tasks`: An array of \Drupal\Core\Link objects for specific follow-up - * tasks that a user can take after activating this project. For example, - * could include a link to a module's configuration form, or a dashboard - * provided by a recipe. - * - * @see \Drupal\project_browser\ProjectBrowser\Project::toArray() - */ - public function getActivationInfo(Project $project): array { - $activator = $this->getActivatorForProject($project); - $data = [ - 'status' => strtolower($activator->getStatus($project)->name), - 'commands' => NULL, - 'tasks' => [], - ]; - - if ($activator instanceof InstructionsInterface) { - $data['commands'] = Xss::filter( - $activator->getInstructions($project), - [...Xss::getAdminTagList(), 'textarea', 'button'], - ); - } - - if ($activator instanceof TasksInterface) { - $map = function (Link $link): array { - $text = $link->getText(); - if (is_array($text)) { - $text = $this->renderer->renderInIsolation($text); - } - return [ - 'text' => (string) $text, - 'url' => $link->getUrl()->setAbsolute()->toString(), - ]; - }; - $data['tasks'] = array_values(array_map($map, $activator->getTasks($project))); - } - - return $data; - } - /** * Activates a project on the current site. * diff --git a/src/ProjectBrowser/Normalizer.php b/src/ProjectBrowser/Normalizer.php index f84c3a2b4..eafa7431c 100644 --- a/src/ProjectBrowser/Normalizer.php +++ b/src/ProjectBrowser/Normalizer.php @@ -4,13 +4,22 @@ declare(strict_types=1); namespace Drupal\project_browser\ProjectBrowser; +use Drupal\Component\Utility\Xss; +use Drupal\Core\Link; +use Drupal\Core\Render\RendererInterface; use Drupal\project_browser\ActivationManager; +use Drupal\project_browser\Activator\InstructionsInterface; +use Drupal\project_browser\Activator\TasksInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +/** + * Normalizes Project and ProjectsResultsPage objects into an array format. + */ final class Normalizer implements NormalizerInterface { public function __construct( private readonly ActivationManager $activationManager, + private readonly RendererInterface $renderer, ) {} /** @@ -19,7 +28,7 @@ final class Normalizer implements NormalizerInterface { public function normalize(mixed $data, ?string $format = NULL, array $context = []): array { if ($data instanceof Project) { assert(array_key_exists('source', $context)); - $data = $this->activationManager->getActivationInfo($data) + $data->toArray(); + $data = $this->getActivationInfo($data) + $data->toArray(); $data['id'] = $context['source'] . '/' . $data['id']; } elseif ($data instanceof ProjectsResultsPage) { @@ -51,4 +60,58 @@ final class Normalizer implements NormalizerInterface { ]; } + /** + * Gets activation information for a project, for delivery to the front-end. + * + * @param \Drupal\project_browser\ProjectBrowser\Project $project + * A project object. + * + * @return array + * An array of activation information. Will consist of: + * - `status`: The activation status of the project on the current site. + * Will be the lowercase name of the one of the cases of + * \Drupal\project_browser\Activator\ActivationStatus. + * - `commands`: The instructions a human can take to activate the project + * manually, or a URL where they can do so. Will be NULL if the registered + * activator which supports the given project is not capable of generating + * instructions. + * - `tasks`: An array of \Drupal\Core\Link objects for specific follow-up + * tasks that a user can take after activating this project. For example, + * could include a link to a module's configuration form, or a dashboard + * provided by a recipe. + * + * @see \Drupal\project_browser\ProjectBrowser\Project::toArray() + */ + private function getActivationInfo(Project $project): array { + $activator = $this->activationManager->getActivatorForProject($project); + $data = [ + 'status' => strtolower($activator->getStatus($project)->name), + 'commands' => NULL, + 'tasks' => [], + ]; + + if ($activator instanceof InstructionsInterface) { + $data['commands'] = Xss::filter( + $activator->getInstructions($project), + [...Xss::getAdminTagList(), 'textarea', 'button'], + ); + } + + if ($activator instanceof TasksInterface) { + $map = function (Link $link): array { + $text = $link->getText(); + if (is_array($text)) { + $text = $this->renderer->renderInIsolation($text); + } + return [ + 'text' => (string) $text, + 'url' => $link->getUrl()->setAbsolute()->toString(), + ]; + }; + $data['tasks'] = array_values(array_map($map, $activator->getTasks($project))); + } + + return $data; + } + } -- GitLab From ff90a1f468e245a0bd3e63ba50367ea5454ce17d Mon Sep 17 00:00:00 2001 From: narendra-drupal <87118318+narendra-drupal@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:59:44 +0530 Subject: [PATCH 04/45] return normalized projects in activate response --- src/Controller/InstallerController.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 63de17042..51fa14308 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -12,13 +12,14 @@ use Drupal\project_browser\ActivationManager; use Drupal\project_browser\ComposerInstaller\Installer; use Drupal\project_browser\EnabledSourceHandler; use Drupal\project_browser\InstallState; +use Drupal\project_browser\ProjectBrowser\Normalizer; use Drupal\system\SystemManager; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -43,6 +44,7 @@ final class InstallerController extends ControllerBase { private readonly ActivationManager $activationManager, private readonly InstallState $installState, private readonly EventDispatcherInterface $eventDispatcher, + private readonly NormalizerInterface $normalizer, ) {} /** @@ -61,6 +63,7 @@ final class InstallerController extends ControllerBase { $container->get(ActivationManager::class), $container->get(InstallState::class), $container->get(EventDispatcherInterface::class), + $container->get(Normalizer::class), ); } @@ -392,21 +395,23 @@ final class InstallerController extends ControllerBase { } /** - * Installs an already downloaded module. + * Installs an already downloaded project. * * @param \Symfony\Component\HttpFoundation\Request $request * The request. * - * @return \Symfony\Component\HttpFoundation\Response - * Status message. + * @return \Symfony\Component\HttpFoundation\JsonResponse + * Returns normalized activated project data or an error message. */ - public function activate(Request $request): Response { + public function activate(Request $request): JsonResponse { + $normalized_projects = []; foreach ($request->toArray() as $project_id) { $this->installState->setState($project_id, 'activating'); try { $project = $this->enabledSourceHandler->getStoredProject($project_id); - $response = $this->activationManager->activate($project); + $this->activationManager->activate($project); $this->installState->setState($project_id, 'installed'); + $normalized_projects[] = $this->normalizer->normalize($project); } catch (\Throwable $e) { return $this->errorResponse($e, 'project install'); @@ -415,7 +420,7 @@ final class InstallerController extends ControllerBase { $this->installState->deleteAll(); } } - return $response ?? new JsonResponse(['status' => 0]); + return new JsonResponse($normalized_projects); } /** -- GitLab From eeae74d973f3cc3704ebc736f03173132217897a Mon Sep 17 00:00:00 2001 From: narendra-drupal <87118318+narendra-drupal@users.noreply.github.com> Date: Wed, 19 Feb 2025 17:47:45 +0530 Subject: [PATCH 05/45] Added source --- src/Controller/InstallerController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 51fa14308..09c8419c7 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -409,9 +409,10 @@ final class InstallerController extends ControllerBase { $this->installState->setState($project_id, 'activating'); try { $project = $this->enabledSourceHandler->getStoredProject($project_id); + $plugin_id = strstr($project_id, '/', true); $this->activationManager->activate($project); $this->installState->setState($project_id, 'installed'); - $normalized_projects[] = $this->normalizer->normalize($project); + $normalized_projects[] = $this->normalizer->normalize($project, 'json', ['source' => $plugin_id]); } catch (\Throwable $e) { return $this->errorResponse($e, 'project install'); -- GitLab From b0bf4669a557cc11454359418717e7bba2d41048 Mon Sep 17 00:00:00 2001 From: narendra-drupal <87118318+narendra-drupal@users.noreply.github.com> Date: Wed, 19 Feb 2025 17:53:10 +0530 Subject: [PATCH 06/45] PHPCS --- src/Controller/InstallerController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 09c8419c7..9bd0b9f5d 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -409,7 +409,7 @@ final class InstallerController extends ControllerBase { $this->installState->setState($project_id, 'activating'); try { $project = $this->enabledSourceHandler->getStoredProject($project_id); - $plugin_id = strstr($project_id, '/', true); + $plugin_id = strstr($project_id, '/', TRUE); $this->activationManager->activate($project); $this->installState->setState($project_id, 'installed'); $normalized_projects[] = $this->normalizer->normalize($project, 'json', ['source' => $plugin_id]); -- GitLab From 8b908709b4a8316d0f7bba6a54c0bc676b93e370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 14:52:36 -0500 Subject: [PATCH 07/45] Make all activators return an array of AJAX commands --- src/ActivationManager.php | 9 ++++----- src/Activator/ActivatorInterface.php | 9 +++------ src/Activator/ModuleActivator.php | 8 ++------ src/Activator/RecipeActivator.php | 19 ++++++------------- 4 files changed, 15 insertions(+), 30 deletions(-) diff --git a/src/ActivationManager.php b/src/ActivationManager.php index 53adf27d0..b2a89d789 100644 --- a/src/ActivationManager.php +++ b/src/ActivationManager.php @@ -7,7 +7,6 @@ namespace Drupal\project_browser; use Drupal\project_browser\Activator\ActivationStatus; use Drupal\project_browser\Activator\ActivatorInterface; use Drupal\project_browser\ProjectBrowser\Project; -use Symfony\Component\HttpFoundation\Response; /** * A generalized activator that can handle any type of project. @@ -77,11 +76,11 @@ final class ActivationManager { * @param \Drupal\project_browser\ProjectBrowser\Project $project * The project to activate. * - * @return \Symfony\Component\HttpFoundation\Response|null - * The response, or lack thereof, returned by the first registered activator - * that supports the given project. + * @return \Drupal\Core\Ajax\CommandInterface[]|null + * The AJAX commands, or lack thereof, returned by the first registered + * activator that supports the given project. */ - public function activate(Project $project): ?Response { + public function activate(Project $project): ?array { return $this->getActivatorForProject($project)->activate($project); } diff --git a/src/Activator/ActivatorInterface.php b/src/Activator/ActivatorInterface.php index 5fc5a8639..ea28361b7 100644 --- a/src/Activator/ActivatorInterface.php +++ b/src/Activator/ActivatorInterface.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Drupal\project_browser\Activator; use Drupal\project_browser\ProjectBrowser\Project; -use Symfony\Component\HttpFoundation\Response; /** * Defines an interface for services which can activate projects. @@ -48,11 +47,9 @@ interface ActivatorInterface { * @param \Drupal\project_browser\ProjectBrowser\Project $project * The project to activate. * - * @return \Symfony\Component\HttpFoundation\Response|null - * Optionally, a response that should be presented to the user in Project - * Browser. This could be a set of additional instructions to display in a - * modal, for example, or a redirect to a configuration form. + * @return \Drupal\Core\Ajax\CommandInterface[]|null + * Optionally, an array of AJAX commands to run on the front end. */ - public function activate(Project $project): ?Response; + public function activate(Project $project): ?array; } diff --git a/src/Activator/ModuleActivator.php b/src/Activator/ModuleActivator.php index 1b0d4727d..55ee2f4d3 100644 --- a/src/Activator/ModuleActivator.php +++ b/src/Activator/ModuleActivator.php @@ -13,7 +13,6 @@ use Drupal\Core\Link; use Drupal\Core\Url; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectType; -use Symfony\Component\HttpFoundation\JsonResponse; /** * An activator for Drupal modules. @@ -52,12 +51,9 @@ final class ModuleActivator implements InstructionsInterface, TasksInterface { /** * {@inheritdoc} */ - public function activate(Project $project): JsonResponse { + public function activate(Project $project): null { $this->moduleInstaller->install([$project->machineName]); - - return new JsonResponse([ - 'tasks' => $this->getTasks($project), - ]); + return NULL; } /** diff --git a/src/Activator/RecipeActivator.php b/src/Activator/RecipeActivator.php index 9f65d171f..c0fe5f580 100644 --- a/src/Activator/RecipeActivator.php +++ b/src/Activator/RecipeActivator.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\project_browser\Activator; use Composer\InstalledVersions; +use Drupal\Core\Ajax\RedirectCommand; use Drupal\Core\Extension\ModuleExtensionList; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\File\FileUrlGeneratorInterface; @@ -18,8 +19,6 @@ use Drupal\Core\Url; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectType; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Response; /** * Applies locally installed recipes. @@ -93,7 +92,7 @@ final class RecipeActivator implements InstructionsInterface, EventSubscriberInt /** * {@inheritdoc} */ - public function activate(Project $project): ?Response { + public function activate(Project $project): ?array { $path = $this->getPath($project); if (!$path) { return NULL; @@ -114,19 +113,13 @@ final class RecipeActivator implements InstructionsInterface, EventSubscriberInt 'recipe' => $path, ], ]); - - // The `redirect` key is not meaningful to JsonResponse; this is handled - // specially by the Svelte app. - // @see sveltejs/src/ProcessInstallListButton.svelte - return new JsonResponse([ - 'redirect' => $url->setAbsolute()->toString(), - ]); + return [ + new RedirectCommand($url->setAbsolute()->toString()), + ]; } RecipeRunner::processRecipe($recipe); - return new JsonResponse([ - 'tasks' => $this->getTasks($project), - ]); + return NULL; } /** -- GitLab From 76097e3e55b11d159f5e1e4b56e7064795d06f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 14:59:41 -0500 Subject: [PATCH 08/45] Make InstallerController deal with AJAX commands --- src/Controller/InstallerController.php | 31 +++++++++++++++++--------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 9bd0b9f5d..b84ac56fe 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -2,9 +2,14 @@ namespace Drupal\project_browser\Controller; +use Drupal\Component\Assertion\Inspector; use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\CommandInterface; +use Drupal\Core\Ajax\MessageCommand; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Url; use Drupal\package_manager\Exception\StageException; use Drupal\package_manager\StatusCheckTrait; @@ -401,27 +406,33 @@ final class InstallerController extends ControllerBase { * The request. * * @return \Symfony\Component\HttpFoundation\JsonResponse - * Returns normalized activated project data or an error message. + * Return an AJAX response, or an error message. */ public function activate(Request $request): JsonResponse { - $normalized_projects = []; + $response = new AjaxResponse(); foreach ($request->toArray() as $project_id) { $this->installState->setState($project_id, 'activating'); try { $project = $this->enabledSourceHandler->getStoredProject($project_id); - $plugin_id = strstr($project_id, '/', TRUE); - $this->activationManager->activate($project); + + $commands = $this->activationManager->activate($project); + if ($commands && !Inspector::assertAllObjects($commands, CommandInterface::class)) { + throw new \RuntimeException('Activators can only return \Drupal\Core\Ajax\CommandInterface objects, or NULL.'); + } + array_walk($commands, $response->addCommand(...)); $this->installState->setState($project_id, 'installed'); - $normalized_projects[] = $this->normalizer->normalize($project, 'json', ['source' => $plugin_id]); } catch (\Throwable $e) { - return $this->errorResponse($e, 'project install'); - } - finally { - $this->installState->deleteAll(); + $response->addCommand(new MessageCommand( + $e->getMessage(), + options: [ + 'type' => MessengerInterface::TYPE_ERROR, + ], + )); } } - return new JsonResponse($normalized_projects); + $this->installState->deleteAll(); + return $response; } /** -- GitLab From 24085764a424e561fcfddd80bbb9a78653fa644e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 15:02:29 -0500 Subject: [PATCH 09/45] Remove redirect handling from Svelte code --- sveltejs/public/build/bundle.js | Bin 274199 -> 273910 bytes sveltejs/public/build/bundle.js.map | Bin 253457 -> 252880 bytes sveltejs/src/InstallListProcessor.js | 10 ---------- 3 files changed, 10 deletions(-) diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index 1f08f0e940735a257cf0e693dc30fba071e420a7..1d86c1ef9b510c40196fb92b18c344b068c94780 100644 GIT binary patch delta 30 lcmbP!PvF~afem~5C-e1+H-DMX{$&Cq5HoH6GJ#on9svIL55@oh delta 262 zcmex%TVVP<fem~5C-=@{pKRXFSD&1pS6re{lv-Q>WTZOh=ar=9l_=OMcm@0W=@ldv z6{l(>mM3PGC}aZFCFbM=K~?INq*jz@Xlhz>aVbDSW}1Qqnn8LQiN*fqc>zWF1*t_P zl^W_rsVSL7smUeknwkpLK%Jp5&E=VSDf#7kIr+(nC7JnodKpEjX+US7n+MZx4Y#!x v%BocW>P*g1&`2#RnyfxaZt|pl3C7yVyuA|5m;2i<_cH=9)Aq~#%*yisk;q&$ diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index a55a93570d0b0daefc94f1720feb89420cbc00b6..3fff11ee5d25fc3b6b6ae9e5b3f8b85b2c2e5c56 100644 GIT binary patch delta 39 vcmbQZhyTKM{)R1#?vd?5QH<MzqL{XoY`43>WXH>7>DOL&muY(4UFOLEJg*Ou delta 486 zcmZ9Gu}i~16voLpIcOKV=yJF;!6V>YIy{on*4oA@9fDm#dNqM^373Pkh}f-jamYU+ z6hT~_ithbK9KE|(gbv4jkN5q)?|rW;tM|&{)oA(8BZjfybNr|T#$m!TfjP}mDX5b$ zL7Bh}e0Asb?KI#SHG@2e1&*aJm`-p1oVK7dVOmyw#LA#}g3Y}cI|?!{XE!`aDHn6I z#_8CPISob4vUUexoX2dO<n}ZP0}&_8jyRpj3--e#tXbKp4u4YL6P8REVUu!RtnQ7o z;@mf?OV>Bnu8-bC4ZvyFJKz9m0yIenprgjVR-^~eBNTuZ$Dio*@PAx<p_4XoBwd$} zH_He`m!M+wnuGvhsfGZ<QmHCMq^@o#E+0^}tKk+vOBa^L0l+{j<rXgK0(6zUCfca_ fi3i}7<1RKTJ~rrL{j)*N5t@0+#ugQg?ZL}0^4^)i diff --git a/sveltejs/src/InstallListProcessor.js b/sveltejs/src/InstallListProcessor.js index 2a89442db..201989f0b 100644 --- a/sveltejs/src/InstallListProcessor.js +++ b/sveltejs/src/InstallListProcessor.js @@ -109,16 +109,6 @@ export const activateProject = async (projectIds) => { await handleError(installResponse); return; } - - try { - const responseContent = JSON.parse(await installResponse.text()); - - if (responseContent.hasOwnProperty('redirect')) { - window.location.href = responseContent.redirect; - } - } catch (err) { - await handleError(installResponse); - } }; /** -- GitLab From d1d1efc344bc1b7f31f9da98eebff6267a27f1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 15:06:13 -0500 Subject: [PATCH 10/45] Log activation error --- src/Controller/InstallerController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index b84ac56fe..9d56abf01 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -423,12 +423,14 @@ final class InstallerController extends ControllerBase { $this->installState->setState($project_id, 'installed'); } catch (\Throwable $e) { + $message = $e->getMessage(); $response->addCommand(new MessageCommand( - $e->getMessage(), + $message, options: [ 'type' => MessengerInterface::TYPE_ERROR, ], )); + $this->logger->error($message); } } $this->installState->deleteAll(); -- GitLab From bfeb65ec9de909cbc3b7c6ad69172d8fc87348b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 15:07:43 -0500 Subject: [PATCH 11/45] Always attach AJAX to PB --- project_browser.libraries.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/project_browser.libraries.yml b/project_browser.libraries.yml index eb86d09ec..1716ee1fd 100644 --- a/project_browser.libraries.yml +++ b/project_browser.libraries.yml @@ -8,6 +8,7 @@ svelte: dependencies: - core/drupalSettings - core/drupal + - core/drupal.ajax - core/drupal.debounce - core/drupal.dialog - core/drupal.announce -- GitLab From 3abfc5e2c4116da976a8b9d257a0fa7451de7c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 15:24:58 -0500 Subject: [PATCH 12/45] Make it use AJAX --- src/Controller/InstallerController.php | 10 ++++++---- sveltejs/public/build/bundle.js | Bin 273910 -> 273818 bytes sveltejs/public/build/bundle.js.map | Bin 252880 -> 252696 bytes sveltejs/src/InstallListProcessor.js | 24 +++++++++++------------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 9d56abf01..95f753605 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -410,16 +410,18 @@ final class InstallerController extends ControllerBase { */ public function activate(Request $request): JsonResponse { $response = new AjaxResponse(); - foreach ($request->toArray() as $project_id) { + + foreach ($request->get('projects', []) as $project_id) { $this->installState->setState($project_id, 'activating'); + try { $project = $this->enabledSourceHandler->getStoredProject($project_id); $commands = $this->activationManager->activate($project); - if ($commands && !Inspector::assertAllObjects($commands, CommandInterface::class)) { - throw new \RuntimeException('Activators can only return \Drupal\Core\Ajax\CommandInterface objects, or NULL.'); + if ($commands) { + Inspector::assertAllObjects($commands, CommandInterface::class) or throw new \RuntimeException('Activators can only return \Drupal\Core\Ajax\CommandInterface objects, or NULL.'); + array_walk($commands, $response->addCommand(...)); } - array_walk($commands, $response->addCommand(...)); $this->installState->setState($project_id, 'installed'); } catch (\Throwable $e) { diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index 1d86c1ef9b510c40196fb92b18c344b068c94780..9bb526895ac6ddcd5b8d4f8211b57cefc2c70ccb 100644 GIT binary patch delta 207 zcmex%TVU30fem5J)4xS9OKlEl{>`P9ms+miQdC-yn4{;Il~|#{r2qwar8zk|Fy3^> zEzEK}aIQi%*JS@GmYIpkC7ET3C8-Gr83l#n(xlwX5-Xq@m{B0QpeR2pHMykN3dZtG rDMr>+i{QYSntG`fsmY}!sTz~Hd!?E$_qSi}X9QxV?U(zRmFEEf2Twee delta 168 zcmbPrTj1Mmfem5Jo6DNNa⁡=M|SIlosVE*iL4gqAi)2T#{LqSdyAx&Bdhv1`rjQ zldC4mvIEuWC{%MzJ}_B<J0mqQCAFy73dFabZYEHx1J$6Qkd&WNX*K!bbZJSLSgj`9 syv#HO4aLmKyC&*0W=?)TQGD_;DFMdX=J@{h_<lwpX4)R#&ul#p01>q}T>t<8 diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index 3fff11ee5d25fc3b6b6ae9e5b3f8b85b2c2e5c56..a7684502f91867a549ff73a79a93887f08476ac7 100644 GIT binary patch delta 446 zcmcbxoqxtQ{)R1#t9_?4mNCj~U+K%}&83i+TCU(yR9cXjqvx2FSfLS<rvL<br8zk| zVCHni?~HOH5Vk@!L{>qev?#}F`u}i7lVGTff<kd=Qf_966+(@ILP1e}R%&udu@#Kv znNkc_UkhbHXidG;iqz!Nl2i>%>vq>j#_g_=Oh+>f%3O7P9UXmjTpb;eS?-RG?hpxQ zM@MICAU1Te_H=aipFVLnlhpLQ6U-db*LO0pZ#O;66u_zL>FDT*P_6^wf>dWYfthfA z5STSxa3-^?pfgy3GhEJV`hx%_k?A=XnFJz=ftD6K>wv8W8e{1KF&pkskk%BPKu5<w z2my9{sH0;jNW#=f$KTP>AH)pM@pg3d2C;l}Kt7rtc#TPf6|8$Z<2fcPUPjY)o7+s& JZEiD90RR$Tfv*4n delta 507 zcmbQSjsL=S{)R1#t9`eh^kwwqnto4%Nn(0u6r<eq{s=}+KI@pgm^=lA<ovwi5{1m^ zFQXV`IZBIibQG#%@}>*KGD`Afq$Z}M78P58MO+gY^?7S`Kw1<OlJZk3t)|~kV3ZaC zi_~gDP0LJE&``{rt`^Ox!=DK>I58(DD7Cl%Xh-Vwb!m)Z?QbI(x4(^GI-JRr;ky07 zA*M=BPG3hyUmbTxNB8M_&oW8X7dtyT7CY+zNeE#LWaPM7dpbHh>bN>Oy6S+{r#d>O zf;B<7-j0sm5G^2s!0JFcjGc8n9UVO(YT%kdGC58V8GlDdf4K4_ph69xW0D;mlOc9x zJ2^ULJApJ*IO~8c0Wp2RTBirLFo{n0xx^%#1`;fAhBybs-98{0sI$Qi0n38j4rYO! z1R_A{jG;~f`!C$lF&yF{xa#TuJ(xsUK`JAr>s(=y-5zt6Nu8I)(ofrY`b05i$@T@e Ln5Hkd#asaZ6Udh9 diff --git a/sveltejs/src/InstallListProcessor.js b/sveltejs/src/InstallListProcessor.js index 201989f0b..222eb3060 100644 --- a/sveltejs/src/InstallListProcessor.js +++ b/sveltejs/src/InstallListProcessor.js @@ -2,6 +2,8 @@ import { get, writable } from 'svelte/store'; import { openPopup } from './popup'; import { BASE_URL, CURRENT_PATH } from './constants'; +const { Drupal } = window; + export const updated = writable(0); // Store for the install list. @@ -95,20 +97,16 @@ export const handleError = async (errorResponse) => { * A promise that resolves when the project is activated. */ export const activateProject = async (projectIds) => { - const url = `${BASE_URL}admin/modules/project_browser/activate`; - - const installResponse = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + new Drupal.Ajax( + null, + document.createElement('div'), + { + url: `${BASE_URL}admin/modules/project_browser/activate`, + submit: { + projects: projectIds, + }, }, - body: JSON.stringify(projectIds), - }); - - if (!installResponse.ok) { - await handleError(installResponse); - return; - } + ).execute(); }; /** -- GitLab From ea4532e1bbfdabf4fad0632966e58bc1d83922db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 15:38:24 -0500 Subject: [PATCH 13/45] Create a dedicated AJAX command to refresh the project --- src/Controller/InstallerController.php | 12 ++++++++--- src/RefreshProjectCommand.php | 28 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 src/RefreshProjectCommand.php diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 95f753605..02fb2cb50 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -2,7 +2,6 @@ namespace Drupal\project_browser\Controller; -use Drupal\Component\Assertion\Inspector; use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Ajax\AjaxResponse; @@ -18,6 +17,7 @@ use Drupal\project_browser\ComposerInstaller\Installer; use Drupal\project_browser\EnabledSourceHandler; use Drupal\project_browser\InstallState; use Drupal\project_browser\ProjectBrowser\Normalizer; +use Drupal\project_browser\RefreshProjectCommand; use Drupal\system\SystemManager; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -413,16 +413,22 @@ final class InstallerController extends ControllerBase { foreach ($request->get('projects', []) as $project_id) { $this->installState->setState($project_id, 'activating'); + [$source_id] = explode('/', $project_id, 2); try { $project = $this->enabledSourceHandler->getStoredProject($project_id); $commands = $this->activationManager->activate($project); if ($commands) { - Inspector::assertAllObjects($commands, CommandInterface::class) or throw new \RuntimeException('Activators can only return \Drupal\Core\Ajax\CommandInterface objects, or NULL.'); - array_walk($commands, $response->addCommand(...)); + foreach ($commands as $command) { + assert($command instanceof CommandInterface); + $response->addCommand($command); + } } $this->installState->setState($project_id, 'installed'); + + $project = $this->normalizer->normalize($project, context: ['source' => $source_id]); + $response->addCommand(new RefreshProjectCommand($project)); } catch (\Throwable $e) { $message = $e->getMessage(); diff --git a/src/RefreshProjectCommand.php b/src/RefreshProjectCommand.php new file mode 100644 index 000000000..c6fab42a0 --- /dev/null +++ b/src/RefreshProjectCommand.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\project_browser; + +use Drupal\Core\Ajax\CommandInterface; + +/** + * An AJAX command to refresh a particular project in the Svelte app. + */ +final class RefreshProjectCommand implements CommandInterface { + + public function __construct( + private readonly array $project, + ) {} + + /** + * {@inheritdoc} + */ + public function render(): array { + return [ + 'command' => 'refresh_project', + 'project' => $this->project, + ]; + } + +} -- GitLab From 9dbfc93e02de3a4e866d59a4dd5012d60329f496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 15:44:32 -0500 Subject: [PATCH 14/45] Oh, Stan --- src/Controller/InstallerController.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 02fb2cb50..0d753b6e4 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -5,7 +5,6 @@ namespace Drupal\project_browser\Controller; use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Ajax\AjaxResponse; -use Drupal\Core\Ajax\CommandInterface; use Drupal\Core\Ajax\MessageCommand; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Messenger\MessengerInterface; @@ -419,15 +418,13 @@ final class InstallerController extends ControllerBase { $project = $this->enabledSourceHandler->getStoredProject($project_id); $commands = $this->activationManager->activate($project); - if ($commands) { - foreach ($commands as $command) { - assert($command instanceof CommandInterface); - $response->addCommand($command); - } + foreach ($commands ?? [] as $command) { + $response->addCommand($command); } $this->installState->setState($project_id, 'installed'); $project = $this->normalizer->normalize($project, context: ['source' => $source_id]); + assert(is_array($project)); $response->addCommand(new RefreshProjectCommand($project)); } catch (\Throwable $e) { -- GitLab From 2980ac4d6d6e3b99405afb83ad388c3eacb0f6c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 15:55:52 -0500 Subject: [PATCH 15/45] Always return AjaxResponse --- src/Controller/InstallerController.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 0d753b6e4..0960f32bd 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -404,10 +404,10 @@ final class InstallerController extends ControllerBase { * @param \Symfony\Component\HttpFoundation\Request $request * The request. * - * @return \Symfony\Component\HttpFoundation\JsonResponse - * Return an AJAX response, or an error message. + * @return \Drupal\Core\Ajax\AjaxResponse + * A response that can be used by the client-side AJAX system. */ - public function activate(Request $request): JsonResponse { + public function activate(Request $request): AjaxResponse { $response = new AjaxResponse(); foreach ($request->get('projects', []) as $project_id) { -- GitLab From f7edc39362fe438bdee0b5929f0d84b72a3c01e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 16:21:50 -0500 Subject: [PATCH 16/45] Refactor InstallerControllerTest --- .../Functional/InstallerControllerTest.php | 65 ++++++++++--------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/tests/src/Functional/InstallerControllerTest.php b/tests/src/Functional/InstallerControllerTest.php index cfe51d95b..3baf8e402 100644 --- a/tests/src/Functional/InstallerControllerTest.php +++ b/tests/src/Functional/InstallerControllerTest.php @@ -23,7 +23,9 @@ use Drupal\project_browser\EnabledSourceHandler; use Drupal\project_browser\InstallState; use Drupal\project_browser_test\Datetime\TestTime; use GuzzleHttp\RequestOptions; +use PHP_CodeSniffer\Tokenizers\JS; use Psr\Http\Message\ResponseInterface; +use function Symfony\Component\String\s; // cspell:ignore crashmore @@ -180,9 +182,10 @@ class InstallerControllerTest extends BrowserTestBase { // via composer. $contents = $this->drupalGet('admin/modules/project_browser/install-begin'); $this->stageId = Json::decode($contents)['stage_id']; - $response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/core', [ - 'stage_id' => $this->stageId, - ]); + $response = $this->getPostResponse( + Url::fromRoute('project_browser.stage.require', ['stage_id' => $this->stageId]), + ['project_browser_test_mock/core'], + ); $this->assertSame(500, (int) $response->getStatusCode()); $this->assertSame('{"message":"StageEventException: The following package is already installed: drupal\/core\n","phase":"require"}', (string) $response->getBody()); } @@ -207,9 +210,10 @@ class InstallerControllerTest extends BrowserTestBase { * @covers ::require */ private function doRequire(): void { - $response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/awesome_module', [ - 'stage_id' => $this->stageId, - ]); + $response = $this->getPostResponse( + Url::fromRoute('project_browser.stage.require', ['stage_id' => $this->stageId]), + ['project_browser_test_mock/awesome_module'], + ); $expected_output = sprintf('{"phase":"require","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, (string) $response->getBody()); $this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'requiring'); @@ -312,9 +316,10 @@ class InstallerControllerTest extends BrowserTestBase { $result = ValidationResult::createError([$message]); $this->doStart(); TestSubscriber::setTestResult([$result], PreRequireEvent::class); - $response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/awesome_module', [ - 'stage_id' => $this->stageId, - ]); + $response = $this->getPostResponse( + Url::fromRoute('project_browser.stage.require', ['stage_id' => $this->stageId]), + ['project_browser_test_mock/awesome_module'], + ); $this->assertSame(500, (int) $response->getStatusCode()); $this->assertSame('{"message":"StageEventException: This is a PreRequire error.\n","phase":"require"}', (string) $response->getBody()); } @@ -328,9 +333,10 @@ class InstallerControllerTest extends BrowserTestBase { $error = new \Exception('PreRequire did not go well.'); TestSubscriber::setException($error, PreRequireEvent::class); $this->doStart(); - $response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/awesome_module', [ - 'stage_id' => $this->stageId, - ]); + $response = $this->getPostResponse( + Url::fromRoute('project_browser.stage.require', ['stage_id' => $this->stageId]), + ['project_browser_test_mock/awesome_module'], + ); $this->assertSame(500, (int) $response->getStatusCode()); $this->assertSame('{"message":"StageEventException: PreRequire did not go well.","phase":"require"}', (string) $response->getBody()); } @@ -344,9 +350,10 @@ class InstallerControllerTest extends BrowserTestBase { $error = new \Exception('PostRequire did not go well.'); TestSubscriber::setException($error, PostRequireEvent::class); $this->doStart(); - $response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/awesome_module', [ - 'stage_id' => $this->stageId, - ]); + $response = $this->getPostResponse( + Url::fromRoute('project_browser.stage.require', ['stage_id' => $this->stageId]), + ['project_browser_test_mock/awesome_module'], + ); $this->assertSame(500, (int) $response->getStatusCode()); $this->assertSame('{"message":"StageEventException: PostRequire did not go well.","phase":"require"}', (string) $response->getBody()); } @@ -546,7 +553,10 @@ class InstallerControllerTest extends BrowserTestBase { $assert_session->checkboxNotChecked('edit-modules-views-enable'); $assert_session->checkboxNotChecked('edit-modules-views-ui-enable'); - $response = $this->getPostResponse('project_browser.activate', 'drupal_core/views_ui'); + $response = $this->getPostResponse( + Url::fromRoute('project_browser.activate'), + ['projects' => ['drupal_core/views_ui']], + ); $this->assertSame(200, $response->getStatusCode()); $this->assertTrue(json_validate((string) $response->getBody())); @@ -574,28 +584,21 @@ class InstallerControllerTest extends BrowserTestBase { /** * Sends a POST request to the specified route with the provided project ID. * - * @param string $route_name - * The route to which the POST request is sent. - * @param string|string[] $project_id - * The project ID(s) to include in the POST request body. - * @param array $route_parameters - * (optional) An associative array of route parameters, such as 'stage_id', - * that will be included in the URL. + * @param \Drupal\Core\Url $url + * The URL to which the POST request is sent. + * @param array $payload + * The POST request body. Will be encoded to JSON. * * @return \Psr\Http\Message\ResponseInterface * The response. */ - private function getPostResponse(string $route_name, string|array $project_id, array $route_parameters = []): ResponseInterface { - $post_url = Url::fromRoute($route_name, $route_parameters); - - $request_options = [ + private function getPostResponse(Url $url, array $payload): ResponseInterface { + return $this->makeApiRequest('POST', $url, [ RequestOptions::HEADERS => [ 'Content-Type' => 'application/json', ], - ]; - $request_options[RequestOptions::BODY] = Json::encode((array) $project_id); - - return $this->makeApiRequest('POST', $post_url, $request_options); + RequestOptions::BODY => Json::encode($payload), + ]); } /** -- GitLab From 61a684949254506798ebd0f4255e3c7c7670c7e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 16:24:46 -0500 Subject: [PATCH 17/45] Fix InstallerControllerTest --- src/Controller/InstallerController.php | 6 +++++- sveltejs/public/build/bundle.js | Bin 273818 -> 273828 bytes sveltejs/public/build/bundle.js.map | Bin 252696 -> 252731 bytes sveltejs/src/InstallListProcessor.js | 2 +- .../Functional/InstallerControllerTest.php | 2 +- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 0960f32bd..c0db9bc55 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -410,7 +410,11 @@ final class InstallerController extends ControllerBase { public function activate(Request $request): AjaxResponse { $response = new AjaxResponse(); - foreach ($request->get('projects', []) as $project_id) { + $projects = $request->getPayload()->get('projects') ?? []; + if ($projects) { + $projects = explode(',', $projects); + } + foreach ($projects as $project_id) { $this->installState->setState($project_id, 'activating'); [$source_id] = explode('/', $project_id, 2); diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index 9bb526895ac6ddcd5b8d4f8211b57cefc2c70ccb..d59cef5586eb27dd4123b401cab5ee01644a81ac 100644 GIT binary patch delta 41 vcmbPrTVTm;feps}TzXmgnRy!OI_jE}6Q+nad-b<_^)mu7({`_Z=74zsP+$*u delta 30 kcmZ2-TVU30feps}lOIeLZw~2i59wzFVy5jO{mcRL0N_v!YybcN diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index a7684502f91867a549ff73a79a93887f08476ac7..faf0e8c683a2111385c42585c547284798c687d2 100644 GIT binary patch delta 64 zcmV-G0Kfm3whz0u4}i1*$XE(4YHw+7C?_l@DTl~d0k_Cl0!eh2#*qRQmrzFn1O_`v WK|^#ym+#X86^AU%0=F#91MCeC`xn3f delta 38 ucmdnJjeo{A{)R1#CnDM}MKEr^6v5<K!e;I1=xplLZgrMvyVY4{Mm_*MN)BoO diff --git a/sveltejs/src/InstallListProcessor.js b/sveltejs/src/InstallListProcessor.js index 222eb3060..5e02ac6f7 100644 --- a/sveltejs/src/InstallListProcessor.js +++ b/sveltejs/src/InstallListProcessor.js @@ -103,7 +103,7 @@ export const activateProject = async (projectIds) => { { url: `${BASE_URL}admin/modules/project_browser/activate`, submit: { - projects: projectIds, + projects: projectIds.join(','), }, }, ).execute(); diff --git a/tests/src/Functional/InstallerControllerTest.php b/tests/src/Functional/InstallerControllerTest.php index 3baf8e402..b8707411c 100644 --- a/tests/src/Functional/InstallerControllerTest.php +++ b/tests/src/Functional/InstallerControllerTest.php @@ -555,7 +555,7 @@ class InstallerControllerTest extends BrowserTestBase { $response = $this->getPostResponse( Url::fromRoute('project_browser.activate'), - ['projects' => ['drupal_core/views_ui']], + ['projects' => 'drupal_core/views_ui'], ); $this->assertSame(200, $response->getStatusCode()); $this->assertTrue(json_validate((string) $response->getBody())); -- GitLab From 00ab1b3d862d893d0fa66ab2c54371170f23b6ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 16:27:24 -0500 Subject: [PATCH 18/45] Remove pointless call to rebuildContainer() --- src/Controller/InstallerController.php | 2 ++ tests/src/Functional/InstallerControllerTest.php | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index c0db9bc55..47823f27e 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -412,8 +412,10 @@ final class InstallerController extends ControllerBase { $projects = $request->getPayload()->get('projects') ?? []; if ($projects) { + assert(is_string($projects)); $projects = explode(',', $projects); } + assert(is_array($projects)); foreach ($projects as $project_id) { $this->installState->setState($project_id, 'activating'); [$source_id] = explode('/', $project_id, 2); diff --git a/tests/src/Functional/InstallerControllerTest.php b/tests/src/Functional/InstallerControllerTest.php index b8707411c..4cd597c70 100644 --- a/tests/src/Functional/InstallerControllerTest.php +++ b/tests/src/Functional/InstallerControllerTest.php @@ -23,9 +23,7 @@ use Drupal\project_browser\EnabledSourceHandler; use Drupal\project_browser\InstallState; use Drupal\project_browser_test\Datetime\TestTime; use GuzzleHttp\RequestOptions; -use PHP_CodeSniffer\Tokenizers\JS; use Psr\Http\Message\ResponseInterface; -use function Symfony\Component\String\s; // cspell:ignore crashmore @@ -560,7 +558,6 @@ class InstallerControllerTest extends BrowserTestBase { $this->assertSame(200, $response->getStatusCode()); $this->assertTrue(json_validate((string) $response->getBody())); - $this->rebuildContainer(); $this->drupalGet('admin/modules'); $assert_session->checkboxChecked('edit-modules-views-enable'); $assert_session->checkboxChecked('edit-modules-views-ui-enable'); -- GitLab From 6e6b66f5fbe418429ba276d14e4bc3cbd00cdda5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 17:14:53 -0500 Subject: [PATCH 19/45] Adjust one test to account for the behavior change --- .../FunctionalJavascript/ProjectBrowserInstallerUiTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php index 9673f0e00..558fb3fe3 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php @@ -98,15 +98,15 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->waitForProject('Pinky and the Brain') ->pressButton('Install Pinky and the Brain'); - $popup = $assert_session->waitForElementVisible('css', '.project-browser-popup'); - $this->assertNotEmpty($popup); // The Pinky and the Brain module doesn't actually exist in the filesystem, // but the test activator pretends it does, in order to test the presence // of the "Install" button as opposed vs. the default "Add and Install" // button. This happens to be a good way to test mid-install exceptions as // well. // @see \Drupal\project_browser_test\TestActivator::getStatus() - $this->assertStringContainsString('MissingDependencyException: Unable to install modules pinky_brain due to missing modules pinky_brain', $popup->getText()); + $message = 'Unable to install modules pinky_brain due to missing modules pinky_brain'; + $this->assertTrue($assert_session->waitForText($message)); + $assert_session->statusMessageContains($message, 'error'); } /** -- GitLab From 93a567f3c3eec968b2b75d344eddc8158f537448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 17:49:51 -0500 Subject: [PATCH 20/45] Give a few seconds for AJAX to set up...I think --- .../src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php index 558fb3fe3..2fffe684a 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php @@ -132,6 +132,8 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { // If we reload, the installation status should be remembered. $this->getSession()->reload(); + // Give the AJAX system a few seconds to attach behaviors. + sleep(3); $this->inputSearchField('image', TRUE); $assert_session->waitForElementVisible('css', ".search__search-submit") ?->click(); -- GitLab From 35f75522334ea700f06df8e0be721c642094657a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 18:01:24 -0500 Subject: [PATCH 21/45] Revert "Give a few seconds for AJAX to set up...I think" This reverts commit 93a567f3c3eec968b2b75d344eddc8158f537448. --- .../src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php index 2fffe684a..558fb3fe3 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php @@ -132,8 +132,6 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { // If we reload, the installation status should be remembered. $this->getSession()->reload(); - // Give the AJAX system a few seconds to attach behaviors. - sleep(3); $this->inputSearchField('image', TRUE); $assert_session->waitForElementVisible('css', ".search__search-submit") ?->click(); -- GitLab From bab0bd11a8d2d4c2dbae03740d6e18b4f7b95865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 18:10:26 -0500 Subject: [PATCH 22/45] My kingdom for an await --- sveltejs/public/build/bundle.js | Bin 273828 -> 273834 bytes sveltejs/public/build/bundle.js.map | Bin 252731 -> 252742 bytes sveltejs/src/InstallListProcessor.js | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index d59cef5586eb27dd4123b401cab5ee01644a81ac..06911682ad912a62539969da86210c79735cd569 100644 GIT binary patch delta 32 mcmZ2-TVT~~feqPxtcm4`nI+9xeeGF&j6lq^J*$t|YCZt->kaS# delta 26 gcmZ2=TVTm;feqPx&4qpKg?)@b%(T6*kJ)lQ0Iop`@Bjb+ diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index faf0e8c683a2111385c42585c547284798c687d2..6507d085be0457013a20ebb51fd977351053032e 100644 GIT binary patch delta 67 zcmdnJjsMs-{)R1#LJ_Qq<%yXk?Sc`E+XW+-f=Xn49UXmjJRKc9b-*lVM@MHc*V@z3 QxvahV4Ab`NGt3A00P^M)@c;k- delta 38 ucmX@Mjeqwx{)R1#LJ{o}5scd<BA5b8rsthu5@*b6Z#%=Zz3mM10X_g9fexDh diff --git a/sveltejs/src/InstallListProcessor.js b/sveltejs/src/InstallListProcessor.js index 5e02ac6f7..67025be16 100644 --- a/sveltejs/src/InstallListProcessor.js +++ b/sveltejs/src/InstallListProcessor.js @@ -97,7 +97,7 @@ export const handleError = async (errorResponse) => { * A promise that resolves when the project is activated. */ export const activateProject = async (projectIds) => { - new Drupal.Ajax( + await new Drupal.Ajax( null, document.createElement('div'), { -- GitLab From 3b000e2517756aa33cc8baf5e221766a02edcd36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Fri, 21 Feb 2025 14:56:21 -0500 Subject: [PATCH 23/45] Use logException --- src/Controller/InstallerController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 5043d692f..d2e2c90cc 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -9,6 +9,7 @@ use Drupal\Core\Ajax\MessageCommand; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Url; +use Drupal\Core\Utility\Error; use Drupal\package_manager\Exception\StageException; use Drupal\package_manager\StatusCheckTrait; use Drupal\project_browser\ActivationManager; @@ -441,7 +442,7 @@ final class InstallerController extends ControllerBase { 'type' => MessengerInterface::TYPE_ERROR, ], )); - $this->logger->error($message); + Error::logException($this->logger, $e); } } $this->installState->deleteAll(); -- GitLab From 8176d4ca96683a0125eea3dd06936bd1d5253e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 18 Feb 2025 18:23:17 -0500 Subject: [PATCH 24/45] Sketch in the normalizer --- project_browser.services.yml | 1 + .../ProjectBrowserEndpointController.php | 10 ++-- src/ProjectBrowser/Normalizer.php | 54 +++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 src/ProjectBrowser/Normalizer.php diff --git a/project_browser.services.yml b/project_browser.services.yml index 4e2e42215..05c3bcd98 100644 --- a/project_browser.services.yml +++ b/project_browser.services.yml @@ -41,3 +41,4 @@ services: public: false tags: - { name: paramconverter } + Drupal\project_browser\ProjectBrowser\Normalizer: ~ diff --git a/src/Controller/ProjectBrowserEndpointController.php b/src/Controller/ProjectBrowserEndpointController.php index b75d3c938..4b91e3db3 100644 --- a/src/Controller/ProjectBrowserEndpointController.php +++ b/src/Controller/ProjectBrowserEndpointController.php @@ -4,8 +4,8 @@ namespace Drupal\project_browser\Controller; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Form\FormState; -use Drupal\project_browser\ActivationManager; use Drupal\project_browser\EnabledSourceHandler; +use Drupal\project_browser\ProjectBrowser\Normalizer; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; use Drupal\system\Form\ModulesUninstallForm; @@ -14,6 +14,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; /** * Controller for the proxy layer. @@ -22,7 +23,7 @@ final class ProjectBrowserEndpointController extends ControllerBase { public function __construct( private readonly EnabledSourceHandler $enabledSource, - private readonly ActivationManager $activationManager, + private readonly NormalizerInterface $normalizer, ) {} /** @@ -31,7 +32,7 @@ final class ProjectBrowserEndpointController extends ControllerBase { public static function create(ContainerInterface $container): static { return new static( $container->get(EnabledSourceHandler::class), - $container->get(ActivationManager::class), + $container->get(Normalizer::class), ); } @@ -54,7 +55,7 @@ final class ProjectBrowserEndpointController extends ControllerBase { } $results = $this->enabledSource->getProjects($query['source'], $query); - return new JsonResponse($this->prepareResults($results)); + return new JsonResponse($this->normalizer->normalize($results)); } /** @@ -68,6 +69,7 @@ final class ProjectBrowserEndpointController extends ControllerBase { * all projects. */ private function prepareResults(ProjectsResultsPage $results): array { + // @todo Move all of this to the normalizer. $data = $results->toArray(); // Add activation info to all the projects in the result set, and fully diff --git a/src/ProjectBrowser/Normalizer.php b/src/ProjectBrowser/Normalizer.php new file mode 100644 index 000000000..f84c3a2b4 --- /dev/null +++ b/src/ProjectBrowser/Normalizer.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\project_browser\ProjectBrowser; + +use Drupal\project_browser\ActivationManager; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +final class Normalizer implements NormalizerInterface { + + public function __construct( + private readonly ActivationManager $activationManager, + ) {} + + /** + * {@inheritdoc} + */ + public function normalize(mixed $data, ?string $format = NULL, array $context = []): array { + if ($data instanceof Project) { + assert(array_key_exists('source', $context)); + $data = $this->activationManager->getActivationInfo($data) + $data->toArray(); + $data['id'] = $context['source'] . '/' . $data['id']; + } + elseif ($data instanceof ProjectsResultsPage) { + $context['source'] = $data->pluginId; + + $data = $data->toArray(); + $data['list'] = array_map( + fn (Project $project): array => $this->normalize($project, $format, $context), + $data['list'], + ); + } + return $data; + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization(mixed $data, ?string $format = NULL, array $context = []): bool { + return $data instanceof Project || $data instanceof ProjectsResultsPage; + } + + /** + * {@inheritdoc} + */ + public function getSupportedTypes(?string $format): array { + return [ + Project::class => TRUE, + ProjectsResultsPage::class => TRUE, + ]; + } + +} -- GitLab From 85beae0c922f50d023f2bfd4cb274a7173cb4139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Tue, 18 Feb 2025 18:56:15 -0500 Subject: [PATCH 25/45] Remove prepareResults() --- .../ProjectBrowserEndpointController.php | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/src/Controller/ProjectBrowserEndpointController.php b/src/Controller/ProjectBrowserEndpointController.php index 4b91e3db3..05c23e0b6 100644 --- a/src/Controller/ProjectBrowserEndpointController.php +++ b/src/Controller/ProjectBrowserEndpointController.php @@ -6,8 +6,6 @@ use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Form\FormState; use Drupal\project_browser\EnabledSourceHandler; use Drupal\project_browser\ProjectBrowser\Normalizer; -use Drupal\project_browser\ProjectBrowser\Project; -use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage; use Drupal\system\Form\ModulesUninstallForm; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; @@ -58,33 +56,6 @@ final class ProjectBrowserEndpointController extends ControllerBase { return new JsonResponse($this->normalizer->normalize($results)); } - /** - * Prepares a set of results to be delivered to the front end. - * - * @param \Drupal\project_browser\ProjectBrowser\ProjectsResultsPage $results - * A page of query results. - * - * @return array - * The query results, with activation info and fully qualified IDs added to - * all projects. - */ - private function prepareResults(ProjectsResultsPage $results): array { - // @todo Move all of this to the normalizer. - $data = $results->toArray(); - - // Add activation info to all the projects in the result set, and fully - // qualify the project IDs by prefixing them with the source plugin ID. - $mapper = function (Project $project) use ($results): array { - $data = $this->activationManager->getActivationInfo($project) + $project->toArray(); - // Always send a fully qualified project ID to the front end. - $data['id'] = $results->pluginId . '/' . $project->id; - return $data; - }; - $data['list'] = array_map($mapper, $data['list']); - - return $data; - } - /** * Prepares to uninstall a module. * -- GitLab From a8f52f4e2ec4577ed3e90263c9354b483bcb156c Mon Sep 17 00:00:00 2001 From: narendra-drupal <87118318+narendra-drupal@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:02:46 +0530 Subject: [PATCH 26/45] getActivationInfo moved to Normalizer --- src/ActivationManager.php | 65 +------------------------------ src/ProjectBrowser/Normalizer.php | 65 ++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/src/ActivationManager.php b/src/ActivationManager.php index 91e78e68d..53adf27d0 100644 --- a/src/ActivationManager.php +++ b/src/ActivationManager.php @@ -4,13 +4,8 @@ declare(strict_types=1); namespace Drupal\project_browser; -use Drupal\Component\Utility\Xss; -use Drupal\Core\Link; -use Drupal\Core\Render\RendererInterface; use Drupal\project_browser\Activator\ActivationStatus; use Drupal\project_browser\Activator\ActivatorInterface; -use Drupal\project_browser\Activator\InstructionsInterface; -use Drupal\project_browser\Activator\TasksInterface; use Drupal\project_browser\ProjectBrowser\Project; use Symfony\Component\HttpFoundation\Response; @@ -29,10 +24,6 @@ final class ActivationManager { */ private array $activators = []; - public function __construct( - private readonly RendererInterface $renderer, - ) {} - /** * Registers an activator. * @@ -71,7 +62,7 @@ final class ActivationManager { * @throws \InvalidArgumentException * Thrown if none of the registered activators can handle the given project. */ - private function getActivatorForProject(Project $project): ActivatorInterface { + public function getActivatorForProject(Project $project): ActivatorInterface { foreach ($this->activators as $activator) { if ($activator->supports($project)) { return $activator; @@ -80,60 +71,6 @@ final class ActivationManager { throw new \InvalidArgumentException("The project '$project->machineName' is not supported by any registered activators."); } - /** - * Gets activation information for a project, for delivery to the front-end. - * - * @param \Drupal\project_browser\ProjectBrowser\Project $project - * A project object. - * - * @return array - * An array of activation information. Will consist of: - * - `status`: The activation status of the project on the current site. - * Will be the lowercase name of the one of the cases of - * \Drupal\project_browser\Activator\ActivationStatus. - * - `commands`: The instructions a human can take to activate the project - * manually, or a URL where they can do so. Will be NULL if the registered - * activator which supports the given project is not capable of generating - * instructions. - * - `tasks`: An array of \Drupal\Core\Link objects for specific follow-up - * tasks that a user can take after activating this project. For example, - * could include a link to a module's configuration form, or a dashboard - * provided by a recipe. - * - * @see \Drupal\project_browser\ProjectBrowser\Project::toArray() - */ - public function getActivationInfo(Project $project): array { - $activator = $this->getActivatorForProject($project); - $data = [ - 'status' => strtolower($activator->getStatus($project)->name), - 'commands' => NULL, - 'tasks' => [], - ]; - - if ($activator instanceof InstructionsInterface) { - $data['commands'] = Xss::filter( - $activator->getInstructions($project), - [...Xss::getAdminTagList(), 'textarea', 'button'], - ); - } - - if ($activator instanceof TasksInterface) { - $map = function (Link $link): array { - $text = $link->getText(); - if (is_array($text)) { - $text = $this->renderer->renderInIsolation($text); - } - return [ - 'text' => (string) $text, - 'url' => $link->getUrl()->setAbsolute()->toString(), - ]; - }; - $data['tasks'] = array_values(array_map($map, $activator->getTasks($project))); - } - - return $data; - } - /** * Activates a project on the current site. * diff --git a/src/ProjectBrowser/Normalizer.php b/src/ProjectBrowser/Normalizer.php index f84c3a2b4..eafa7431c 100644 --- a/src/ProjectBrowser/Normalizer.php +++ b/src/ProjectBrowser/Normalizer.php @@ -4,13 +4,22 @@ declare(strict_types=1); namespace Drupal\project_browser\ProjectBrowser; +use Drupal\Component\Utility\Xss; +use Drupal\Core\Link; +use Drupal\Core\Render\RendererInterface; use Drupal\project_browser\ActivationManager; +use Drupal\project_browser\Activator\InstructionsInterface; +use Drupal\project_browser\Activator\TasksInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +/** + * Normalizes Project and ProjectsResultsPage objects into an array format. + */ final class Normalizer implements NormalizerInterface { public function __construct( private readonly ActivationManager $activationManager, + private readonly RendererInterface $renderer, ) {} /** @@ -19,7 +28,7 @@ final class Normalizer implements NormalizerInterface { public function normalize(mixed $data, ?string $format = NULL, array $context = []): array { if ($data instanceof Project) { assert(array_key_exists('source', $context)); - $data = $this->activationManager->getActivationInfo($data) + $data->toArray(); + $data = $this->getActivationInfo($data) + $data->toArray(); $data['id'] = $context['source'] . '/' . $data['id']; } elseif ($data instanceof ProjectsResultsPage) { @@ -51,4 +60,58 @@ final class Normalizer implements NormalizerInterface { ]; } + /** + * Gets activation information for a project, for delivery to the front-end. + * + * @param \Drupal\project_browser\ProjectBrowser\Project $project + * A project object. + * + * @return array + * An array of activation information. Will consist of: + * - `status`: The activation status of the project on the current site. + * Will be the lowercase name of the one of the cases of + * \Drupal\project_browser\Activator\ActivationStatus. + * - `commands`: The instructions a human can take to activate the project + * manually, or a URL where they can do so. Will be NULL if the registered + * activator which supports the given project is not capable of generating + * instructions. + * - `tasks`: An array of \Drupal\Core\Link objects for specific follow-up + * tasks that a user can take after activating this project. For example, + * could include a link to a module's configuration form, or a dashboard + * provided by a recipe. + * + * @see \Drupal\project_browser\ProjectBrowser\Project::toArray() + */ + private function getActivationInfo(Project $project): array { + $activator = $this->activationManager->getActivatorForProject($project); + $data = [ + 'status' => strtolower($activator->getStatus($project)->name), + 'commands' => NULL, + 'tasks' => [], + ]; + + if ($activator instanceof InstructionsInterface) { + $data['commands'] = Xss::filter( + $activator->getInstructions($project), + [...Xss::getAdminTagList(), 'textarea', 'button'], + ); + } + + if ($activator instanceof TasksInterface) { + $map = function (Link $link): array { + $text = $link->getText(); + if (is_array($text)) { + $text = $this->renderer->renderInIsolation($text); + } + return [ + 'text' => (string) $text, + 'url' => $link->getUrl()->setAbsolute()->toString(), + ]; + }; + $data['tasks'] = array_values(array_map($map, $activator->getTasks($project))); + } + + return $data; + } + } -- GitLab From 3b548e4c6b8cea42de87cfb0b7545fe8d4fbcd2f Mon Sep 17 00:00:00 2001 From: narendra-drupal <87118318+narendra-drupal@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:59:44 +0530 Subject: [PATCH 27/45] return normalized projects in activate response --- src/Controller/InstallerController.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index d7ba08c98..a723a24fd 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -12,13 +12,14 @@ use Drupal\project_browser\ActivationManager; use Drupal\project_browser\ComposerInstaller\Installer; use Drupal\project_browser\EnabledSourceHandler; use Drupal\project_browser\InstallState; +use Drupal\project_browser\ProjectBrowser\Normalizer; use Drupal\system\SystemManager; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -43,6 +44,7 @@ final class InstallerController extends ControllerBase { private readonly ActivationManager $activationManager, private readonly InstallState $installState, private readonly EventDispatcherInterface $eventDispatcher, + private readonly NormalizerInterface $normalizer, ) {} /** @@ -61,6 +63,7 @@ final class InstallerController extends ControllerBase { $container->get(ActivationManager::class), $container->get(InstallState::class), $container->get(EventDispatcherInterface::class), + $container->get(Normalizer::class), ); } @@ -392,21 +395,23 @@ final class InstallerController extends ControllerBase { } /** - * Installs an already downloaded module. + * Installs an already downloaded project. * * @param \Symfony\Component\HttpFoundation\Request $request * The request. * - * @return \Symfony\Component\HttpFoundation\Response - * Status message. + * @return \Symfony\Component\HttpFoundation\JsonResponse + * Returns normalized activated project data or an error message. */ - public function activate(Request $request): Response { + public function activate(Request $request): JsonResponse { + $normalized_projects = []; foreach ($request->toArray() as $project_id) { $this->installState->setState($project_id, 'activating'); try { $project = $this->enabledSourceHandler->getStoredProject($project_id); - $response = $this->activationManager->activate($project); + $this->activationManager->activate($project); $this->installState->setState($project_id, 'installed'); + $normalized_projects[] = $this->normalizer->normalize($project); } catch (\Throwable $e) { return $this->errorResponse($e, 'project install'); @@ -415,7 +420,7 @@ final class InstallerController extends ControllerBase { $this->installState->deleteAll(); } } - return $response ?? new JsonResponse(['status' => 0]); + return new JsonResponse($normalized_projects); } /** -- GitLab From d02c9e9bdd69b83dd0dac417b8b83ab2e3a6d1a4 Mon Sep 17 00:00:00 2001 From: narendra-drupal <87118318+narendra-drupal@users.noreply.github.com> Date: Wed, 19 Feb 2025 17:47:45 +0530 Subject: [PATCH 28/45] Added source --- src/Controller/InstallerController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index a723a24fd..e06ffc0a2 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -409,9 +409,10 @@ final class InstallerController extends ControllerBase { $this->installState->setState($project_id, 'activating'); try { $project = $this->enabledSourceHandler->getStoredProject($project_id); + $plugin_id = strstr($project_id, '/', TRUE); $this->activationManager->activate($project); $this->installState->setState($project_id, 'installed'); - $normalized_projects[] = $this->normalizer->normalize($project); + $normalized_projects[] = $this->normalizer->normalize($project, 'json', ['source' => $plugin_id]); } catch (\Throwable $e) { return $this->errorResponse($e, 'project install'); -- GitLab From e1af8d1a02b75eef1067f1b54d3a12ff85882ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 14:52:36 -0500 Subject: [PATCH 29/45] Make all activators return an array of AJAX commands --- src/ActivationManager.php | 9 ++++----- src/Activator/ActivatorInterface.php | 9 +++------ src/Activator/ModuleActivator.php | 8 ++------ src/Activator/RecipeActivator.php | 19 ++++++------------- 4 files changed, 15 insertions(+), 30 deletions(-) diff --git a/src/ActivationManager.php b/src/ActivationManager.php index 53adf27d0..b2a89d789 100644 --- a/src/ActivationManager.php +++ b/src/ActivationManager.php @@ -7,7 +7,6 @@ namespace Drupal\project_browser; use Drupal\project_browser\Activator\ActivationStatus; use Drupal\project_browser\Activator\ActivatorInterface; use Drupal\project_browser\ProjectBrowser\Project; -use Symfony\Component\HttpFoundation\Response; /** * A generalized activator that can handle any type of project. @@ -77,11 +76,11 @@ final class ActivationManager { * @param \Drupal\project_browser\ProjectBrowser\Project $project * The project to activate. * - * @return \Symfony\Component\HttpFoundation\Response|null - * The response, or lack thereof, returned by the first registered activator - * that supports the given project. + * @return \Drupal\Core\Ajax\CommandInterface[]|null + * The AJAX commands, or lack thereof, returned by the first registered + * activator that supports the given project. */ - public function activate(Project $project): ?Response { + public function activate(Project $project): ?array { return $this->getActivatorForProject($project)->activate($project); } diff --git a/src/Activator/ActivatorInterface.php b/src/Activator/ActivatorInterface.php index 5fc5a8639..ea28361b7 100644 --- a/src/Activator/ActivatorInterface.php +++ b/src/Activator/ActivatorInterface.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Drupal\project_browser\Activator; use Drupal\project_browser\ProjectBrowser\Project; -use Symfony\Component\HttpFoundation\Response; /** * Defines an interface for services which can activate projects. @@ -48,11 +47,9 @@ interface ActivatorInterface { * @param \Drupal\project_browser\ProjectBrowser\Project $project * The project to activate. * - * @return \Symfony\Component\HttpFoundation\Response|null - * Optionally, a response that should be presented to the user in Project - * Browser. This could be a set of additional instructions to display in a - * modal, for example, or a redirect to a configuration form. + * @return \Drupal\Core\Ajax\CommandInterface[]|null + * Optionally, an array of AJAX commands to run on the front end. */ - public function activate(Project $project): ?Response; + public function activate(Project $project): ?array; } diff --git a/src/Activator/ModuleActivator.php b/src/Activator/ModuleActivator.php index 9389d7173..6edee6617 100644 --- a/src/Activator/ModuleActivator.php +++ b/src/Activator/ModuleActivator.php @@ -14,7 +14,6 @@ use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectType; -use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RequestStack; /** @@ -56,12 +55,9 @@ final class ModuleActivator implements InstructionsInterface, TasksInterface { /** * {@inheritdoc} */ - public function activate(Project $project): JsonResponse { + public function activate(Project $project): null { $this->moduleInstaller->install([$project->machineName]); - - return new JsonResponse([ - 'tasks' => $this->getTasks($project), - ]); + return NULL; } /** diff --git a/src/Activator/RecipeActivator.php b/src/Activator/RecipeActivator.php index 5af9c6e77..2cff5fc00 100644 --- a/src/Activator/RecipeActivator.php +++ b/src/Activator/RecipeActivator.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\project_browser\Activator; use Composer\InstalledVersions; +use Drupal\Core\Ajax\RedirectCommand; use Drupal\Core\Extension\ModuleExtensionList; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\File\FileUrlGeneratorInterface; @@ -18,8 +19,6 @@ use Drupal\Core\Url; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectType; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Response; /** * Applies locally installed recipes. @@ -93,7 +92,7 @@ final class RecipeActivator implements InstructionsInterface, EventSubscriberInt /** * {@inheritdoc} */ - public function activate(Project $project): ?Response { + public function activate(Project $project): ?array { $path = $this->getPath($project); if (!$path) { return NULL; @@ -114,19 +113,13 @@ final class RecipeActivator implements InstructionsInterface, EventSubscriberInt 'recipe' => $path, ], ]); - - // The `redirect` key is not meaningful to JsonResponse; this is handled - // specially by the Svelte app. - // @see sveltejs/src/ProcessInstallListButton.svelte - return new JsonResponse([ - 'redirect' => $url->setAbsolute()->toString(), - ]); + return [ + new RedirectCommand($url->setAbsolute()->toString()), + ]; } RecipeRunner::processRecipe($recipe); - return new JsonResponse([ - 'tasks' => $this->getTasks($project), - ]); + return NULL; } /** -- GitLab From 93bbda43d22b492379d48446b987a790f97e0cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 14:59:41 -0500 Subject: [PATCH 30/45] Make InstallerController deal with AJAX commands --- src/Controller/InstallerController.php | 31 +++++++++++++++++--------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index e06ffc0a2..c524d1130 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -2,9 +2,14 @@ namespace Drupal\project_browser\Controller; +use Drupal\Component\Assertion\Inspector; use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\CommandInterface; +use Drupal\Core\Ajax\MessageCommand; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Url; use Drupal\package_manager\Exception\StageException; use Drupal\package_manager\StatusCheckTrait; @@ -401,27 +406,33 @@ final class InstallerController extends ControllerBase { * The request. * * @return \Symfony\Component\HttpFoundation\JsonResponse - * Returns normalized activated project data or an error message. + * Return an AJAX response, or an error message. */ public function activate(Request $request): JsonResponse { - $normalized_projects = []; + $response = new AjaxResponse(); foreach ($request->toArray() as $project_id) { $this->installState->setState($project_id, 'activating'); try { $project = $this->enabledSourceHandler->getStoredProject($project_id); - $plugin_id = strstr($project_id, '/', TRUE); - $this->activationManager->activate($project); + + $commands = $this->activationManager->activate($project); + if ($commands && !Inspector::assertAllObjects($commands, CommandInterface::class)) { + throw new \RuntimeException('Activators can only return \Drupal\Core\Ajax\CommandInterface objects, or NULL.'); + } + array_walk($commands, $response->addCommand(...)); $this->installState->setState($project_id, 'installed'); - $normalized_projects[] = $this->normalizer->normalize($project, 'json', ['source' => $plugin_id]); } catch (\Throwable $e) { - return $this->errorResponse($e, 'project install'); - } - finally { - $this->installState->deleteAll(); + $response->addCommand(new MessageCommand( + $e->getMessage(), + options: [ + 'type' => MessengerInterface::TYPE_ERROR, + ], + )); } } - return new JsonResponse($normalized_projects); + $this->installState->deleteAll(); + return $response; } /** -- GitLab From 2b8b99c0ca3049c7233f36aef23c8b00f5573863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 15:02:29 -0500 Subject: [PATCH 31/45] Remove redirect handling from Svelte code --- sveltejs/public/build/bundle.js | Bin 274199 -> 273910 bytes sveltejs/public/build/bundle.js.map | Bin 253457 -> 252880 bytes sveltejs/src/InstallListProcessor.js | 10 ---------- 3 files changed, 10 deletions(-) diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index 1f08f0e940735a257cf0e693dc30fba071e420a7..1d86c1ef9b510c40196fb92b18c344b068c94780 100644 GIT binary patch delta 30 lcmbP!PvF~afem~5C-e1+H-DMX{$&Cq5HoH6GJ#on9svIL55@oh delta 262 zcmex%TVVP<fem~5C-=@{pKRXFSD&1pS6re{lv-Q>WTZOh=ar=9l_=OMcm@0W=@ldv z6{l(>mM3PGC}aZFCFbM=K~?INq*jz@Xlhz>aVbDSW}1Qqnn8LQiN*fqc>zWF1*t_P zl^W_rsVSL7smUeknwkpLK%Jp5&E=VSDf#7kIr+(nC7JnodKpEjX+US7n+MZx4Y#!x v%BocW>P*g1&`2#RnyfxaZt|pl3C7yVyuA|5m;2i<_cH=9)Aq~#%*yisk;q&$ diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index a55a93570d0b0daefc94f1720feb89420cbc00b6..3fff11ee5d25fc3b6b6ae9e5b3f8b85b2c2e5c56 100644 GIT binary patch delta 39 vcmbQZhyTKM{)R1#?vd?5QH<MzqL{XoY`43>WXH>7>DOL&muY(4UFOLEJg*Ou delta 486 zcmZ9Gu}i~16voLpIcOKV=yJF;!6V>YIy{on*4oA@9fDm#dNqM^373Pkh}f-jamYU+ z6hT~_ithbK9KE|(gbv4jkN5q)?|rW;tM|&{)oA(8BZjfybNr|T#$m!TfjP}mDX5b$ zL7Bh}e0Asb?KI#SHG@2e1&*aJm`-p1oVK7dVOmyw#LA#}g3Y}cI|?!{XE!`aDHn6I z#_8CPISob4vUUexoX2dO<n}ZP0}&_8jyRpj3--e#tXbKp4u4YL6P8REVUu!RtnQ7o z;@mf?OV>Bnu8-bC4ZvyFJKz9m0yIenprgjVR-^~eBNTuZ$Dio*@PAx<p_4XoBwd$} zH_He`m!M+wnuGvhsfGZ<QmHCMq^@o#E+0^}tKk+vOBa^L0l+{j<rXgK0(6zUCfca_ fi3i}7<1RKTJ~rrL{j)*N5t@0+#ugQg?ZL}0^4^)i diff --git a/sveltejs/src/InstallListProcessor.js b/sveltejs/src/InstallListProcessor.js index 2a89442db..201989f0b 100644 --- a/sveltejs/src/InstallListProcessor.js +++ b/sveltejs/src/InstallListProcessor.js @@ -109,16 +109,6 @@ export const activateProject = async (projectIds) => { await handleError(installResponse); return; } - - try { - const responseContent = JSON.parse(await installResponse.text()); - - if (responseContent.hasOwnProperty('redirect')) { - window.location.href = responseContent.redirect; - } - } catch (err) { - await handleError(installResponse); - } }; /** -- GitLab From 56ad83f8503db2ffa646c6d1323218c04714968b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 15:06:13 -0500 Subject: [PATCH 32/45] Log activation error --- src/Controller/InstallerController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index c524d1130..5489a06c1 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -423,12 +423,14 @@ final class InstallerController extends ControllerBase { $this->installState->setState($project_id, 'installed'); } catch (\Throwable $e) { + $message = $e->getMessage(); $response->addCommand(new MessageCommand( - $e->getMessage(), + $message, options: [ 'type' => MessengerInterface::TYPE_ERROR, ], )); + $this->logger->error($message); } } $this->installState->deleteAll(); -- GitLab From bd2fc64b7dcfe92885457aee9f3e776a743ff59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 15:07:43 -0500 Subject: [PATCH 33/45] Always attach AJAX to PB --- project_browser.libraries.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/project_browser.libraries.yml b/project_browser.libraries.yml index eb86d09ec..1716ee1fd 100644 --- a/project_browser.libraries.yml +++ b/project_browser.libraries.yml @@ -8,6 +8,7 @@ svelte: dependencies: - core/drupalSettings - core/drupal + - core/drupal.ajax - core/drupal.debounce - core/drupal.dialog - core/drupal.announce -- GitLab From d8c8d2ac9bf8c4032dd09cacc3ed69e93d02928e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 15:24:58 -0500 Subject: [PATCH 34/45] Make it use AJAX --- src/Controller/InstallerController.php | 10 ++++++---- sveltejs/public/build/bundle.js | Bin 273910 -> 273818 bytes sveltejs/public/build/bundle.js.map | Bin 252880 -> 252696 bytes sveltejs/src/InstallListProcessor.js | 24 +++++++++++------------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 5489a06c1..89446059c 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -410,16 +410,18 @@ final class InstallerController extends ControllerBase { */ public function activate(Request $request): JsonResponse { $response = new AjaxResponse(); - foreach ($request->toArray() as $project_id) { + + foreach ($request->get('projects', []) as $project_id) { $this->installState->setState($project_id, 'activating'); + try { $project = $this->enabledSourceHandler->getStoredProject($project_id); $commands = $this->activationManager->activate($project); - if ($commands && !Inspector::assertAllObjects($commands, CommandInterface::class)) { - throw new \RuntimeException('Activators can only return \Drupal\Core\Ajax\CommandInterface objects, or NULL.'); + if ($commands) { + Inspector::assertAllObjects($commands, CommandInterface::class) or throw new \RuntimeException('Activators can only return \Drupal\Core\Ajax\CommandInterface objects, or NULL.'); + array_walk($commands, $response->addCommand(...)); } - array_walk($commands, $response->addCommand(...)); $this->installState->setState($project_id, 'installed'); } catch (\Throwable $e) { diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index 1d86c1ef9b510c40196fb92b18c344b068c94780..9bb526895ac6ddcd5b8d4f8211b57cefc2c70ccb 100644 GIT binary patch delta 207 zcmex%TVU30fem5J)4xS9OKlEl{>`P9ms+miQdC-yn4{;Il~|#{r2qwar8zk|Fy3^> zEzEK}aIQi%*JS@GmYIpkC7ET3C8-Gr83l#n(xlwX5-Xq@m{B0QpeR2pHMykN3dZtG rDMr>+i{QYSntG`fsmY}!sTz~Hd!?E$_qSi}X9QxV?U(zRmFEEf2Twee delta 168 zcmbPrTj1Mmfem5Jo6DNNa⁡=M|SIlosVE*iL4gqAi)2T#{LqSdyAx&Bdhv1`rjQ zldC4mvIEuWC{%MzJ}_B<J0mqQCAFy73dFabZYEHx1J$6Qkd&WNX*K!bbZJSLSgj`9 syv#HO4aLmKyC&*0W=?)TQGD_;DFMdX=J@{h_<lwpX4)R#&ul#p01>q}T>t<8 diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index 3fff11ee5d25fc3b6b6ae9e5b3f8b85b2c2e5c56..a7684502f91867a549ff73a79a93887f08476ac7 100644 GIT binary patch delta 446 zcmcbxoqxtQ{)R1#t9_?4mNCj~U+K%}&83i+TCU(yR9cXjqvx2FSfLS<rvL<br8zk| zVCHni?~HOH5Vk@!L{>qev?#}F`u}i7lVGTff<kd=Qf_966+(@ILP1e}R%&udu@#Kv znNkc_UkhbHXidG;iqz!Nl2i>%>vq>j#_g_=Oh+>f%3O7P9UXmjTpb;eS?-RG?hpxQ zM@MICAU1Te_H=aipFVLnlhpLQ6U-db*LO0pZ#O;66u_zL>FDT*P_6^wf>dWYfthfA z5STSxa3-^?pfgy3GhEJV`hx%_k?A=XnFJz=ftD6K>wv8W8e{1KF&pkskk%BPKu5<w z2my9{sH0;jNW#=f$KTP>AH)pM@pg3d2C;l}Kt7rtc#TPf6|8$Z<2fcPUPjY)o7+s& JZEiD90RR$Tfv*4n delta 507 zcmbQSjsL=S{)R1#t9`eh^kwwqnto4%Nn(0u6r<eq{s=}+KI@pgm^=lA<ovwi5{1m^ zFQXV`IZBIibQG#%@}>*KGD`Afq$Z}M78P58MO+gY^?7S`Kw1<OlJZk3t)|~kV3ZaC zi_~gDP0LJE&``{rt`^Ox!=DK>I58(DD7Cl%Xh-Vwb!m)Z?QbI(x4(^GI-JRr;ky07 zA*M=BPG3hyUmbTxNB8M_&oW8X7dtyT7CY+zNeE#LWaPM7dpbHh>bN>Oy6S+{r#d>O zf;B<7-j0sm5G^2s!0JFcjGc8n9UVO(YT%kdGC58V8GlDdf4K4_ph69xW0D;mlOc9x zJ2^ULJApJ*IO~8c0Wp2RTBirLFo{n0xx^%#1`;fAhBybs-98{0sI$Qi0n38j4rYO! z1R_A{jG;~f`!C$lF&yF{xa#TuJ(xsUK`JAr>s(=y-5zt6Nu8I)(ofrY`b05i$@T@e Ln5Hkd#asaZ6Udh9 diff --git a/sveltejs/src/InstallListProcessor.js b/sveltejs/src/InstallListProcessor.js index 201989f0b..222eb3060 100644 --- a/sveltejs/src/InstallListProcessor.js +++ b/sveltejs/src/InstallListProcessor.js @@ -2,6 +2,8 @@ import { get, writable } from 'svelte/store'; import { openPopup } from './popup'; import { BASE_URL, CURRENT_PATH } from './constants'; +const { Drupal } = window; + export const updated = writable(0); // Store for the install list. @@ -95,20 +97,16 @@ export const handleError = async (errorResponse) => { * A promise that resolves when the project is activated. */ export const activateProject = async (projectIds) => { - const url = `${BASE_URL}admin/modules/project_browser/activate`; - - const installResponse = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + new Drupal.Ajax( + null, + document.createElement('div'), + { + url: `${BASE_URL}admin/modules/project_browser/activate`, + submit: { + projects: projectIds, + }, }, - body: JSON.stringify(projectIds), - }); - - if (!installResponse.ok) { - await handleError(installResponse); - return; - } + ).execute(); }; /** -- GitLab From f9bbbc02ca47946efe5bfd4bc75108e3a0ac673a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 15:38:24 -0500 Subject: [PATCH 35/45] Create a dedicated AJAX command to refresh the project --- src/Controller/InstallerController.php | 12 ++++++++--- src/RefreshProjectCommand.php | 28 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 src/RefreshProjectCommand.php diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 89446059c..fab7fff9b 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -2,7 +2,6 @@ namespace Drupal\project_browser\Controller; -use Drupal\Component\Assertion\Inspector; use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Ajax\AjaxResponse; @@ -18,6 +17,7 @@ use Drupal\project_browser\ComposerInstaller\Installer; use Drupal\project_browser\EnabledSourceHandler; use Drupal\project_browser\InstallState; use Drupal\project_browser\ProjectBrowser\Normalizer; +use Drupal\project_browser\RefreshProjectCommand; use Drupal\system\SystemManager; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -413,16 +413,22 @@ final class InstallerController extends ControllerBase { foreach ($request->get('projects', []) as $project_id) { $this->installState->setState($project_id, 'activating'); + [$source_id] = explode('/', $project_id, 2); try { $project = $this->enabledSourceHandler->getStoredProject($project_id); $commands = $this->activationManager->activate($project); if ($commands) { - Inspector::assertAllObjects($commands, CommandInterface::class) or throw new \RuntimeException('Activators can only return \Drupal\Core\Ajax\CommandInterface objects, or NULL.'); - array_walk($commands, $response->addCommand(...)); + foreach ($commands as $command) { + assert($command instanceof CommandInterface); + $response->addCommand($command); + } } $this->installState->setState($project_id, 'installed'); + + $project = $this->normalizer->normalize($project, context: ['source' => $source_id]); + $response->addCommand(new RefreshProjectCommand($project)); } catch (\Throwable $e) { $message = $e->getMessage(); diff --git a/src/RefreshProjectCommand.php b/src/RefreshProjectCommand.php new file mode 100644 index 000000000..c6fab42a0 --- /dev/null +++ b/src/RefreshProjectCommand.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\project_browser; + +use Drupal\Core\Ajax\CommandInterface; + +/** + * An AJAX command to refresh a particular project in the Svelte app. + */ +final class RefreshProjectCommand implements CommandInterface { + + public function __construct( + private readonly array $project, + ) {} + + /** + * {@inheritdoc} + */ + public function render(): array { + return [ + 'command' => 'refresh_project', + 'project' => $this->project, + ]; + } + +} -- GitLab From a45a329e2e1752489365f2b113004470ef8ccc5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 15:44:32 -0500 Subject: [PATCH 36/45] Oh, Stan --- src/Controller/InstallerController.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index fab7fff9b..06a7625e2 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -5,7 +5,6 @@ namespace Drupal\project_browser\Controller; use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Ajax\AjaxResponse; -use Drupal\Core\Ajax\CommandInterface; use Drupal\Core\Ajax\MessageCommand; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Messenger\MessengerInterface; @@ -419,15 +418,13 @@ final class InstallerController extends ControllerBase { $project = $this->enabledSourceHandler->getStoredProject($project_id); $commands = $this->activationManager->activate($project); - if ($commands) { - foreach ($commands as $command) { - assert($command instanceof CommandInterface); - $response->addCommand($command); - } + foreach ($commands ?? [] as $command) { + $response->addCommand($command); } $this->installState->setState($project_id, 'installed'); $project = $this->normalizer->normalize($project, context: ['source' => $source_id]); + assert(is_array($project)); $response->addCommand(new RefreshProjectCommand($project)); } catch (\Throwable $e) { -- GitLab From 2757a98e15898ffdd1bc4ba3fc14f5b8129543a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 15:55:52 -0500 Subject: [PATCH 37/45] Always return AjaxResponse --- src/Controller/InstallerController.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 06a7625e2..be9ce6f34 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -404,10 +404,10 @@ final class InstallerController extends ControllerBase { * @param \Symfony\Component\HttpFoundation\Request $request * The request. * - * @return \Symfony\Component\HttpFoundation\JsonResponse - * Return an AJAX response, or an error message. + * @return \Drupal\Core\Ajax\AjaxResponse + * A response that can be used by the client-side AJAX system. */ - public function activate(Request $request): JsonResponse { + public function activate(Request $request): AjaxResponse { $response = new AjaxResponse(); foreach ($request->get('projects', []) as $project_id) { -- GitLab From 5548d949e330faeafa58e41d586c8bb104835c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 16:21:50 -0500 Subject: [PATCH 38/45] Refactor InstallerControllerTest --- .../Functional/InstallerControllerTest.php | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/tests/src/Functional/InstallerControllerTest.php b/tests/src/Functional/InstallerControllerTest.php index 6b4d6494e..e341517f1 100644 --- a/tests/src/Functional/InstallerControllerTest.php +++ b/tests/src/Functional/InstallerControllerTest.php @@ -180,9 +180,10 @@ class InstallerControllerTest extends BrowserTestBase { // via composer. $contents = $this->drupalGet('admin/modules/project_browser/install-begin'); $this->stageId = Json::decode($contents)['stage_id']; - $response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/core', [ - 'stage_id' => $this->stageId, - ]); + $response = $this->getPostResponse( + Url::fromRoute('project_browser.stage.require', ['stage_id' => $this->stageId]), + ['project_browser_test_mock/core'], + ); $this->assertSame(500, (int) $response->getStatusCode()); $this->assertSame('{"message":"StageEventException: The following package is already installed: drupal\/core\n","phase":"require"}', (string) $response->getBody()); } @@ -207,9 +208,10 @@ class InstallerControllerTest extends BrowserTestBase { * @covers ::require */ private function doRequire(): void { - $response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/awesome_module', [ - 'stage_id' => $this->stageId, - ]); + $response = $this->getPostResponse( + Url::fromRoute('project_browser.stage.require', ['stage_id' => $this->stageId]), + ['project_browser_test_mock/awesome_module'], + ); $expected_output = sprintf('{"phase":"require","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, (string) $response->getBody()); $this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'requiring'); @@ -312,9 +314,10 @@ class InstallerControllerTest extends BrowserTestBase { $result = ValidationResult::createError([$message]); $this->doStart(); TestSubscriber::setTestResult([$result], PreRequireEvent::class); - $response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/awesome_module', [ - 'stage_id' => $this->stageId, - ]); + $response = $this->getPostResponse( + Url::fromRoute('project_browser.stage.require', ['stage_id' => $this->stageId]), + ['project_browser_test_mock/awesome_module'], + ); $this->assertSame(500, (int) $response->getStatusCode()); $this->assertSame('{"message":"StageEventException: This is a PreRequire error.\n","phase":"require"}', (string) $response->getBody()); } @@ -328,9 +331,10 @@ class InstallerControllerTest extends BrowserTestBase { $error = new \Exception('PreRequire did not go well.'); TestSubscriber::setException($error, PreRequireEvent::class); $this->doStart(); - $response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/awesome_module', [ - 'stage_id' => $this->stageId, - ]); + $response = $this->getPostResponse( + Url::fromRoute('project_browser.stage.require', ['stage_id' => $this->stageId]), + ['project_browser_test_mock/awesome_module'], + ); $this->assertSame(500, (int) $response->getStatusCode()); $this->assertSame('{"message":"StageEventException: PreRequire did not go well.","phase":"require"}', (string) $response->getBody()); } @@ -344,9 +348,10 @@ class InstallerControllerTest extends BrowserTestBase { $error = new \Exception('PostRequire did not go well.'); TestSubscriber::setException($error, PostRequireEvent::class); $this->doStart(); - $response = $this->getPostResponse('project_browser.stage.require', 'project_browser_test_mock/awesome_module', [ - 'stage_id' => $this->stageId, - ]); + $response = $this->getPostResponse( + Url::fromRoute('project_browser.stage.require', ['stage_id' => $this->stageId]), + ['project_browser_test_mock/awesome_module'], + ); $this->assertSame(500, (int) $response->getStatusCode()); $this->assertSame('{"message":"StageEventException: PostRequire did not go well.","phase":"require"}', (string) $response->getBody()); } @@ -546,7 +551,10 @@ class InstallerControllerTest extends BrowserTestBase { $assert_session->checkboxNotChecked('edit-modules-views-enable'); $assert_session->checkboxNotChecked('edit-modules-views-ui-enable'); - $response = $this->getPostResponse('project_browser.activate', 'drupal_core/views_ui'); + $response = $this->getPostResponse( + Url::fromRoute('project_browser.activate'), + ['projects' => ['drupal_core/views_ui']], + ); $this->assertSame(200, $response->getStatusCode()); $this->assertTrue(json_validate((string) $response->getBody())); @@ -574,28 +582,21 @@ class InstallerControllerTest extends BrowserTestBase { /** * Sends a POST request to the specified route with the provided project ID. * - * @param string $route_name - * The route to which the POST request is sent. - * @param string|string[] $project_id - * The project ID(s) to include in the POST request body. - * @param array $route_parameters - * (optional) An associative array of route parameters, such as 'stage_id', - * that will be included in the URL. + * @param \Drupal\Core\Url $url + * The URL to which the POST request is sent. + * @param array $payload + * The POST request body. Will be encoded to JSON. * * @return \Psr\Http\Message\ResponseInterface * The response. */ - private function getPostResponse(string $route_name, string|array $project_id, array $route_parameters = []): ResponseInterface { - $post_url = Url::fromRoute($route_name, $route_parameters); - - $request_options = [ + private function getPostResponse(Url $url, array $payload): ResponseInterface { + return $this->makeApiRequest('POST', $url, [ RequestOptions::HEADERS => [ 'Content-Type' => 'application/json', ], - ]; - $request_options[RequestOptions::BODY] = Json::encode((array) $project_id); - - return $this->makeApiRequest('POST', $post_url, $request_options); + RequestOptions::BODY => Json::encode($payload), + ]); } /** -- GitLab From 6e18bb1605c6a0ee89596462e8f1cab63c42a1f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 16:24:46 -0500 Subject: [PATCH 39/45] Fix InstallerControllerTest --- src/Controller/InstallerController.php | 6 +++++- sveltejs/public/build/bundle.js | Bin 273818 -> 273828 bytes sveltejs/public/build/bundle.js.map | Bin 252696 -> 252731 bytes sveltejs/src/InstallListProcessor.js | 2 +- .../Functional/InstallerControllerTest.php | 2 +- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index be9ce6f34..69934e5ab 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -410,7 +410,11 @@ final class InstallerController extends ControllerBase { public function activate(Request $request): AjaxResponse { $response = new AjaxResponse(); - foreach ($request->get('projects', []) as $project_id) { + $projects = $request->getPayload()->get('projects') ?? []; + if ($projects) { + $projects = explode(',', $projects); + } + foreach ($projects as $project_id) { $this->installState->setState($project_id, 'activating'); [$source_id] = explode('/', $project_id, 2); diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index 9bb526895ac6ddcd5b8d4f8211b57cefc2c70ccb..d59cef5586eb27dd4123b401cab5ee01644a81ac 100644 GIT binary patch delta 41 vcmbPrTVTm;feps}TzXmgnRy!OI_jE}6Q+nad-b<_^)mu7({`_Z=74zsP+$*u delta 30 kcmZ2-TVU30feps}lOIeLZw~2i59wzFVy5jO{mcRL0N_v!YybcN diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index a7684502f91867a549ff73a79a93887f08476ac7..faf0e8c683a2111385c42585c547284798c687d2 100644 GIT binary patch delta 64 zcmV-G0Kfm3whz0u4}i1*$XE(4YHw+7C?_l@DTl~d0k_Cl0!eh2#*qRQmrzFn1O_`v WK|^#ym+#X86^AU%0=F#91MCeC`xn3f delta 38 ucmdnJjeo{A{)R1#CnDM}MKEr^6v5<K!e;I1=xplLZgrMvyVY4{Mm_*MN)BoO diff --git a/sveltejs/src/InstallListProcessor.js b/sveltejs/src/InstallListProcessor.js index 222eb3060..5e02ac6f7 100644 --- a/sveltejs/src/InstallListProcessor.js +++ b/sveltejs/src/InstallListProcessor.js @@ -103,7 +103,7 @@ export const activateProject = async (projectIds) => { { url: `${BASE_URL}admin/modules/project_browser/activate`, submit: { - projects: projectIds, + projects: projectIds.join(','), }, }, ).execute(); diff --git a/tests/src/Functional/InstallerControllerTest.php b/tests/src/Functional/InstallerControllerTest.php index e341517f1..05011448d 100644 --- a/tests/src/Functional/InstallerControllerTest.php +++ b/tests/src/Functional/InstallerControllerTest.php @@ -553,7 +553,7 @@ class InstallerControllerTest extends BrowserTestBase { $response = $this->getPostResponse( Url::fromRoute('project_browser.activate'), - ['projects' => ['drupal_core/views_ui']], + ['projects' => 'drupal_core/views_ui'], ); $this->assertSame(200, $response->getStatusCode()); $this->assertTrue(json_validate((string) $response->getBody())); -- GitLab From 06f3ef66952abf1f761829604100fa9b62f0dd47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 16:27:24 -0500 Subject: [PATCH 40/45] Remove pointless call to rebuildContainer() --- src/Controller/InstallerController.php | 2 ++ tests/src/Functional/InstallerControllerTest.php | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 69934e5ab..5043d692f 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -412,8 +412,10 @@ final class InstallerController extends ControllerBase { $projects = $request->getPayload()->get('projects') ?? []; if ($projects) { + assert(is_string($projects)); $projects = explode(',', $projects); } + assert(is_array($projects)); foreach ($projects as $project_id) { $this->installState->setState($project_id, 'activating'); [$source_id] = explode('/', $project_id, 2); diff --git a/tests/src/Functional/InstallerControllerTest.php b/tests/src/Functional/InstallerControllerTest.php index 05011448d..7eec5d1bc 100644 --- a/tests/src/Functional/InstallerControllerTest.php +++ b/tests/src/Functional/InstallerControllerTest.php @@ -558,7 +558,6 @@ class InstallerControllerTest extends BrowserTestBase { $this->assertSame(200, $response->getStatusCode()); $this->assertTrue(json_validate((string) $response->getBody())); - $this->rebuildContainer(); $this->drupalGet('admin/modules'); $assert_session->checkboxChecked('edit-modules-views-enable'); $assert_session->checkboxChecked('edit-modules-views-ui-enable'); -- GitLab From 2c9030f4f93815dcbbb2f35078cd6ec150a8a9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 17:14:53 -0500 Subject: [PATCH 41/45] Adjust one test to account for the behavior change --- .../FunctionalJavascript/ProjectBrowserInstallerUiTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php index f06182970..c29e71767 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php @@ -96,14 +96,15 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->drupalGet('admin/modules/browse/project_browser_test_mock'); $this->waitForProject('Pinky and the Brain') ->pressButton('Install Pinky and the Brain'); - $popup = $this->assertElementIsVisible('css', '.project-browser-popup'); // The Pinky and the Brain module doesn't actually exist in the filesystem, // but the test activator pretends it does, in order to test the presence // of the "Install" button as opposed vs. the default "Add and Install" // button. This happens to be a good way to test mid-install exceptions as // well. // @see \Drupal\project_browser_test\TestActivator::getStatus() - $this->assertStringContainsString('MissingDependencyException: Unable to install modules pinky_brain due to missing modules pinky_brain', $popup->getText()); + $message = 'Unable to install modules pinky_brain due to missing modules pinky_brain'; + $this->assertPageHasText($message); + $this->assertSession()->statusMessageContains($message, 'error'); } /** -- GitLab From 9119d2c0d8e54280d6f4d7097538d366c15bfbce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 17:49:51 -0500 Subject: [PATCH 42/45] Give a few seconds for AJAX to set up...I think --- .../src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php index c29e71767..8d3995413 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php @@ -130,6 +130,8 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { // If we reload, the installation status should be remembered. $this->getSession()->reload(); + // Give the AJAX system a few seconds to attach behaviors. + sleep(3); $this->inputSearchField('image', TRUE); $this->assertElementIsVisible('css', ".search__search-submit")->click(); $card = $this->waitForProject('Image media type'); -- GitLab From 9a54e675564612b1bd85dd3785d9b411ec8f294e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 18:01:24 -0500 Subject: [PATCH 43/45] Revert "Give a few seconds for AJAX to set up...I think" This reverts commit 93a567f3c3eec968b2b75d344eddc8158f537448. --- .../src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php index 8d3995413..c29e71767 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php @@ -130,8 +130,6 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { // If we reload, the installation status should be remembered. $this->getSession()->reload(); - // Give the AJAX system a few seconds to attach behaviors. - sleep(3); $this->inputSearchField('image', TRUE); $this->assertElementIsVisible('css', ".search__search-submit")->click(); $card = $this->waitForProject('Image media type'); -- GitLab From 01deb45e8af2028ba2ca3c3482493db01c64934b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Thu, 20 Feb 2025 18:10:26 -0500 Subject: [PATCH 44/45] My kingdom for an await --- sveltejs/public/build/bundle.js | Bin 273828 -> 273834 bytes sveltejs/public/build/bundle.js.map | Bin 252731 -> 252742 bytes sveltejs/src/InstallListProcessor.js | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index d59cef5586eb27dd4123b401cab5ee01644a81ac..06911682ad912a62539969da86210c79735cd569 100644 GIT binary patch delta 32 mcmZ2-TVT~~feqPxtcm4`nI+9xeeGF&j6lq^J*$t|YCZt->kaS# delta 26 gcmZ2=TVTm;feqPx&4qpKg?)@b%(T6*kJ)lQ0Iop`@Bjb+ diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index faf0e8c683a2111385c42585c547284798c687d2..6507d085be0457013a20ebb51fd977351053032e 100644 GIT binary patch delta 67 zcmdnJjsMs-{)R1#LJ_Qq<%yXk?Sc`E+XW+-f=Xn49UXmjJRKc9b-*lVM@MHc*V@z3 QxvahV4Ab`NGt3A00P^M)@c;k- delta 38 ucmX@Mjeqwx{)R1#LJ{o}5scd<BA5b8rsthu5@*b6Z#%=Zz3mM10X_g9fexDh diff --git a/sveltejs/src/InstallListProcessor.js b/sveltejs/src/InstallListProcessor.js index 5e02ac6f7..67025be16 100644 --- a/sveltejs/src/InstallListProcessor.js +++ b/sveltejs/src/InstallListProcessor.js @@ -97,7 +97,7 @@ export const handleError = async (errorResponse) => { * A promise that resolves when the project is activated. */ export const activateProject = async (projectIds) => { - new Drupal.Ajax( + await new Drupal.Ajax( null, document.createElement('div'), { -- GitLab From 5df425de4e2dc2e7b9b236176212fdf5ab0159bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net> Date: Fri, 21 Feb 2025 14:56:21 -0500 Subject: [PATCH 45/45] Use logException --- src/Controller/InstallerController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index 5043d692f..d2e2c90cc 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -9,6 +9,7 @@ use Drupal\Core\Ajax\MessageCommand; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Url; +use Drupal\Core\Utility\Error; use Drupal\package_manager\Exception\StageException; use Drupal\package_manager\StatusCheckTrait; use Drupal\project_browser\ActivationManager; @@ -441,7 +442,7 @@ final class InstallerController extends ControllerBase { 'type' => MessengerInterface::TYPE_ERROR, ], )); - $this->logger->error($message); + Error::logException($this->logger, $e); } } $this->installState->deleteAll(); -- GitLab