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
parent
96b418f515
commit
fbc03520b1
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue