Issue #3448131 by mandclu, phenaproxima, ultrabob, immaculatexavier, alexpott, thejimbirch, mtift, laura.j.johnson@gmail.com: Create flexible config actions to place a block in the admin or default themes

merge-requests/8887/head
Alex Pott 2024-07-23 09:16:45 +01:00
parent 96b418f515
commit fbc03520b1
No known key found for this signature in database
GPG Key ID: BDA67E7EE836E5CE
3 changed files with 270 additions and 0 deletions

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Drupal\block\Plugin\ConfigAction;
use Drupal\block\BlockInterface;
use Drupal\Core\Config\Action\Attribute\ConfigAction;
use Drupal\Core\Config\Action\ConfigActionException;
use Drupal\Core\Config\Action\ConfigActionPluginInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Places a block in either the admin or default theme.
*
* @internal
* This API is experimental.
*/
#[ConfigAction(
id: 'placeBlock',
admin_label: new TranslatableMarkup('Place a block'),
entity_types: ['block'],
deriver: PlaceBlockDeriver::class,
)]
final class PlaceBlock implements ConfigActionPluginInterface, ContainerFactoryPluginInterface {
public function __construct(
private readonly ConfigActionPluginInterface $createAction,
private readonly string $whichTheme,
private readonly ConfigFactoryInterface $configFactory,
private readonly ConfigEntityStorageInterface $blockStorage,
) {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('plugin.manager.config_action')->createInstance('entity_create:createIfNotExists'),
$plugin_definition['which_theme'],
$container->get(ConfigFactoryInterface::class),
$container->get(EntityTypeManagerInterface::class)->getStorage('block'),
);
}
/**
* {@inheritdoc}
*/
public function apply(string $configName, mixed $value): void {
assert(is_array($value));
$theme = $this->configFactory->get('system.theme')->get($this->whichTheme);
$value['theme'] = $theme;
if (array_key_exists('region', $value)) {
// Since the recipe author might not know ahead of time what theme the
// block is in, they should supply a map whose keys are theme names and
// values are region names, so we know where to place this block. If the
// target theme is not in the map, they should supply the name of a
// fallback region. If all that fails, give up with an exception.
assert(is_array($value['region']));
$value['region'] = $value['region'][$theme] ?? $value['default_region'] ?? throw new ConfigActionException("Cannot determine which region to place this block into, because no default region was provided.");
}
// Allow the recipe author to position the block in the region without
// needing to know exact weights.
if (array_key_exists('position', $value)) {
$blocks = $this->blockStorage->loadByProperties([
'theme' => $theme,
'region' => $value['region'],
]);
// Sort the blocks by weight. Don't use \Drupal\block\Entity\Block::sort()
// here because it seems to be intended to sort blocks in the UI, where
// we really just want to get the weights right in this situation.
uasort($blocks, fn (BlockInterface $a, BlockInterface $b) => $a->getWeight() <=> $b->getWeight());
$value['weight'] = match ($value['position']) {
'first' => reset($blocks)->getWeight() - 1,
'last' => end($blocks)->getWeight() + 1,
};
}
// Remove values that are not valid properties of block entities.
unset($value['position'], $value['default_region']);
// Ensure a weight is set by default.
$value += ['weight' => 0];
$this->createAction->apply($configName, $value);
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Drupal\block\Plugin\ConfigAction;
use Drupal\Component\Plugin\Derivative\DeriverBase;
/**
* Defines a deriver for the `placeBlock` config action.
*
* This creates two actions: `placeBlockInDefaultTheme`, and
* `placeBlockInAdminTheme`. They behave identically except for which theme
* they target.
*/
final class PlaceBlockDeriver extends DeriverBase {
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives['placeBlockInAdminTheme'] = [
'which_theme' => 'admin',
] + $base_plugin_definition;
$this->derivatives['placeBlockInDefaultTheme'] = [
'which_theme' => 'default',
] + $base_plugin_definition;
return parent::getDerivativeDefinitions($base_plugin_definition);
}
}

View File

@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\block\Kernel;
use Drupal\block\Entity\Block;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Config\Action\ConfigActionException;
use Drupal\Core\Config\Action\ConfigActionManager;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ThemeInstallerInterface;
use Drupal\KernelTests\KernelTestBase;
/**
* @covers \Drupal\block\Plugin\ConfigAction\PlaceBlock
* @covers \Drupal\block\Plugin\ConfigAction\PlaceBlockDeriver
* @group block
*/
class ConfigActionsTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'user', 'system'];
private readonly ConfigActionManager $configActionManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->container->get(ThemeInstallerInterface::class)->install([
'olivero',
'claro',
'umami',
]);
$this->config('system.theme')
->set('default', 'olivero')
->set('admin', 'claro')
->save();
$this->configActionManager = $this->container->get('plugin.manager.config_action');
}
/**
* @testWith ["placeBlockInDefaultTheme"]
* ["placeBlockInAdminTheme"]
*/
public function testActionOnlyWorksOnBlocks(string $action): void {
$this->expectException(PluginNotFoundException::class);
$this->expectExceptionMessage("The \"$action\" plugin does not exist.");
$this->configActionManager->applyAction($action, 'user.role.anonymous', []);
}
public function testExistingBlockIsNotChanged(): void {
$extant_region = Block::load('olivero_powered')->getRegion();
$this->assertNotSame('content', $extant_region);
$this->configActionManager->applyAction('placeBlockInDefaultTheme', 'block.block.olivero_powered', [
'plugin' => 'system_powered_by_block',
'region' => [
'olivero' => 'content',
],
]);
// The extant block should be unchanged.
$this->assertSame($extant_region, Block::load('olivero_powered')->getRegion());
}
/**
* @testWith ["placeBlockInDefaultTheme", "olivero", "header"]
* ["placeBlockInAdminTheme", "claro", "page_bottom"]
*/
public function testPlaceBlockInTheme(string $action, string $expected_theme, string $expected_region): void {
$this->configActionManager->applyAction($action, 'block.block.test_block', [
'plugin' => 'system_powered_by_block',
'region' => [
'olivero' => 'header',
'claro' => 'page_bottom',
],
'default_region' => 'content',
]);
$block = Block::load('test_block');
$this->assertInstanceOf(Block::class, $block);
$this->assertSame('system_powered_by_block', $block->getPluginId());
$this->assertSame($expected_theme, $block->getTheme());
$this->assertSame($expected_region, $block->getRegion());
$this->expectException(ConfigActionException::class);
$this->expectExceptionMessage('Cannot determine which region to place this block into, because no default region was provided.');
$this->configActionManager->applyAction($action, 'block.block.no_region', [
'plugin' => 'system_powered_by_block',
'region' => [],
]);
}
public function testPlaceBlockInDefaultRegion(): void {
$this->config('system.theme')->set('default', 'umami')->save();
$this->testPlaceBlockInTheme('placeBlockInDefaultTheme', 'umami', 'content');
}
public function testPlaceBlockAtPosition(): void {
// Ensure there's at least one block already in the region.
$block = Block::create([
'id' => 'block_1',
'theme' => 'olivero',
'region' => 'content_above',
'weight' => 0,
'plugin' => 'system_powered_by_block',
]);
$block->save();
$this->configActionManager->applyAction('placeBlockInDefaultTheme', 'block.block.first', [
'plugin' => $block->getPluginId(),
'region' => [
$block->getTheme() => $block->getRegion(),
],
'position' => 'first',
]);
$this->configActionManager->applyAction('placeBlockInDefaultTheme', 'block.block.last', [
'plugin' => $block->getPluginId(),
'region' => [
$block->getTheme() => $block->getRegion(),
],
'position' => 'last',
]);
// Query for blocks in the region, ordered by weight.
$blocks = $this->container->get(EntityTypeManagerInterface::class)
->getStorage('block')
->getQuery()
->condition('theme', $block->getTheme())
->condition('region', $block->getRegion())
->sort('weight', 'ASC')
->execute();
$this->assertGreaterThanOrEqual(3, $blocks);
$this->assertSame('first', key($blocks));
$this->assertSame('last', end($blocks));
}
}