Skip to content
Snippets Groups Projects
Unverified Commit e77ca8b3 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3143635 by b_sharpe, tim.plunkett, alexpott: Change Layout Preparation...

Issue #3143635 by b_sharpe, tim.plunkett, alexpott: Change Layout Preparation into an Event to allow proper alterations
parent 032661ed
No related branches found
No related tags found
No related merge requests found
Showing
with 478 additions and 35 deletions
......@@ -51,3 +51,8 @@ services:
class: Drupal\layout_builder\Controller\LayoutBuilderHtmlEntityFormController
public: false
arguments: ['@layout_builder.controller.entity_form.inner']
layout_builder.element.prepare_layout:
class: Drupal\layout_builder\EventSubscriber\PrepareLayout
arguments: ['@layout_builder.tempstore_repository', '@messenger']
tags:
- { name: event_subscriber }
......@@ -3,18 +3,18 @@
namespace Drupal\layout_builder\Element;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\RenderElement;
use Drupal\Core\Url;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\Event\PrepareLayoutEvent;
use Drupal\layout_builder\LayoutBuilderEvents;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\OverridesSectionStorageInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Defines a render element for building the Layout Builder UI.
......@@ -31,18 +31,11 @@ class LayoutBuilder extends RenderElement implements ContainerFactoryPluginInter
use LayoutBuilderHighlightTrait;
/**
* The layout tempstore repository.
* The event dispatcher.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $layoutTempstoreRepository;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
protected $eventDispatcher;
/**
* Constructs a new LayoutBuilder.
......@@ -53,15 +46,24 @@ class LayoutBuilder extends RenderElement implements ContainerFactoryPluginInter
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher service.
* @param \Drupal\Core\Messenger\MessengerInterface|null $messenger
* The messenger service. This is no longer used and will be removed in
* drupal:10.0.0.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, $event_dispatcher, $messenger = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->messenger = $messenger;
if (!($event_dispatcher instanceof EventDispatcherInterface)) {
@trigger_error('The event_dispatcher service should be passed to LayoutBuilder::__construct() instead of the layout_builder.tempstore_repository service since 9.1.0. This will be required in Drupal 10.0.0. See https://www.drupal.org/node/3152690', E_USER_DEPRECATED);
$event_dispatcher = \Drupal::service('event_dispatcher');
}
$this->eventDispatcher = $event_dispatcher;
if ($messenger) {
@trigger_error('Calling LayoutBuilder::__construct() with the $messenger argument is deprecated in drupal:9.1.0 and will be removed in drupal:10.0.0. See https://www.drupal.org/node/3152690', E_USER_DEPRECATED);
}
}
/**
......@@ -72,8 +74,7 @@ public static function create(ContainerInterface $container, array $configuratio
$configuration,
$plugin_id,
$plugin_definition,
$container->get('layout_builder.tempstore_repository'),
$container->get('messenger')
$container->get('event_dispatcher')
);
}
......@@ -145,19 +146,8 @@ protected function layout(SectionStorageInterface $section_storage) {
* The section storage.
*/
protected function prepareLayout(SectionStorageInterface $section_storage) {
// If the layout has pending changes, add a warning.
if ($this->layoutTempstoreRepository->has($section_storage)) {
$this->messenger->addWarning($this->t('You have unsaved changes.'));
}
// If the layout is an override that has not yet been overridden, copy the
// sections from the corresponding default.
elseif ($section_storage instanceof OverridesSectionStorageInterface && !$section_storage->isOverridden()) {
$sections = $section_storage->getDefaultSectionStorage()->getSections();
foreach ($sections as $section) {
$section_storage->appendSection($section);
}
$this->layoutTempstoreRepository->set($section_storage);
}
$event = new PrepareLayoutEvent($section_storage);
$this->eventDispatcher->dispatch($event, LayoutBuilderEvents::PREPARE_LAYOUT);
}
/**
......
<?php
namespace Drupal\layout_builder\Event;
use Drupal\layout_builder\SectionStorageInterface;
use Drupal\Component\EventDispatcher\Event;
/**
* Event fired in #pre_render of \Drupal\layout_builder\Element\LayoutBuilder.
*
* Subscribers to this event can prepare section storage before rendering.
*
* @see \Drupal\layout_builder\LayoutBuilderEvents::PREPARE_LAYOUT
* @see \Drupal\layout_builder\Element\LayoutBuilder::prepareLayout()
*/
class PrepareLayoutEvent extends Event {
/**
* The section storage plugin.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new PrepareLayoutEvent.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage preparing the Layout.
*/
public function __construct(SectionStorageInterface $section_storage) {
$this->sectionStorage = $section_storage;
}
/**
* Gets the section storage.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* The section storage.
*/
public function getSectionStorage(): SectionStorageInterface {
return $this->sectionStorage;
}
}
<?php
namespace Drupal\layout_builder\EventSubscriber;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\layout_builder\Event\PrepareLayoutEvent;
use Drupal\layout_builder\LayoutBuilderEvents;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\OverridesSectionStorageInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* An event subscriber to prepare section storage via the
* \Drupal\layout_builder\Event\PrepareLayoutEvent.
*
* @see \Drupal\layout_builder\Event\PrepareLayoutEvent
* @see \Drupal\layout_builder\Element\LayoutBuilder::prepareLayout()
*/
class PrepareLayout implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a new PrepareLayout.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The tempstore repository.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[LayoutBuilderEvents::PREPARE_LAYOUT][] = ['onPrepareLayout', 10];
return $events;
}
/**
* Prepares a layout for use in the UI.
*
* @param \Drupal\layout_builder\Event\PrepareLayoutEvent $event
* The prepare layout event.
*/
public function onPrepareLayout(PrepareLayoutEvent $event) {
$section_storage = $event->getSectionStorage();
// If the layout has pending changes, add a warning.
if ($this->layoutTempstoreRepository->has($section_storage)) {
$this->messenger->addWarning($this->t('You have unsaved changes.'));
}
// If the layout is an override that has not yet been overridden, copy the
// sections from the corresponding default.
elseif ($section_storage instanceof OverridesSectionStorageInterface && !$section_storage->isOverridden()) {
$sections = $section_storage->getDefaultSectionStorage()->getSections();
foreach ($sections as $section) {
$section_storage->appendSection($section);
}
$this->layoutTempstoreRepository->set($section_storage);
}
}
}
......@@ -26,4 +26,17 @@ final class LayoutBuilderEvents {
*/
const SECTION_COMPONENT_BUILD_RENDER_ARRAY = 'section_component.build.render_array';
/**
* Name of the event fired in when preparing a layout builder element.
*
* This event allows modules to collaborate on creating the sections used in
* \Drupal\layout_builder\Element\LayoutBuilder during #pre_render.
*
* @see \Drupal\layout_builder\Event\PrepareLayoutEvent
* @see \Drupal\layout_builder\Element\LayoutBuilder
*
* @var string
*/
const PREPARE_LAYOUT = 'prepare_layout';
}
name: 'Layout Builder element test'
type: module
description: 'Support module for testing the layout builder element.'
package: Testing
version: VERSION
dependencies:
- drupal:layout_builder
services:
layout_builder_element_test.prepare_layout:
class: Drupal\layout_builder_element_test\EventSubscriber\TestPrepareLayout
arguments: ['@layout_builder.tempstore_repository', '@messenger']
tags:
- { name: event_subscriber }
<?php
namespace Drupal\layout_builder_element_test\EventSubscriber;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\layout_builder\Event\PrepareLayoutEvent;
use Drupal\layout_builder\LayoutBuilderEvents;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* An event subscriber to test altering section storage via the
* \Drupal\layout_builder\Event\PrepareLayoutEvent.
*
* @see \Drupal\layout_builder\Event\PrepareLayoutEvent
* @see \Drupal\layout_builder\Element\LayoutBuilder::prepareLayout()
*/
class TestPrepareLayout implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a new TestPrepareLayout.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The tempstore repository.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// Act before core's layout builder subscriber.
$events[LayoutBuilderEvents::PREPARE_LAYOUT][] = ['onBeforePrepareLayout', 20];
// Act after core's layout builder subscriber.
$events[LayoutBuilderEvents::PREPARE_LAYOUT][] = ['onAfterPrepareLayout', -10];
return $events;
}
/**
* Subscriber to test acting before the LB subscriber.
*
* @param \Drupal\layout_builder\Event\PrepareLayoutEvent $event
* The prepare layout event.
*/
public function onBeforePrepareLayout(PrepareLayoutEvent $event) {
$section_storage = $event->getSectionStorage();
$context = $section_storage->getContextValues();
if (!empty($context['entity'])) {
/** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = $context['entity'];
// Node 1 or 2: Append a block to the layout.
if (in_array($entity->id(), ['1', '2'])) {
$section = new Section('layout_onecol');
$section->appendComponent(new SectionComponent('fake-uuid', 'content', [
'id' => 'static_block',
'label' => 'Test static block title',
'label_display' => 'visible',
]));
$section_storage->appendSection($section);
}
// Node 2: Stop event propagation.
if ($entity->id() === '2') {
$event->stopPropagation();
}
}
}
/**
* Subscriber to test acting after the LB subscriber.
*
* @param \Drupal\layout_builder\Event\PrepareLayoutEvent $event
* The prepare layout event.
*/
public function onAfterPrepareLayout(PrepareLayoutEvent $event) {
$section_storage = $event->getSectionStorage();
$context = $section_storage->getContextValues();
if (!empty($context['entity'])) {
/** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = $context['entity'];
// Node 1, 2, or 3: Append a block to the layout.
if (in_array($entity->id(), ['1', '2', '3'])) {
$section = new Section('layout_onecol');
$section->appendComponent(new SectionComponent('fake-uuid', 'content', [
'id' => 'static_block_two',
'label' => 'Test second static block title',
'label_display' => 'visible',
]));
$section_storage->appendSection($section);
}
}
}
}
<?php
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the ability to alter a layout builder element while preparing.
*
* @group layout_builder
*/
class LayoutBuilderPrepareLayoutTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_builder',
'node',
'layout_builder_element_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType(['type' => 'bundle_with_section_field']);
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'The first node title',
'body' => [
[
'value' => 'The first node body',
],
],
]);
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'The second node title',
'body' => [
[
'value' => 'The second node body',
],
],
]);
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'The third node title',
'body' => [
[
'value' => 'The third node body',
],
],
]);
}
/**
* Tests that we can alter a Layout Builder element while preparing.
*
* @see \Drupal\layout_builder_element_test\EventSubscriber\TestPrepareLayout;
*/
public function testAlterPrepareLayout() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'access content',
'configure any layout',
'administer node display',
'configure all bundle_with_section_field node layout overrides',
]));
// Add a block to the defaults.
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$page->clickLink('Manage layout');
$page->clickLink('Add block');
$page->clickLink('Powered by Drupal');
$page->fillField('settings[label]', 'Default block title');
$page->checkField('settings[label_display]');
$page->pressButton('Add block');
$page->pressButton('Save layout');
// Check the block is on the node page.
$this->drupalGet('node/1');
$assert_session->pageTextContains('Default block title');
// When we edit the layout, it gets the static blocks.
$this->drupalGet('node/1/layout');
$assert_session->pageTextContains('Test static block title');
$assert_session->pageTextNotContains('Default block title');
$assert_session->pageTextContains('Test second static block title');
// When we edit the second node, only the first event fires.
$this->drupalGet('node/2/layout');
$assert_session->pageTextContains('Test static block title');
$assert_session->pageTextNotContains('Default block title');
$assert_session->pageTextNotContains('Test second static block title');
// When we edit the third node, the default exists PLUS our static block.
$this->drupalGet('node/3/layout');
$assert_session->pageTextNotContains('Test static block title');
$assert_session->pageTextContains('Default block title');
$assert_session->pageTextContains('Test second static block title');
}
}
<?php
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\layout_builder\Element\LayoutBuilder;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Tests the deprecation notices of the layout builder element.
*
* @coversDefaultClass \Drupal\layout_builder\Element\LayoutBuilder
*
* @group layout_builder
*/
class LayoutBuilderElementTest extends KernelTestBase {
/**
* @group legacy
* @expectedDeprecation The event_dispatcher service should be passed to LayoutBuilder::__construct() instead of the layout_builder.tempstore_repository service since 9.1.0. This will be required in Drupal 10.0.0. See https://www.drupal.org/node/3152690
*/
public function testConstructorTempStoreDeprecation() {
$layout_temp_storage = $this->prophesize(LayoutTempstoreRepositoryInterface::class);
$element = new LayoutBuilder(
[],
'element_id',
[],
$layout_temp_storage->reveal()
);
$this->assertNotNull($element);
}
/**
* @group legacy
* @expectedDeprecation Calling LayoutBuilder::__construct() with the $messenger argument is deprecated in drupal:9.1.0 and will be removed in drupal:10.0.0. See https://www.drupal.org/node/3152690
*/
public function testConstructorMessengerDeprecation() {
$event_dispatcher = $this->prophesize(EventDispatcherInterface::class);
$messenger = $this->prophesize(MessengerInterface::class);
$element = new LayoutBuilder(
[],
'element_id',
[],
$event_dispatcher->reveal(),
$messenger->reveal()
);
$this->assertNotNull($element);
}
}
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