diff --git a/core/modules/layout_builder/layout_builder.install b/core/modules/layout_builder/layout_builder.install index 44001abc8d..acb1e4fdf3 100644 --- a/core/modules/layout_builder/layout_builder.install +++ b/core/modules/layout_builder/layout_builder.install @@ -31,5 +31,10 @@ function layout_builder_install() { } $display->save(); } + + // Clear the rendered cache to ensure the new layout builder flow is used. + // While in many cases the above change will not affect the rendered output, + // the cacheability metadata will have changed and should be processed to + // prepare for future changes. Cache::invalidateTags(['rendered']); } diff --git a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php index 32a96a608b..5de6275bd1 100644 --- a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php +++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php @@ -192,7 +192,7 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s $section = $section_storage->getSection($delta); $layout = $section->getLayout(); - $build = $section->toRenderArray($this->getAvailableContexts($section_storage)); + $build = $section->toRenderArray($this->getAvailableContexts($section_storage), TRUE); $layout_definition = $layout->getPluginDefinition(); foreach ($layout_definition->getRegions() as $region => $info) { diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php index c53c5a76bf..9ff94c9681 100644 --- a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php @@ -2,11 +2,13 @@ namespace Drupal\layout_builder\Entity; +use Drupal\Component\Plugin\DependentPluginInterface; +use Drupal\Component\Plugin\PluginInspectionInterface; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\ContentEntityStorageInterface; use Drupal\Core\Entity\Entity\EntityViewDisplay as BaseEntityViewDisplay; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\FieldableEntityInterface; -use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Plugin\Context\Context; use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\Core\StringTranslation\TranslatableMarkup; @@ -316,6 +318,8 @@ protected function getRuntimeSections(FieldableEntityInterface $entity) { /** * {@inheritdoc} + * + * @todo Move this upstream in https://www.drupal.org/node/2939931. */ public function label() { $bundle_info = \Drupal::service('entity_type.bundle.info')->getBundleInfo($this->getTargetEntityTypeId()); @@ -369,27 +373,68 @@ protected function getRouteParameters() { return $route_parameters; } + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + parent::calculateDependencies(); + + foreach ($this->getSections() as $delta => $section) { + foreach ($section->getComponents() as $uuid => $component) { + $this->calculatePluginDependencies($component->getPlugin($this->getContexts())); + } + } + + return $this; + } + /** * {@inheritdoc} */ public function onDependencyRemoval(array $dependencies) { - $return = parent::onDependencyRemoval($dependencies); - foreach ($dependencies['config'] as $config_entity) { - if (!$config_entity instanceof FieldDefinitionInterface) { - continue; - } + $changed = parent::onDependencyRemoval($dependencies); - $id = 'field_block:' . $this->getTargetEntityTypeId() . ':' . $config_entity->getName(); - foreach ($this->getSections() as $delta => $section) { - foreach ($section->getComponents() as $uuid => $component) { - if ($component->getPluginId() === $id) { - $section->removeComponent($uuid); - $return = TRUE; - } + // Loop through all components and determine if the removed dependencies are + // used by their plugins. + foreach ($this->getSections() as $delta => $section) { + foreach ($section->getComponents() as $uuid => $component) { + $plugin_dependencies = $this->getPluginDependencies($component->getPlugin($this->getContexts())); + $component_removed_dependencies = $this->getPluginRemovedDependencies($plugin_dependencies, $dependencies); + if ($component_removed_dependencies) { + // @todo Allow the plugins to react to their dependency removal in + // https://www.drupal.org/project/drupal/issues/2579743. + $section->removeComponent($uuid); + $changed = TRUE; } } } - return $return; + return $changed; + } + + /** + * Calculates and returns dependencies of a specific plugin instance. + * + * @param \Drupal\Component\Plugin\PluginInspectionInterface $instance + * The plugin instance. + * + * @return array + * An array of dependencies keyed by the type of dependency. + * + * @todo Replace this in https://www.drupal.org/project/drupal/issues/2939925. + */ + protected function getPluginDependencies(PluginInspectionInterface $instance) { + $definition = $instance->getPluginDefinition(); + $dependencies['module'][] = $definition['provider']; + // Plugins can declare additional dependencies in their definition. + if (isset($definition['config_dependencies'])) { + $dependencies = NestedArray::mergeDeep($dependencies, $definition['config_dependencies']); + } + + // If a plugin is dependent, calculate its dependencies. + if ($instance instanceof DependentPluginInterface && $plugin_dependencies = $instance->calculateDependencies()) { + $dependencies = NestedArray::mergeDeep($dependencies, $plugin_dependencies); + } + return $dependencies; } /** diff --git a/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php b/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php index fb8bcb2110..c6e861db6e 100644 --- a/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php +++ b/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php @@ -11,6 +11,7 @@ use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Field\FieldConfigInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FormatterInterface; use Drupal\Core\Field\FormatterPluginManager; @@ -147,8 +148,8 @@ public function build() { protected function blockAccess(AccountInterface $account) { $entity = $this->getEntity(); - // First consult the entity, except not while the entity is being previewed. - $access = empty($entity->in_preview) ? $entity->access('view', $account, TRUE) : AccessResult::allowed(); + // First consult the entity. + $access = $entity->access('view', $account, TRUE); if (!$access->isAllowed()) { return $access; } @@ -366,4 +367,24 @@ protected function getFormatter(array $parents, FormStateInterface $form_state) ]); } + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $dependencies = []; + + // Retrieve the field definition from the context object if it is present. + if ($this->getContext('entity')->hasContextValue()) { + $field_definition = $this->getEntity()->getFieldDefinition($this->fieldName); + } + else { + $field_definition = $this->getFieldDefinition(); + } + + if ($field_definition instanceof FieldConfigInterface) { + $dependencies[$field_definition->getConfigDependencyKey()][] = $field_definition->getConfigDependencyName(); + } + return $dependencies; + } + } diff --git a/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php b/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php index a0d986bcf1..94160a7736 100644 --- a/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php +++ b/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php @@ -75,6 +75,8 @@ public function getDerivativeDefinitions($base_plugin_definition) { 'weight' => 5, 'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id], ]; + // @todo This link should be conditionally displayed, see + // https://www.drupal.org/node/2917777. $this->derivatives["entity.$entity_type_id.layout_builder_revert"] = $base_plugin_definition + [ 'route_name' => "entity.$entity_type_id.layout_builder_revert", 'title' => $this->t('Revert to defaults'), diff --git a/core/modules/layout_builder/src/Section.php b/core/modules/layout_builder/src/Section.php index aa103f133f..04b1cbb2c6 100644 --- a/core/modules/layout_builder/src/Section.php +++ b/core/modules/layout_builder/src/Section.php @@ -68,14 +68,16 @@ public function __construct($layout_id, array $layout_settings = [], array $comp * * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts * An array of available contexts. + * @param bool $in_preview + * TRUE if the section is being previewed, FALSE otherwise. * * @return array * A renderable array representing the content of the section. */ - public function toRenderArray(array $contexts = []) { + public function toRenderArray(array $contexts = [], $in_preview = FALSE) { $regions = []; foreach ($this->getComponents() as $component) { - if ($output = $component->toRenderArray($contexts)) { + if ($output = $component->toRenderArray($contexts, $in_preview)) { $regions[$component->getRegion()][$component->getUuid()] = $output; } } diff --git a/core/modules/layout_builder/src/SectionComponent.php b/core/modules/layout_builder/src/SectionComponent.php index c7b46ef1c9..7af03af132 100644 --- a/core/modules/layout_builder/src/SectionComponent.php +++ b/core/modules/layout_builder/src/SectionComponent.php @@ -90,11 +90,13 @@ public function __construct($uuid, $region, array $configuration = [], array $ad * * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts * An array of available contexts. + * @param bool $in_preview + * TRUE if the component is being previewed, FALSE otherwise. * * @return array * A renderable array representing the content of the component. */ - public function toRenderArray(array $contexts = []) { + public function toRenderArray(array $contexts = [], $in_preview = FALSE) { $output = []; $plugin = $this->getPlugin($contexts); @@ -104,7 +106,7 @@ public function toRenderArray(array $contexts = []) { $access = $plugin->access($this->currentUser(), TRUE); $cacheability = CacheableMetadata::createFromObject($access); - if ($access->isAllowed()) { + if ($in_preview || $access->isAllowed()) { $cacheability->addCacheableDependency($plugin); // @todo Move this to BlockBase in https://www.drupal.org/node/2931040. $output = [ diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php index fd6a6afaf2..63c084cf98 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php @@ -50,12 +50,6 @@ protected function setUp() { ], ], ]); - - $this->drupalLogin($this->drupalCreateUser([ - 'configure any layout', - 'administer node display', - 'administer node fields', - ])); } /** @@ -65,6 +59,12 @@ public function testLayoutBuilderUi() { $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + 'administer node display', + 'administer node fields', + ])); + $this->drupalGet('node/1'); $assert_session->pageTextContains('The first node body'); $assert_session->pageTextNotContains('Powered by Drupal'); @@ -177,4 +177,56 @@ public function testLayoutBuilderUi() { $assert_session->elementNotExists('css', '.field--name-field-my-text'); } + /** + * Tests that component's dependencies are respected during removal. + */ + public function testPluginDependencies() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $this->container->get('module_installer')->install(['menu_ui']); + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + 'administer node display', + 'administer menu', + ])); + + // Create a new menu. + $this->drupalGet('admin/structure/menu/add'); + $page->fillField('label', 'My Menu'); + $page->fillField('id', 'mymenu'); + $page->pressButton('Save'); + + // Add a menu block. + $this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display-layout/default'); + $assert_session->linkExists('Add Block'); + $this->clickLink('Add Block'); + $assert_session->linkExists('My Menu'); + $this->clickLink('My Menu'); + $page->pressButton('Add Block'); + + // Add another block alongside the menu. + $assert_session->linkExists('Add Block'); + $this->clickLink('Add Block'); + $assert_session->linkExists('Powered by Drupal'); + $this->clickLink('Powered by Drupal'); + $page->pressButton('Add Block'); + + // Assert that the blocks are visible, and save the layout. + $assert_session->pageTextContains('Powered by Drupal'); + $assert_session->pageTextContains('My Menu'); + $assert_session->elementExists('css', '.block.menu--mymenu'); + $assert_session->linkExists('Save Layout'); + $this->clickLink('Save Layout'); + + // Delete the menu. + $this->drupalPostForm('admin/structure/menu/manage/mymenu/delete', [], 'Delete'); + + // Ensure that the menu block is gone, but that the other block remains. + $this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display-layout/default'); + $assert_session->pageTextContains('Powered by Drupal'); + $assert_session->pageTextNotContains('My Menu'); + $assert_session->elementNotExists('css', '.block.menu--mymenu'); + } + } diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php index 5d3f17d1e0..bd5504c6b1 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php @@ -34,10 +34,10 @@ protected function setUp() { parent::setUp(); $this->createContentType([ - 'type' => 'bundle_with_section_field', + 'type' => 'bundle_without_section_field', ]); $this->createContentType([ - 'type' => 'bundle_without_section_field', + 'type' => 'bundle_with_section_field', ]); LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default') @@ -284,21 +284,22 @@ public function testLayoutUrlNoSectionField() { public function testLayoutDeletingField() { $assert_session = $this->assertSession(); - LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default') - ->setComponent('body', ['type' => 'text_default']) - ->save(); - $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default'); $assert_session->statusCodeEquals(200); + $assert_session->elementExists('css', '.field--name-body'); // Delete the field from both bundles. - $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/fields/node.bundle_with_section_field.body/delete'); - $this->submitForm([], 'Delete'); $this->drupalGet('/admin/structure/types/manage/bundle_without_section_field/fields/node.bundle_without_section_field.body/delete'); $this->submitForm([], 'Delete'); + $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default'); + $assert_session->statusCodeEquals(200); + $assert_session->elementExists('css', '.field--name-body'); + $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/fields/node.bundle_with_section_field.body/delete'); + $this->submitForm([], 'Delete'); $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default'); $assert_session->statusCodeEquals(200); + $assert_session->elementNotExists('css', '.field--name-body'); } /** diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php index e0800c06b6..034a76628e 100644 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php @@ -44,7 +44,7 @@ public function testGetSectionInvalidDelta() { */ public function testInvalidConfiguration() { $this->setExpectedException(SchemaIncompleteException::class); - $this->sectionStorage->getSection(0)->getComponent('first-uuid')->setConfiguration(['bar' => 'baz']); + $this->sectionStorage->getSection(0)->getComponent('first-uuid')->setConfiguration(['id' => 'foo', 'bar' => 'baz']); $this->sectionStorage->save(); } diff --git a/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php b/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php index 173ad90153..151cf736f9 100644 --- a/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php +++ b/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php @@ -2,14 +2,14 @@ namespace Drupal\Tests\layout_builder\Kernel; -use Drupal\KernelTests\KernelTestBase; +use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; use Drupal\layout_builder\Section; use Drupal\layout_builder\SectionComponent; /** * Provides a base class for testing implementations of section storage. */ -abstract class SectionStorageTestBase extends KernelTestBase { +abstract class SectionStorageTestBase extends EntityKernelTestBase { /** * {@inheritdoc} @@ -18,9 +18,6 @@ 'layout_builder', 'layout_discovery', 'layout_test', - 'user', - 'entity_test', - 'system', ]; /** @@ -40,10 +37,10 @@ protected function setUp() { $section_data = [ new Section('layout_test_plugin', [], [ - 'first-uuid' => new SectionComponent('first-uuid', 'content'), + 'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']), ]), new Section('layout_test_plugin', ['setting_1' => 'bar'], [ - 'second-uuid' => new SectionComponent('second-uuid', 'content'), + 'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']), ]), ]; $this->sectionStorage = $this->getSectionStorage($section_data); @@ -66,10 +63,10 @@ protected function setUp() { public function testGetSections() { $expected = [ new Section('layout_test_plugin', [], [ - 'first-uuid' => new SectionComponent('first-uuid', 'content'), + 'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']), ]), new Section('layout_test_plugin', ['setting_1' => 'bar'], [ - 'second-uuid' => new SectionComponent('second-uuid', 'content'), + 'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']), ]), ]; $this->assertSections($expected); @@ -96,11 +93,11 @@ public function testGetSectionInvalidDelta() { public function testInsertSection() { $expected = [ new Section('layout_test_plugin', [], [ - 'first-uuid' => new SectionComponent('first-uuid', 'content'), + 'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']), ]), new Section('setting_1'), new Section('layout_test_plugin', ['setting_1' => 'bar'], [ - 'second-uuid' => new SectionComponent('second-uuid', 'content'), + 'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']), ]), ]; @@ -114,10 +111,10 @@ public function testInsertSection() { public function testAppendSection() { $expected = [ new Section('layout_test_plugin', [], [ - 'first-uuid' => new SectionComponent('first-uuid', 'content'), + 'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']), ]), new Section('layout_test_plugin', ['setting_1' => 'bar'], [ - 'second-uuid' => new SectionComponent('second-uuid', 'content'), + 'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']), ]), new Section('foo'), ]; @@ -132,7 +129,7 @@ public function testAppendSection() { public function testRemoveSection() { $expected = [ new Section('layout_test_plugin', ['setting_1' => 'bar'], [ - 'second-uuid' => new SectionComponent('second-uuid', 'content'), + 'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']), ]), ];