From 9393f883a6bbc097bbb38ca405b979d5052e1746 Mon Sep 17 00:00:00 2001 From: Adam G-H <32250-phenaproxima@users.noreply.drupalcode.org> Date: Mon, 10 Feb 2025 22:16:34 +0000 Subject: [PATCH] Issue #3505188 by phenaproxima, tim.plunkett: Simplify EnabledSourceHandler by removing any activation-related logic from it --- src/Controller/InstallerController.php | 64 +++----------- .../ProjectBrowserEndpointController.php | 34 ++++---- src/EnabledSourceHandler.php | 80 ++++++------------ src/InstallState.php | 12 ++- src/ProjectBrowser/Project.php | 20 +---- src/ProjectBrowser/ProjectsResultsPage.php | 18 +++- src/Routing/ProjectBrowserRoutes.php | 19 ----- sveltejs/public/build/bundle.js | Bin 275927 -> 274113 bytes sveltejs/public/build/bundle.js.map | Bin 253970 -> 253240 bytes sveltejs/src/ProjectBrowser.svelte | 30 +++---- .../Functional/EnabledSourceHandlerTest.php | 45 +++------- .../Functional/InstallerControllerTest.php | 20 ++--- .../ProjectBrowserInstallerUiTest.php | 3 +- 13 files changed, 107 insertions(+), 238 deletions(-) diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php index f91671d3f..1a79c2964 100644 --- a/src/Controller/InstallerController.php +++ b/src/Controller/InstallerController.php @@ -13,7 +13,6 @@ use Drupal\project_browser\ActivatorInterface; use Drupal\project_browser\ComposerInstaller\Installer; use Drupal\project_browser\EnabledSourceHandler; use Drupal\project_browser\InstallState; -use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser_test\Plugin\ProjectBrowserSource\ProjectBrowserTestMock; use Drupal\system\SystemManager; use Psr\Log\LoggerInterface; @@ -31,27 +30,6 @@ final class InstallerController extends ControllerBase { use StatusCheckTrait; - /** - * No require or install in progress for a given module. - * - * @var int - */ - protected const STATUS_IDLE = 0; - - /** - * A staging install in progress for a given module. - * - * @var int - */ - protected const STATUS_REQUIRING_PROJECT = 1; - - /** - * A core install in progress for a given project. - * - * @var int - */ - protected const STATUS_INSTALLING_PROJECT = 2; - /** * The endpoint successfully returned the expected data. * @@ -119,28 +97,6 @@ final class InstallerController extends ControllerBase { } } - /** - * Returns the status of the project in the temp store. - * - * @param \Drupal\project_browser\ProjectBrowser\Project $project - * A project whose status to report. - * - * @return \Symfony\Component\HttpFoundation\JsonResponse - * Information about the project's require/install status. - */ - public function inProgress(Project $project): JsonResponse { - $project_state = $this->installState->getStatus($project); - $return = ['status' => self::STATUS_IDLE]; - - if ($project_state !== NULL) { - $return['status'] = ($project_state === 'requiring' || $project_state === 'applying') - ? self::STATUS_REQUIRING_PROJECT - : self::STATUS_INSTALLING_PROJECT; - $return['phase'] = $project_state; - } - return new JsonResponse($return); - } - /** * Provides a JSON response for a given error. * @@ -364,10 +320,10 @@ final class InstallerController extends ControllerBase { */ public function require(Request $request, string $stage_id): JsonResponse { $package_names = []; - foreach ($request->toArray() as $project) { - $project = $this->enabledSourceHandler->getStoredProject($project); - if ($project->source === 'project_browser_test_mock') { - $source = $this->enabledSourceHandler->getCurrentSources()[$project->source] ?? NULL; + foreach ($request->toArray() as $project_id) { + $project = $this->enabledSourceHandler->getStoredProject($project_id); + if (str_starts_with($project_id, 'project_browser_test_mock/')) { + $source = $this->enabledSourceHandler->getCurrentSources()['project_browser_test_mock'] ?? NULL; if ($source === NULL) { return new JsonResponse(['message' => "Cannot download $project->id from any available source"], 500); } @@ -376,7 +332,7 @@ final class InstallerController extends ControllerBase { return new JsonResponse(['message' => "$project->machineName is not safe to add because its security coverage has been revoked"], 500); } } - $this->installState->setState($project, 'requiring'); + $this->installState->setState($project_id, 'requiring'); $package_names[] = $project->packageName; } try { @@ -400,7 +356,7 @@ final class InstallerController extends ControllerBase { */ public function apply(string $stage_id): JsonResponse { foreach (array_keys($this->installState->toArray()) as $project_id) { - $this->installState->setState($this->enabledSourceHandler->getStoredProject($project_id), 'applying'); + $this->installState->setState($project_id, 'applying'); } try { $this->installer->claim($stage_id)->apply(); @@ -464,12 +420,12 @@ final class InstallerController extends ControllerBase { * Status message. */ public function activate(Request $request): Response { - foreach ($request->toArray() as $project) { - $project = $this->enabledSourceHandler->getStoredProject($project); - $this->installState->setState($project, 'activating'); + foreach ($request->toArray() as $project_id) { + $this->installState->setState($project_id, 'activating'); try { + $project = $this->enabledSourceHandler->getStoredProject($project_id); $response = $this->activator->activate($project); - $this->installState->setState($project, 'installed'); + $this->installState->setState($project_id, 'installed'); } catch (\Throwable $e) { return $this->errorResponse($e, 'project install'); diff --git a/src/Controller/ProjectBrowserEndpointController.php b/src/Controller/ProjectBrowserEndpointController.php index 0c985be25..5b1c4299c 100644 --- a/src/Controller/ProjectBrowserEndpointController.php +++ b/src/Controller/ProjectBrowserEndpointController.php @@ -3,6 +3,7 @@ namespace Drupal\project_browser\Controller; use Drupal\Core\Controller\ControllerBase; +use Drupal\project_browser\ActivatorInterface; use Drupal\project_browser\EnabledSourceHandler; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; @@ -14,14 +15,9 @@ use Symfony\Component\HttpFoundation\Response; */ final class ProjectBrowserEndpointController extends ControllerBase { - /** - * Constructor for endpoint controller. - * - * @param \Drupal\project_browser\EnabledSourceHandler $enabledSource - * The enabled project browser source. - */ public function __construct( private readonly EnabledSourceHandler $enabledSource, + private readonly ActivatorInterface $activator, ) {} /** @@ -30,33 +26,37 @@ final class ProjectBrowserEndpointController extends ControllerBase { public static function create(ContainerInterface $container): static { return new static( $container->get(EnabledSourceHandler::class), + $container->get(ActivatorInterface::class), ); } /** - * Responds to GET requests. - * - * Returns a list of bundles for specified entity. + * Returns a list of projects that match a query. * * @param \Symfony\Component\HttpFoundation\Request $request * The request. * * @return \Symfony\Component\HttpFoundation\JsonResponse - * Typically a project listing. + * A list of projects. + * + * @see \Drupal\project_browser\ProjectBrowser\ProjectsResultsPage */ public function getAllProjects(Request $request): JsonResponse { - $id = $request->query->get('id'); - if ($id) { - assert(is_string($id)); - return new JsonResponse($this->enabledSource->getStoredProject($id)); - } - $current_sources = $this->enabledSource->getCurrentSources(); $query = $this->buildQuery($request); if (!$current_sources || empty($query['source'])) { return new JsonResponse([], Response::HTTP_ACCEPTED); } - return new JsonResponse($this->enabledSource->getProjects($query['source'], $query)); + + // The activator is the source of truth about the status of the project with + // respect to the current site, and it is responsible for generating + // the activation instructions or commands. + $result = $this->enabledSource->getProjects($query['source'], $query); + foreach ($result->list as $project) { + $project->status = $this->activator->getStatus($project); + $project->commands = $this->activator->getInstructions($project); + } + return new JsonResponse($result); } /** diff --git a/src/EnabledSourceHandler.php b/src/EnabledSourceHandler.php index 07933be68..1f3ad4119 100644 --- a/src/EnabledSourceHandler.php +++ b/src/EnabledSourceHandler.php @@ -25,7 +25,6 @@ class EnabledSourceHandler implements LoggerAwareInterface, EventSubscriberInter public function __construct( private readonly ConfigFactoryInterface $configFactory, private readonly ProjectBrowserSourceManager $pluginManager, - private readonly ActivatorInterface $activator, private readonly KeyValueFactoryInterface $keyValueFactory, ) {} @@ -105,10 +104,10 @@ class EnabledSourceHandler implements LoggerAwareInterface, EventSubscriberInter * @param array $query * (optional) The query to pass to the specified source. * - * @return \Drupal\project_browser\ProjectBrowser\ProjectsResultsPage[] - * The result of the query, keyed by source plugin ID. + * @return \Drupal\project_browser\ProjectBrowser\ProjectsResultsPage + * The result of the query. */ - public function getProjects(string $source_id, array $query = []): array { + public function getProjects(string $source_id, array $query = []): ProjectsResultsPage { // Cache only exact query, down to the page number. $cache_key = 'query:' . md5(Json::encode($query)); @@ -118,35 +117,28 @@ class EnabledSourceHandler implements LoggerAwareInterface, EventSubscriberInter // If $results is an array, it's a set of arguments to ProjectsResultsPage, // with a list of project IDs that we expect to be in the data store. if (is_array($results)) { - $results[1] = array_map($this->getStoredProject(...), $results[1]); - $results = new ProjectsResultsPage(...$results); + $results[1] = $storage->getMultiple($results[1]); + $results[1] = array_values($results[1]); + return new ProjectsResultsPage(...$results); } - else { - $results = $this->doQuery($source_id, $query); - - foreach ($results->list as $project) { - // Prefix the local project ID with the source plugin ID, so we can - // look it up unambiguously. - $project->id = $source_id . '/' . $project->id; - - $storage->setIfNotExists($project->id, $project); - // Add activation data to the project. This is volatile, which is why we - // never store it. - $this->getActivationData($project); - } - // If there were no query errors, store the results as a set of arguments - // to ProjectsResultsPage. - if (empty($results->error)) { - $storage->set($cache_key, [ - $results->totalResults, - array_column($results->list, 'id'), - $results->pluginLabel, - $source_id, - $results->error, - ]); - } + $results = $this->doQuery($source_id, $query); + // Cache all the projects individually so they can be loaded by + // ::getStoredProject(). + foreach ($results->list as $project) { + $storage->setIfNotExists($project->id, $project); + } + // If there were no query errors, store the results as a set of arguments + // to ProjectsResultsPage. + if (empty($results->error)) { + $storage->set($cache_key, [ + $results->totalResults, + array_column($results->list, 'id'), + $results->pluginLabel, + $source_id, + $results->error, + ]); } - return [$source_id => $results]; + return $results; } /** @@ -174,35 +166,17 @@ class EnabledSourceHandler implements LoggerAwareInterface, EventSubscriberInter * Looks up a previously stored project by its ID. * * @param string $id - * The project ID. See ::getProjects() for where this is set. + * The fully qualified project ID, in the form `SOURCE_ID/LOCAL_ID`. * * @return \Drupal\project_browser\ProjectBrowser\Project - * The project object, with activation status and commands added. + * The project object. * * @throws \RuntimeException * Thrown if the project is not found in the non-volatile data store. */ public function getStoredProject(string $id): Project { - [$source_id] = explode('/', $id, 2); - $project = $this->keyValue($source_id)->get($id) ?? throw new \RuntimeException("Project '$id' was not found in non-volatile storage."); - $this->getActivationData($project); - return $project; - } - - /** - * Adds activation data to a project object. - * - * @param \Drupal\project_browser\ProjectBrowser\Project $project - * The project object. - */ - private function getActivationData(Project $project): void { - // The project's activator is the source of truth about the status of - // the project with respect to the current site. - $project->status = $this->activator->getStatus($project); - // The activator is responsible for generating the instructions. - $project->commands = $this->activator->getInstructions($project); - // Give the front-end the ID of the source plugin that exposed this project. - [$project->source] = explode('/', $project->id, 2); + [$source_id, $local_id] = explode('/', $id, 2); + return $this->keyValue($source_id)->get($local_id) ?? throw new \RuntimeException("Project '$id' was not found in non-volatile storage."); } /** diff --git a/src/InstallState.php b/src/InstallState.php index acc81e53f..dea603390 100644 --- a/src/InstallState.php +++ b/src/InstallState.php @@ -43,11 +43,9 @@ final class InstallState { * Example return value: * [ * 'project_id1' => [ - * 'source' => 'source_plugin_id1', * 'status' => 'requiring', * ], * 'project_id2' => [ - * 'source' => 'source_plugin_id2', * 'status' => 'installing', * ], * '__timestamp' => 1732086755, @@ -64,20 +62,20 @@ final class InstallState { /** * Sets project state and initializes a timestamp if not set. * - * @param \Drupal\project_browser\ProjectBrowser\Project $project - * The project object containing the ID and source of the project. + * @param string $project_id + * The fully qualified ID of the project, in the form `SOURCE_ID/LOCAL_ID`. * @param string|null $status * The installation status to set for the project, or NULL if no status. * The status can be any arbitrary string, depending on the context * or use case. */ - public function setState(Project $project, ?string $status): void { + public function setState(string $project_id, ?string $status): void { $this->keyValue->setIfNotExists('__timestamp', $this->time->getRequestTime()); if (is_string($status)) { - $this->keyValue->set($project->id, ['source' => $project->source, 'status' => $status]); + $this->keyValue->set($project_id, ['status' => $status]); } else { - $this->keyValue->delete($project->id); + $this->keyValue->delete($project_id); } } diff --git a/src/ProjectBrowser/Project.php b/src/ProjectBrowser/Project.php index 953f997cb..46b6b9346 100644 --- a/src/ProjectBrowser/Project.php +++ b/src/ProjectBrowser/Project.php @@ -19,24 +19,9 @@ class Project implements \JsonSerializable { /** * A persistent ID for this project in non-volatile storage. * - * This property is internal and should be ignored by source plugins. - * * @var string - * - * @see \Drupal\project_browser\EnabledSourceHandler::getProjects() - */ - public string $id; - - /** - * The ID of the source plugin which exposed this project. - * - * This property is internal and should be ignored by source plugins. - * - * @var string - * - * @see \Drupal\project_browser\EnabledSourceHandler::getProjects() */ - public string $source; + public readonly string $id; /** * The status of this project in the current site. @@ -56,7 +41,7 @@ class Project implements \JsonSerializable { * * @see \Drupal\project_browser\ActivatorInterface::getInstructions() */ - public string|Url|null $commands; + public string|Url|null $commands = NULL; /** * The project type (e.g., module, theme, recipe, or something else). @@ -232,7 +217,6 @@ class Project implements \JsonSerializable { 'selector_id' => $this->getSelectorId(), 'commands' => $commands, 'id' => $this->id, - 'source' => $this->source, ]; } diff --git a/src/ProjectBrowser/ProjectsResultsPage.php b/src/ProjectBrowser/ProjectsResultsPage.php index 3ce74890f..7e89a60d5 100644 --- a/src/ProjectBrowser/ProjectsResultsPage.php +++ b/src/ProjectBrowser/ProjectsResultsPage.php @@ -7,7 +7,7 @@ use Drupal\Component\Assertion\Inspector; /** * One page of search results from a query. */ -class ProjectsResultsPage { +class ProjectsResultsPage implements \JsonSerializable { /** * Constructor for project browser results page. @@ -34,4 +34,20 @@ class ProjectsResultsPage { assert(Inspector::assertAllObjects($list, Project::class)); } + /** + * {@inheritdoc} + */ + public function jsonSerialize(): array { + // Fully qualify the project IDs before sending them to the front end. + $map = function (Project $project): array { + return [ + 'id' => $this->pluginId . '/' . $project->id, + ] + $project->jsonSerialize(); + }; + + return [ + 'list' => array_map($map, $this->list), + ] + get_object_vars($this); + } + } diff --git a/src/Routing/ProjectBrowserRoutes.php b/src/Routing/ProjectBrowserRoutes.php index 2ca6b7859..c72bd9c4e 100644 --- a/src/Routing/ProjectBrowserRoutes.php +++ b/src/Routing/ProjectBrowserRoutes.php @@ -143,25 +143,6 @@ final class ProjectBrowserRoutes implements ContainerInjectionInterface { 'methods' => ['POST'], ] ); - $routes['project_browser.module.install_in_progress'] = new Route( - '/admin/modules/project_browser/install_in_progress/{source}/{id}', - [ - '_controller' => InstallerController::class . '::inProgress', - '_title' => 'Install in progress', - 'project' => NULL, - ], - [ - '_permission' => 'administer modules', - '_custom_access' => InstallerController::class . '::access', - ], - [ - 'parameters' => [ - 'project' => [ - 'project_browser.project' => ['source', 'id'], - ], - ], - ] - ); $routes['project_browser.install.unlock'] = new Route( '/admin/modules/project_browser/install/unlock', [ diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index a5cccf50992e64ca199944035b6570b47ace8a1b..e6485015a05bd9e1a27627069385f94533919d71 100644 GIT binary patch delta 1048 zcmaiyYfRf!7{>FS_p}r!P{vld4gZV;DVI`8X@LP@%9hQ|Xl9Ht2D1vC>B7aeY?v4$ z8VxFF&_BuZVMY_@ht0%L;_#?(;uIG)=L)z5G6SM9amr<&E`u?N|6)k|=C^a6Jn!ZF z&NDe@J2!3XIzzwKV)b|0qe|*1B@Vk5s0Oy9bOifvkX0@2rpt*?tK{w%@8ZeZl#fHV zI1MESsR+{@WW&r~Y{zgi-KCrl(}6V1Tf`RCdze~YLU5EDRNWj2fqNGy4HHqetJVeD zXN?`xF}6zmc=I6bP_Nvkres_{DN?)FEkdnZI5>>HWj5<KYAkr8k-fs>!@wwK3r`ik z8)XN&>PWKN=RwZ}z7Jy;_*3@y8j<@lJJirc)`U@^CNJ?jBz)!Qyvz!NCYgbKQO?_N zF8v|0(Pt8Qh(uY7P1}m=Q4XjF$9SI5<!2K<Hc2}&dMOpb34Rn`P4FYg_?~l7KgByx zuZc?RKThwVXN;@S`WThr^W&6&hg_l<KV0Dfa;cuHEKPZCPj#8gp*@=yJghbQJN#OH zz7}pj7HSG;wY9Yyk{Y<i*94WTz)xI}#AVeU3`PvBXBOEh^p-CzD7_`jwpevxsn*o- z%;WAxqYY?=b1a(*N@h48wpnBMvtG6$$IE7Xv&<>N+laDak%r%vc?7O%F43Pqr(yPW zO2Tk8Th)O-c^BzxU&mu%kP{H;=Oq38^L9k)joWNDr|2J!SpW4qZ!)%&7XHsBy+0a= zA}85~E-RT)ddHyX`bETHY%gbFslmW)ndJ;@-_3XHr_X02`YJtvJu93V+m-H6I=*;~ z?)gtmYmX74iz`NQ+SVx>uA4@Sr5-BO7rwRN(z=nzzN5xC*IQzz8d~Kd!rY+2Aodh| z7AIVK$x5d5Rc?Z9UgI?>TvbDU`L_v8W_iE4qQd8@@TwuR92ew8cf9nO%H2M1r5cHs zZxaHEvSHISm?$+iOvB?M7tf80Y?YECUnKJAkwv>YWs~`2d}cINenM1%)sGAsAEe1C zbz-ZmA@X5rn><8rRb`hyGv*birpIzotNA<q!454H2s@92+uI5pj-C1oSGFPNDkUVw zkULv~;f@V49n)FTDq|wnGo_a741_}Mp;)#yw3;lLvAr$OsWpU-J?(FCJ`f0p{Z9o7 c98Q08b3x3a+Owo)qhg$%qylxeNM<Jd18=u=U;qFB delta 1450 zcmZWpTWnNS6wN;Sw4IsGv@?CtQYg1jYp0Au@1--HKBj;`AR@Nl3qsm5PA^n!J7wmU zheQL$#1B8HxrY7YGsYNv03YIs(WEsJQ$%S&i~$sjAsT^}DG&_^t>+Gu`k0@|IeYDW z)>?b-`^8W0A1=9fe+<8cL8A#TEEB#)aAy=~6`OUVO^-x7+M=;;L*HSvsbw0i8HI9k zZ-p)NegK_fY&jgyl!@}XfWEo`#gueGBl*@sJ80sSweWkc{NJ?+o_mp+mxGPQ!mvn$ zF99&BUk+iheH3b)R;bWQ-$#4bLK)2&haAy&6V_zW&P|X@w<{sX>^zY9QnsCmY&$;O zPfeq;*u+I>WC4xd9>iHJP)+@V=rt$K+s(eKZt@Rdx_PC+M$SqIvg%rDyaWqT3u$y^ z5SOwxm9(SyDF(C_>I>jZv-xngnDI3#OjZ=N-{2d-LMom97KMfFJB|(3Q;Wyoe(JV_ zW(}cY(I}(`hftRo@!k+#hP&V*`tSs9p@AE~sqiE|YF$5b5=(iT>aC}7&eqYYZPH3v zYBqJPhfL}*z(YTOhb7d!QND3>HPl%FmD)Z>2klPhKJuQzgAf#_Pa#iJ7OHis=2dph zdwP?Si1o&!x)KhDmG+1cc_Px;tABC^&#-w_b!wFbvKpC+Es4h?JC#Uc8c2~Hpis0O zJ^R|3q)^Q4X6Jy7DPh`~#FDI3waNub)Y!2>+W6XKET@cPY%U#`#Rh3aXKotyK^~pE z$!zpO67&C?@n#a|nvEmtY4$kIq;ro!HXXXaR#D#^R$`7GvzhMmA#y(}V{=Mb47F-i zoc$34E~pWni@1W(Dh{sv1(lYx6#ctow3d4E9LHH^Q-21HwBkCbppqdh+qV_m^hYuC zc)B7+v_nr!kq@;{X${JGx{OEY;7aH)_n*z9#rt6%DKWH}Z@uFZ4P)p7LI-T6(*sas zessi1?hn94pFJWqj{GT056oab8haelsUnGssQCd%qs6UYo4+jH+Y{;ZDe>6$#L`%A zx8b|n8Q-+^l8Y7y2B|fI#X0|1M5|p8w`RA1Tn0Qz{wxTP+@k=UGo)|(v)~8~kE4f4 zEqmU@!j!L4PX@JKMNi6Hb6_O%4%_6*Fx$K}vYK{pXZ2#mHJlIBk--XS(hd2PWn)fC zcF57Y&?QHC_&TPsP>`hMMIw0}|Kdyyh|wE(J1s5cI$fN^hiq!Fwko8BLWk@42?il@ zBCknT7phe?8cp89UE)jRr7)dQOp}~@@1+TU8dnfPqRGbl0fOeCu>x_QgO`JpI1!TD z)Z^q%(UZwv1*oC%!<a`!F8-^lQ#Ynf6xUpQB|t4TWb=(sEnd&&zhI91P9+SbOHU;9 z?#+5!2`jWDkLTIz9g6(f?RMIm%V)59hq>oe{#@-JO_9*1r?%)(!@td1okWGz7%vS= zIF)=44ySeAaA(U}=vXdaV7qs^nah<mQzyS3kH_Lld3lQKzi<7Wl9tgSw{eP({T^3; Yq`f`$=v0bm&Etyuj*a5^GQQODHz0%ZZU6uP diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index 523888a5249060df4794f8a62742063dc487937c..547b743ececee6a2160a1145beeab700720cfe06 100644 GIT binary patch delta 811 zcmbVKUr1A76z6{D&NbIu(5(DJT@;sW7t_Ry-9~)(&epALu7pqoxop-}%h{dlj}#OI z5g}QT2cJ@+hk{^IaWhQqAw|so;7c#dqO3=u*9iLVCRVR~4~OrZ-|zg+?>l_Arku~7 zJ1<`0o{+IBW2uUa@i^}xf?ZFZNeO--s`YAk-Agv(sFw&uHC?)%)CIRY4=j60DP}yx zWvkJm(Q1=yREnh2;f^28F-&GEnbsp+g5RH4tLaIkkHx#y=!K4MtbNS6u(O1eA@3vQ z*zRL`yH;*~T>j1#VfGQ{ygKJ2#+(lvmmKG0B_7hmAf-W3K~0A;tX(2jbc{n|fmf!1 zMj20aKnX_1p~8|>C~d%u1GeJ832@@wjbOpZ4>o1k1vX2H!L2yz0Cyg)cmXMh1rDJ9 zGowsSz*DUKLhOb<0Sz3sd?kE=N>g$|!HFACKll;^V`d75c<wZ6ui&^vrQx$O4(x`~ zdZw(3EYN)DGH=XuvzhJ}n^u9y3ZRNAW+$P#Ag<8EY(U90DE1KTm1DBlOm_tE(;e{G zB`HJ`fqkqJ$Kh$Hw8nBt80#KD#o#w$O{la*F$YmmTUGuKFO(``n9^|0sr2_r%cehb zL<rYSf(s9pLm>`qf-+-d2KqU!54X*N$aNbXv+x%9BeZFcv3DLW^A^^s9Kv7kq1v9? ziewgaTzp81H;C##3(G+<8Q--+@s>79+y3lQl2qE%C+8o@x+sp^hV{5}0oLMcjTB-g zMAlfT)J7Hd7jAxlVjEjP(Bubkk?CQ}5b$_!0X$r<@n!)kZLnQ##dH?VV^0>Yk-BF5 UnT3An3AAHo7D|o#AK@hT8*N(s5&!@I delta 1210 zcmZvaU2GIp6vs37?5yl=>+;oTLEvtYy6p~Ig5cV08hdAEEnBi(u~mZ5kZE_=4`+9l z*<DJd#2SqTqA~TsLt^BK_cjqSN`sJq!Ndow`UEc+V|-Do@kxEqduQ8jO`M0DIp_Y* z|NPFG`Sg1DgInR*x1_7&vprpwS*JC-;Y^w}IzW$)Cr?x;70H}YH<;rX)9(Bpa@+UR zq+O}iY1ypR%u3#LxVU0YQQc`&4J)4EX(Mk&dy-h{CDGUW`Y5ATkr&vfXuV)k<BU<X zj0wwJYgV^up0}wwcFCM<wu_72;%Nb8C&X?1Rdc!)6+}HApU|xFagr6)8N+IrLbq$Z z(6Oh)XhMIyX*`Y}_mZ6()}qB;LSI~s;-=%+4(;y#Pen5uxA^S!LJvK+N=jQBi5t0` z$GGa`(il0}o-vKd!a7=`<^?Yc*7GeJG19r+#ZxP@(}95|vZsqztwz3BN$1X^@`>~~ zp6n#s@tbGKPJFVTJb^)R@VDnM_M@~Le+rTIcV_yDJJS!F-oS-liQhM^YSTD(9VX<W z#`FXZE|YDzc@etgn#R~c4C|1P^ZZpqwoE$l;bRbxr+7Su8~GJ}y9lSDq#wfS71)NA z8*mM8{Xs%*<OUp+{4tdcTN?hJhK^uaW$dsr#MmfC_QDnn-GOcl|4c&u8ox=X|ErQT z{N)lv@V6g0>wdLQ#mGf?8kgsY>|XjDuKJ|cFmoG9vaPWrbzDkti19XfM9K-gE<t|u z5GL+`CM<lxEIX?fR3*b=c=JyZYfC1xtf~+5L|Mnve-RqUGRuo!5@8e{^2TymV`D?O zF#(-8`{=599AEjHY~IvRc?U68R^eM=N(v)7>=VnC-D_XKMM>(z!MmVHj$6J9_h82; zOARZdd<~+A>%}1|lSf(V1$Xc(c+<DvVA}KRG-=_UVJstrnvxY^t1ip5s)k?QgQ!25 zOtT>e2mj%}!QLeZ;>ZDh?0A<1aQ-{+W19(qV6$7AY5h&%oI$qW_yP%`Jwi5wR%rPB z*>y%^3uJ4@F~*KHr}TgYaY@6uSzf;@400%?92Vc0<EC5L?JHRi-5WxnCyy|#q~rH* zldbsv3F1ebCGDPzl){@|!vRv!ao=U=aDz+G9)M@mG5qL0%;1Oj;Q}d)VB`V3C)M5g I2XI097a1>q4gdfE diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte index 24f1410ba..46ff0b192 100644 --- a/sveltejs/src/ProjectBrowser.svelte +++ b/sveltejs/src/ProjectBrowser.svelte @@ -45,8 +45,6 @@ let rowsCount = 0; let data; let rows = []; - let sources = []; - let dataArray = []; const pageIndex = 0; // first row const preferredView = writable('Grid'); @@ -89,16 +87,12 @@ const res = await fetch(url); if (res.ok) { - const messenger = new Drupal.Message(); data = await res.json(); - // A list of the available sources to get project data. - sources = Object.keys(data); - dataArray = Object.values(data); - rows = data[source].list; - rowsCount = data[source].totalResults; + rows = data.list; + rowsCount = data.totalResults; - if (data[source].error && data[source].error.length) { - messenger.add(data[source].error, { type: 'error' }); + if (data.error && data.error.length) { + new Drupal.Message().add(data.error, { type: 'error' }); } } else { rows = []; @@ -227,15 +221,13 @@ <div class="pb-layout__header"> <div class="pb-search-results"> - {#each dataArray as dataValue} - {#if source === dataValue.pluginId} - {Drupal.formatPlural( - rowsCount, - `${numberFormatter.format(1)} Result`, - `${numberFormatter.format(rowsCount)} Results`, - )} - {/if} - {/each} + {#if data && source === data.pluginId} + {Drupal.formatPlural( + rowsCount, + `${numberFormatter.format(1)} Result`, + `${numberFormatter.format(rowsCount)} Results`, + )} + {/if} </div> {#if matches} diff --git a/tests/src/Functional/EnabledSourceHandlerTest.php b/tests/src/Functional/EnabledSourceHandlerTest.php index 6d44e2f63..6942a41c7 100644 --- a/tests/src/Functional/EnabledSourceHandlerTest.php +++ b/tests/src/Functional/EnabledSourceHandlerTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\Tests\project_browser\Functional; +use Drupal\project_browser\ActivationStatus; use Drupal\project_browser\EnabledSourceHandler; use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser_test\Plugin\ProjectBrowserSource\ProjectBrowserTestMock; @@ -41,10 +42,10 @@ class EnabledSourceHandlerTest extends BrowserTestBase { */ public function testExceptionOnGetUnknownProject(): void { $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage("Project 'unseen' was not found in non-volatile storage."); + $this->expectExceptionMessage("Project 'sight/unseen' was not found in non-volatile storage."); $this->container->get(EnabledSourceHandler::class) - ->getStoredProject('unseen'); + ->getStoredProject('sight/unseen'); } /** @@ -53,43 +54,20 @@ class EnabledSourceHandlerTest extends BrowserTestBase { public function testGetStoredProject(): void { $handler = $this->container->get(EnabledSourceHandler::class); - $projects = $handler->getProjects('project_browser_test_mock'); - assert(!empty($projects)); - $list = reset($projects)->list; - $this->assertNotEmpty($list); - $project = reset($list); + $project = $handler->getProjects('project_browser_test_mock')->list[0]; - $project_again = $handler->getStoredProject($project->id); + $project_again = $handler->getStoredProject('project_browser_test_mock/' . $project->id); $this->assertNotSame($project, $project_again); + // Project::$status is a typed property and therefore must be initialized + // before it is accessed by jsonSerialize(). + $project->status = ActivationStatus::Active; + $project_again->status = ActivationStatus::Active; $this->assertSame($project->jsonSerialize(), $project_again->jsonSerialize()); // The activation status and commands should be set. $this->assertTrue(self::hasActivationData($project_again)); } - /** - * Tests that projects are not stored with any activation data. - */ - public function testProjectsAreStoredWithoutActivationData(): void { - // Projects returned from getProjects() should have their activation status - // and commands set. - $projects = $this->container->get(EnabledSourceHandler::class) - ->getProjects('project_browser_test_mock'); - assert(!empty($projects)); - $list = reset($projects)->list; - $this->assertNotEmpty($list); - $project = reset($list); - $this->assertTrue(self::hasActivationData($project)); - - // But if we pull the project directly from the data store, the `status` and - // `commands` properties should be uninitialized. - $project = $this->container->get('keyvalue') - ->get('project_browser:project_browser_test_mock') - ->get($project->id); - $this->assertInstanceOf(Project::class, $project); - $this->assertFalse(self::hasActivationData($project)); - } - /** * Tests that query results are not stored if there was an error. */ @@ -133,10 +111,9 @@ class EnabledSourceHandlerTest extends BrowserTestBase { * Tests that the install profile is ignored by the drupal_core source. */ public function testProfileNotListedByCoreSource(): void { - $projects = $this->container->get(EnabledSourceHandler::class)->getProjects('drupal_core'); - $this->assertNotEmpty($projects); + $result = $this->container->get(EnabledSourceHandler::class)->getProjects('drupal_core'); // Assert that the current install profile is not returned by the source. - $this->assertNotContains($this->profile, array_column(reset($projects)->list, 'machineName')); + $this->assertNotContains($this->profile, array_column($result->list, 'machineName')); } } diff --git a/tests/src/Functional/InstallerControllerTest.php b/tests/src/Functional/InstallerControllerTest.php index 8ed41aa59..6d370ee5b 100644 --- a/tests/src/Functional/InstallerControllerTest.php +++ b/tests/src/Functional/InstallerControllerTest.php @@ -243,7 +243,7 @@ class InstallerControllerTest extends BrowserTestBase { ]); $expected_output = sprintf('{"phase":"create","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); - $this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'project_browser_test_mock', 'requiring'); + $this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'requiring'); } /** @@ -255,7 +255,7 @@ class InstallerControllerTest extends BrowserTestBase { $this->drupalGet("/admin/modules/project_browser/install-apply/$this->stageId"); $expected_output = sprintf('{"phase":"apply","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); - $this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'project_browser_test_mock', 'applying'); + $this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'applying'); } /** @@ -267,7 +267,7 @@ class InstallerControllerTest extends BrowserTestBase { $this->drupalGet("/admin/modules/project_browser/install-post_apply/$this->stageId"); $expected_output = sprintf('{"phase":"post apply","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); - $this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'project_browser_test_mock', 'applying'); + $this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'applying'); } /** @@ -279,7 +279,7 @@ class InstallerControllerTest extends BrowserTestBase { $this->drupalGet("/admin/modules/project_browser/install-destroy/$this->stageId"); $expected_output = sprintf('{"phase":"destroy","status":0,"stage_id":"%s"}', $this->stageId); $this->assertSame($expected_output, $this->getSession()->getPage()->getContent()); - $this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'project_browser_test_mock', 'applying'); + $this->assertInstallInProgress('project_browser_test_mock/awesome_module', 'applying'); } /** @@ -469,7 +469,6 @@ class InstallerControllerTest extends BrowserTestBase { $assert_unlock_response($response, "The process for adding the project that was locked less than 1 minutes ago might still be in progress. Consider waiting a few more minutes before using [+unlock link]."); $expected = [ 'project_browser_test_mock/awesome_module' => [ - 'source' => 'project_browser_test_mock', 'status' => 'requiring', ], ]; @@ -593,21 +592,14 @@ class InstallerControllerTest extends BrowserTestBase { * * @param string $project_id * The ID of the project being enabled. - * @param string $source - * The project source. - * @param string $status + * @param string|null $status * The install state. */ - protected function assertInstallInProgress(string $project_id, string $source, ?string $status = NULL): void { + protected function assertInstallInProgress(string $project_id, ?string $status = NULL): void { $expect_install[$project_id] = [ - 'source' => $source, 'status' => $status, ]; $this->assertSame($expect_install, $this->getInstallState()->toArray()); - $this->drupalGet("/admin/modules/project_browser/install_in_progress/$project_id"); - $this->assertSame(sprintf('{"status":1,"phase":"%s"}', $status), $this->getSession()->getPage()->getContent()); - $this->drupalGet('/admin/modules/project_browser/install_in_progress/project_browser_test_mock/metatag'); - $this->assertSame('{"status":0}', $this->getSession()->getPage()->getContent()); } /** diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php index b3dadd9ce..daa07e2ad 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php @@ -349,8 +349,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { */ private function chooseProjectToInstall(array $except_these_machine_names = [], string $source_id = 'project_browser_test_mock'): string { $handler = $this->container->get(EnabledSourceHandler::class); - $projects = $handler->getProjects($source_id); - $results = $projects[$source_id]; + $results = $handler->getProjects($source_id); foreach ($results->list as $project) { if (in_array($project->machineName, $except_these_machine_names, TRUE)) { -- GitLab