Skip to content
Snippets Groups Projects
Commit 5fb1a6a8 authored by Adam G-H's avatar Adam G-H Committed by Chris Wells
Browse files

Issue #3322601 by phenaproxima, matthieuscarset, tim.plunkett,...

Issue #3322601 by phenaproxima, matthieuscarset, tim.plunkett, chrisfromredfin, leslieg, yesct, thejimbirch: Make ModuleActivator expose a Configure follow-up task for modules which have one
parent 1f2ded66
No related branches found
No related tags found
1 merge request!727Just do this again on 2.0.x
Pipeline #423502 passed
...@@ -5,9 +5,12 @@ declare(strict_types=1); ...@@ -5,9 +5,12 @@ declare(strict_types=1);
namespace Drupal\project_browser; namespace Drupal\project_browser;
use Drupal\Component\Utility\Xss; 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\ActivationStatus;
use Drupal\project_browser\Activator\ActivatorInterface; use Drupal\project_browser\Activator\ActivatorInterface;
use Drupal\project_browser\Activator\InstructionsInterface; use Drupal\project_browser\Activator\InstructionsInterface;
use Drupal\project_browser\Activator\TasksInterface;
use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectBrowser\Project;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
...@@ -26,6 +29,10 @@ final class ActivationManager { ...@@ -26,6 +29,10 @@ final class ActivationManager {
*/ */
private array $activators = []; private array $activators = [];
public function __construct(
private readonly RendererInterface $renderer,
) {}
/** /**
* Registers an activator. * Registers an activator.
* *
...@@ -88,14 +95,20 @@ final class ActivationManager { ...@@ -88,14 +95,20 @@ final class ActivationManager {
* manually, or a URL where they can do so. Will be NULL if the registered * 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 * activator which supports the given project is not capable of generating
* instructions. * 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() * @see \Drupal\project_browser\ProjectBrowser\Project::toArray()
*/ */
public function getActivationInfo(Project $project): array { public function getActivationInfo(Project $project): array {
$data = [];
$activator = $this->getActivatorForProject($project); $activator = $this->getActivatorForProject($project);
$data['status'] = strtolower($activator->getStatus($project)->name); $data = [
'status' => strtolower($activator->getStatus($project)->name),
'commands' => NULL,
'tasks' => [],
];
if ($activator instanceof InstructionsInterface) { if ($activator instanceof InstructionsInterface) {
$data['commands'] = Xss::filter( $data['commands'] = Xss::filter(
...@@ -103,8 +116,19 @@ final class ActivationManager { ...@@ -103,8 +116,19 @@ final class ActivationManager {
[...Xss::getAdminTagList(), 'textarea', 'button'], [...Xss::getAdminTagList(), 'textarea', 'button'],
); );
} }
else {
$data['commands'] = NULL; 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; return $data;
......
...@@ -8,15 +8,16 @@ use Composer\InstalledVersions; ...@@ -8,15 +8,16 @@ use Composer\InstalledVersions;
use Drupal\Core\Extension\ModuleExtensionList; use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleInstallerInterface; use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\File\FileUrlGeneratorInterface; use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Link;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\project_browser\ProjectBrowser\Project; use Drupal\project_browser\ProjectBrowser\Project;
use Drupal\project_browser\ProjectType; use Drupal\project_browser\ProjectType;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\JsonResponse;
/** /**
* An activator for Drupal modules. * An activator for Drupal modules.
*/ */
final class ModuleActivator implements InstructionsInterface { final class ModuleActivator implements InstructionsInterface, TasksInterface {
use InstructionsTrait; use InstructionsTrait;
...@@ -24,8 +25,7 @@ final class ModuleActivator implements InstructionsInterface { ...@@ -24,8 +25,7 @@ final class ModuleActivator implements InstructionsInterface {
private readonly ModuleInstallerInterface $moduleInstaller, private readonly ModuleInstallerInterface $moduleInstaller,
protected readonly ModuleExtensionList $moduleList, protected readonly ModuleExtensionList $moduleList,
protected readonly FileUrlGeneratorInterface $fileUrlGenerator, protected readonly FileUrlGeneratorInterface $fileUrlGenerator,
) { ) {}
}
/** /**
* {@inheritdoc} * {@inheritdoc}
...@@ -50,9 +50,12 @@ final class ModuleActivator implements InstructionsInterface { ...@@ -50,9 +50,12 @@ final class ModuleActivator implements InstructionsInterface {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function activate(Project $project): ?Response { public function activate(Project $project): JsonResponse {
$this->moduleInstaller->install([$project->machineName]); $this->moduleInstaller->install([$project->machineName]);
return NULL;
return new JsonResponse([
'tasks' => $this->getTasks($project),
]);
} }
/** /**
...@@ -104,4 +107,22 @@ final class ModuleActivator implements InstructionsInterface { ...@@ -104,4 +107,22 @@ final class ModuleActivator implements InstructionsInterface {
return $commands; return $commands;
} }
/**
* {@inheritdoc}
*/
public function getTasks(Project $project): array {
$tasks = [];
// If the module isn't active, there's nothing for the user to do.
if ($this->getStatus($project) !== ActivationStatus::Active) {
return $tasks;
}
$info = $this->moduleList->getExtensionInfo($project->machineName);
if (array_key_exists('configure', $info)) {
$tasks[] = Link::createFromRoute($this->t('Configure'), $info['configure']);
}
return $tasks;
}
} }
<?php
declare(strict_types=1);
namespace Drupal\project_browser\Activator;
use Drupal\project_browser\ProjectBrowser\Project;
/**
* An interface for activators that can expose follow-up tasks for a project.
*/
interface TasksInterface extends ActivatorInterface {
/**
* Returns a set of follow-up tasks for a project.
*
* Tasks are exposed as simple links, but could link anywhere. Examples:
* - The configuration form for a module.
* - A setup wizard or dashboard created by a recipe.
* - An uninstall confirmation form for a module.
* - ...etc.
*
* @param \Drupal\project_browser\ProjectBrowser\Project $project
* A project object.
*
* @return \Drupal\Core\Link[]
* An array of follow-up task links for the project.
*/
public function getTasks(Project $project): array;
}
...@@ -546,7 +546,7 @@ class InstallerControllerTest extends BrowserTestBase { ...@@ -546,7 +546,7 @@ class InstallerControllerTest extends BrowserTestBase {
$response = $this->getPostResponse('project_browser.activate', 'drupal_core/views_ui'); $response = $this->getPostResponse('project_browser.activate', 'drupal_core/views_ui');
$this->assertSame(200, $response->getStatusCode()); $this->assertSame(200, $response->getStatusCode());
$this->assertSame('{"status":0}', (string) $response->getBody()); $this->assertTrue(json_validate((string) $response->getBody()));
$this->rebuildContainer(); $this->rebuildContainer();
$this->drupalGet('admin/modules'); $this->drupalGet('admin/modules');
......
<?php
declare(strict_types=1);
namespace Drupal\Tests\project_browser\Kernel;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\KernelTests\KernelTestBase;
use Drupal\project_browser\Activator\ActivationStatus;
use Drupal\project_browser\Activator\ModuleActivator;
use Drupal\project_browser\EnabledSourceHandler;
/**
* Tests the module activator.
*
* @group project_browser
* @covers \Drupal\project_browser\Activator\ModuleActivator
*/
class ModuleActivatorTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'help',
'project_browser',
];
/**
* The activator under test.
*
* @var \Drupal\project_browser\Activator\ModuleActivator
*/
private ModuleActivator $activator;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->activator = $this->container->get(ModuleActivator::class);
}
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container): void {
parent::register($container);
$container->getDefinition(ModuleActivator::class)->setPublic(TRUE);
}
/**
* Tests that the module activator returns a "Configure" task if available.
*/
public function testConfigureLinksAreExposedIfDefined(): void {
$this->config('project_browser.admin_settings')
->set('enabled_sources', ['drupal_core'])
->save();
// Prime the project cache.
/** @var \Drupal\project_browser\EnabledSourceHandler $sources_handler */
$sources_handler = $this->container->get(EnabledSourceHandler::class);
$sources_handler->getProjects('drupal_core');
// The Help module has no configuration options, so it should not have any
// tasks.
$project = $sources_handler->getStoredProject('drupal_core/help');
$this->assertSame(ActivationStatus::Active, $this->activator->getStatus($project));
$this->assertEmpty($this->activator->getTasks($project));
// Block has a configure link, so that should be exposed as a task.
$project = $sources_handler->getStoredProject('drupal_core/block');
$this->assertSame(ActivationStatus::Active, $this->activator->getStatus($project));
$tasks = $this->activator->getTasks($project);
$this->assertNotEmpty($tasks);
$link_text = $tasks[0]->getText();
assert(is_string($link_text) || $link_text instanceof \Stringable);
$this->assertSame('Configure', (string) $link_text);
$this->assertStringStartsWith('block.', $tasks[0]->getUrl()->getRouteName());
// We should not get any tasks for a module which isn't installed.
$project = $sources_handler->getStoredProject('drupal_core/content_moderation');
$this->assertSame(ActivationStatus::Present, $this->activator->getStatus($project));
$this->assertEmpty($this->activator->getTasks($project));
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment