summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwebchick2018-08-25 17:07:16 (GMT)
committerwebchick2018-08-25 17:09:24 (GMT)
commitfb03eec3010b59db328c88dc1741ecd7ab8f4954 (patch)
tree016ed401bafc1a789e4fca79a120ba24bc5fad8f
parent258712353b1b7bbfc4caa3a754329a8527577fed (diff)
Issue #2957425 by tedbow, johndevman, mpotter, tim.plunkett, hawkeye.twolf, alexpott, Berdir, samuel.mortenson, xjm, kevincrafts, jibran, amateescu, larowlan, twfahey, EclipseGc, sjerdo, japerry, mtodor, phenaproxima, johnzzon, mglaman: Allow the inline creation of non-reusable Custom Blocks in the layout builder
(cherry picked from commit 4ed41f4e8e630ee85793872de350ceda8464add7)
-rw-r--r--core/modules/layout_builder/config/schema/layout_builder.schema.yml17
-rw-r--r--core/modules/layout_builder/layout_builder.install74
-rw-r--r--core/modules/layout_builder/layout_builder.module70
-rw-r--r--core/modules/layout_builder/layout_builder.services.yml3
-rw-r--r--core/modules/layout_builder/src/Access/LayoutPreviewAccessAllowed.php27
-rw-r--r--core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php20
-rw-r--r--core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php156
-rw-r--r--core/modules/layout_builder/src/Form/RevertOverridesForm.php4
-rw-r--r--core/modules/layout_builder/src/InlineBlockEntityOperations.php267
-rw-r--r--core/modules/layout_builder/src/InlineBlockUsage.php111
-rw-r--r--core/modules/layout_builder/src/LayoutBuilderServiceProvider.php40
-rw-r--r--core/modules/layout_builder/src/LayoutEntityHelperTrait.php108
-rw-r--r--core/modules/layout_builder/src/Plugin/Block/InlineBlock.php283
-rw-r--r--core/modules/layout_builder/src/Plugin/Derivative/InlineBlockDeriver.php59
-rw-r--r--core/modules/layout_builder/tests/modules/no_transitions_css/css/css_fix.theme.css23
-rw-r--r--core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.info.yml6
-rw-r--r--core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.libraries.yml5
-rw-r--r--core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.module16
-rw-r--r--core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php1
-rw-r--r--core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockPrivateFilesTest.php291
-rw-r--r--core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTest.php431
-rw-r--r--core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php222
-rw-r--r--core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php87
23 files changed, 2311 insertions, 10 deletions
diff --git a/core/modules/layout_builder/config/schema/layout_builder.schema.yml b/core/modules/layout_builder/config/schema/layout_builder.schema.yml
index b6b5fa6..7faad99 100644
--- a/core/modules/layout_builder/config/schema/layout_builder.schema.yml
+++ b/core/modules/layout_builder/config/schema/layout_builder.schema.yml
@@ -47,3 +47,20 @@ layout_builder.component:
additional:
type: ignore
label: 'Additional data'
+
+inline_block:
+ type: block_settings
+ label: 'Inline block'
+ mapping:
+ view_mode:
+ type: string
+ lable: 'View mode'
+ block_revision_id:
+ type: integer
+ label: 'Block revision ID'
+ block_serialized:
+ type: string
+ label: 'Serialized block'
+
+block.settings.inline_block:*:
+ type: inline_block
diff --git a/core/modules/layout_builder/layout_builder.install b/core/modules/layout_builder/layout_builder.install
index 1bb2a36..ec16a05 100644
--- a/core/modules/layout_builder/layout_builder.install
+++ b/core/modules/layout_builder/layout_builder.install
@@ -6,6 +6,8 @@
*/
use Drupal\Core\Cache\Cache;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Section;
@@ -62,3 +64,75 @@ function layout_builder_update_8601(&$sandbox) {
$sandbox['#finished'] = empty($sandbox['ids']) ? 1 : ($sandbox['count'] - count($sandbox['ids'])) / $sandbox['count'];
}
+
+/**
+ * Implements hook_schema().
+ */
+function layout_builder_schema() {
+ $schema['inline_block_usage'] = [
+ 'description' => 'Track where a block_content entity is used.',
+ 'fields' => [
+ 'block_content_id' => [
+ 'description' => 'The block_content entity ID.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ],
+ 'layout_entity_type' => [
+ 'description' => 'The entity type of the parent entity.',
+ 'type' => 'varchar_ascii',
+ 'length' => EntityTypeInterface::ID_MAX_LENGTH,
+ 'not null' => FALSE,
+ 'default' => '',
+ ],
+ 'layout_entity_id' => [
+ 'description' => 'The ID of the parent entity.',
+ 'type' => 'varchar_ascii',
+ 'length' => 128,
+ 'not null' => FALSE,
+ 'default' => 0,
+ ],
+ ],
+ 'primary key' => ['block_content_id'],
+ 'indexes' => [
+ 'type_id' => ['layout_entity_type', 'layout_entity_id'],
+ ],
+ ];
+ return $schema;
+}
+
+/**
+ * Create the 'inline_block_usage' table.
+ */
+function layout_builder_update_8602() {
+ $inline_block_usage = [
+ 'description' => 'Track where a block_content entity is used.',
+ 'fields' => [
+ 'block_content_id' => [
+ 'description' => 'The block_content entity ID.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ],
+ 'layout_entity_type' => [
+ 'description' => 'The entity type of the parent entity.',
+ 'type' => 'varchar_ascii',
+ 'length' => EntityTypeInterface::ID_MAX_LENGTH,
+ 'not null' => FALSE,
+ 'default' => '',
+ ],
+ 'layout_entity_id' => [
+ 'description' => 'The ID of the parent entity.',
+ 'type' => 'varchar_ascii',
+ 'length' => 128,
+ 'not null' => FALSE,
+ 'default' => 0,
+ ],
+ ],
+ 'primary key' => ['block_content_id'],
+ 'indexes' => [
+ 'type_id' => ['layout_entity_type', 'layout_entity_id'],
+ ],
+ ];
+ Database::getConnection()->schema()->createTable('inline_block_usage', $inline_block_usage);
+}
diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module
index 76ec534..5d7c606 100644
--- a/core/modules/layout_builder/layout_builder.module
+++ b/core/modules/layout_builder/layout_builder.module
@@ -5,6 +5,7 @@
* Provides hook implementations for Layout Builder.
*/
+use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
@@ -12,10 +13,12 @@ use Drupal\field\FieldConfigInterface;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplayStorage;
use Drupal\layout_builder\Form\LayoutBuilderEntityViewDisplayForm;
-use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\layout_builder\Plugin\Block\ExtraFieldBlock;
+use Drupal\layout_builder\InlineBlockEntityOperations;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Access\AccessResult;
/**
* Implements hook_help().
@@ -134,3 +137,68 @@ function layout_builder_module_implements_alter(&$implementations, $hook) {
$implementations['layout_builder'] = $group;
}
}
+
+/**
+ * Implements hook_entity_presave().
+ */
+function layout_builder_entity_presave(EntityInterface $entity) {
+ if (\Drupal::moduleHandler()->moduleExists('block_content')) {
+ /** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
+ $entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
+ $entity_operations->handlePreSave($entity);
+ }
+}
+
+/**
+ * Implements hook_entity_delete().
+ */
+function layout_builder_entity_delete(EntityInterface $entity) {
+ if (\Drupal::moduleHandler()->moduleExists('block_content')) {
+ /** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
+ $entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
+ $entity_operations->handleEntityDelete($entity);
+ }
+}
+
+/**
+ * Implements hook_cron().
+ */
+function layout_builder_cron() {
+ if (\Drupal::moduleHandler()->moduleExists('block_content')) {
+ /** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
+ $entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
+ $entity_operations->removeUnused();
+ }
+}
+
+/**
+ * Implements hook_plugin_filter_TYPE_alter().
+ */
+function layout_builder_plugin_filter_block_alter(array &$definitions, array $extra, $consumer) {
+ // @todo Determine the 'inline_block' blocks should be allowed outside
+ // of layout_builder https://www.drupal.org/node/2979142.
+ if ($consumer !== 'layout_builder') {
+ foreach ($definitions as $id => $definition) {
+ if ($definition['id'] === 'inline_block') {
+ unset($definitions[$id]);
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_access().
+ */
+function layout_builder_block_content_access(EntityInterface $entity, $operation, AccountInterface $account) {
+ /** @var \Drupal\block_content\BlockContentInterface $entity */
+ if ($operation === 'view' || $entity->isReusable() || empty(\Drupal::service('inline_block.usage')->getUsage($entity->id()))) {
+ // If the operation is 'view' or this is reusable block or if this is
+ // non-reusable that isn't used by this module then don't alter the access.
+ return AccessResult::neutral();
+ }
+
+ if ($account->hasPermission('configure any layout')) {
+ return AccessResult::allowed();
+ }
+ return AccessResult::forbidden();
+}
diff --git a/core/modules/layout_builder/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml
index f2360a5..56b4bf8 100644
--- a/core/modules/layout_builder/layout_builder.services.yml
+++ b/core/modules/layout_builder/layout_builder.services.yml
@@ -43,3 +43,6 @@ services:
logger.channel.layout_builder:
parent: logger.channel_base
arguments: ['layout_builder']
+ inline_block.usage:
+ class: Drupal\layout_builder\InlineBlockUsage
+ arguments: ['@database']
diff --git a/core/modules/layout_builder/src/Access/LayoutPreviewAccessAllowed.php b/core/modules/layout_builder/src/Access/LayoutPreviewAccessAllowed.php
new file mode 100644
index 0000000..6754e2b
--- /dev/null
+++ b/core/modules/layout_builder/src/Access/LayoutPreviewAccessAllowed.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\layout_builder\Access;
+
+use Drupal\Core\Access\AccessibleInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Accessible class to allow access for inline blocks in the Layout Builder.
+ *
+ * @internal
+ */
+class LayoutPreviewAccessAllowed implements AccessibleInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
+ if ($operation === 'view') {
+ return $return_as_object ? AccessResult::allowed() : TRUE;
+ }
+ // The layout builder preview should only need 'view' access.
+ return $return_as_object ? AccessResult::forbidden() : FALSE;
+ }
+
+}
diff --git a/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php b/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php
index 181ed82..c2e3bfb 100644
--- a/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php
+++ b/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php
@@ -2,9 +2,11 @@
namespace Drupal\layout_builder\EventSubscriber;
+use Drupal\block_content\Access\RefinableDependentAccessInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Session\AccountInterface;
+use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed;
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
use Drupal\layout_builder\LayoutBuilderEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -56,6 +58,24 @@ class BlockComponentRenderArray implements EventSubscriberInterface {
return;
}
+ // Set block access dependency even if we are not checking access on
+ // this level. The block itself may render another
+ // RefinableDependentAccessInterface object and need to pass on this value.
+ if ($block instanceof RefinableDependentAccessInterface) {
+ $contexts = $event->getContexts();
+ if (isset($contexts['layout_builder.entity'])) {
+ if ($entity = $contexts['layout_builder.entity']->getContextValue()) {
+ if ($event->inPreview()) {
+ // If previewing in Layout Builder allow access.
+ $block->setAccessDependency(new LayoutPreviewAccessAllowed());
+ }
+ else {
+ $block->setAccessDependency($entity);
+ }
+ }
+ }
+ }
+
// Only check access if the component is not being previewed.
if ($event->inPreview()) {
$access = AccessResult::allowed()->setCacheMaxAge(0);
diff --git a/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
new file mode 100644
index 0000000..edc05f8
--- /dev/null
+++ b/core/modules/layout_builder/src/EventSubscriber/SetInlineBlockDependency.php
@@ -0,0 +1,156 @@
+<?php
+
+namespace Drupal\layout_builder\EventSubscriber;
+
+use Drupal\block_content\BlockContentEvents;
+use Drupal\block_content\BlockContentInterface;
+use Drupal\block_content\Event\BlockContentGetDependencyEvent;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\layout_builder\InlineBlockUsage;
+use Drupal\layout_builder\LayoutEntityHelperTrait;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * An event subscriber that returns an access dependency for inline blocks.
+ *
+ * When used within the layout builder the access dependency for inline blocks
+ * will be explicitly set but if access is evaluated outside of the layout
+ * builder then the dependency may not have been set.
+ *
+ * A known example of when the access dependency will not have been set is when
+ * determining 'view' or 'download' access to a file entity that is attached
+ * to a content block via a field that is using the private file system. The
+ * file access handler will evaluate access on the content block without setting
+ * the dependency.
+ *
+ * @internal
+ *
+ * @see \Drupal\file\FileAccessControlHandler::checkAccess()
+ * @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess()
+ */
+class SetInlineBlockDependency implements EventSubscriberInterface {
+
+ use LayoutEntityHelperTrait;
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * The database connection.
+ *
+ * @var \Drupal\Core\Database\Connection
+ */
+ protected $database;
+
+ /**
+ * The inline block usage service.
+ *
+ * @var \Drupal\layout_builder\InlineBlockUsage
+ */
+ protected $usage;
+
+ /**
+ * Constructs SetInlineBlockDependency object.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ * @param \Drupal\Core\Database\Connection $database
+ * The database connection.
+ * @param \Drupal\layout_builder\InlineBlockUsage $usage
+ * The inline block usage service.
+ */
+ public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, InlineBlockUsage $usage) {
+ $this->entityTypeManager = $entity_type_manager;
+ $this->database = $database;
+ $this->usage = $usage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ return [
+ BlockContentEvents::BLOCK_CONTENT_GET_DEPENDENCY => 'onGetDependency',
+ ];
+ }
+
+ /**
+ * Handles the BlockContentEvents::INLINE_BLOCK_GET_DEPENDENCY event.
+ *
+ * @param \Drupal\block_content\Event\BlockContentGetDependencyEvent $event
+ * The event.
+ */
+ public function onGetDependency(BlockContentGetDependencyEvent $event) {
+ if ($dependency = $this->getInlineBlockDependency($event->getBlockContentEntity())) {
+ $event->setAccessDependency($dependency);
+ }
+ }
+
+ /**
+ * Get the access dependency of an inline block.
+ *
+ * If the block is used in an entity that entity will be returned as the
+ * dependency.
+ *
+ * For revisionable entities the entity will only be returned if it is used in
+ * the latest revision of the entity. For inline blocks that are not used in
+ * the latest revision but are used in a previous revision the entity will not
+ * be returned because calling
+ * \Drupal\Core\Access\AccessibleInterface::access() will only check access on
+ * the latest revision. Therefore if the previous revision of the entity was
+ * returned as the dependency access would be granted to inline block
+ * regardless of whether the user has access to the revision in which the
+ * inline block was used.
+ *
+ * @param \Drupal\block_content\BlockContentInterface $block_content
+ * The block content entity.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface|null
+ * Returns the layout dependency.
+ *
+ * @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess()
+ * @see \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender()
+ */
+ protected function getInlineBlockDependency(BlockContentInterface $block_content) {
+ $layout_entity_info = $this->usage->getUsage($block_content->id());
+ if (empty($layout_entity_info)) {
+ // If the block does not have usage information then we cannot set a
+ // dependency. It may be used by another module besides layout builder.
+ return NULL;
+ }
+ /** @var \Drupal\layout_builder\InlineBlockUsage $usage */
+ $layout_entity_storage = $this->entityTypeManager->getStorage($layout_entity_info->layout_entity_type);
+ $layout_entity = $layout_entity_storage->load($layout_entity_info->layout_entity_id);
+ if ($this->isLayoutCompatibleEntity($layout_entity)) {
+ if ($this->isBlockRevisionUsedInEntity($layout_entity, $block_content)) {
+ return $layout_entity;
+ }
+
+ }
+ return NULL;
+ }
+
+ /**
+ * Determines if a block content revision is used in an entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $layout_entity
+ * The layout entity.
+ * @param \Drupal\block_content\BlockContentInterface $block_content
+ * The block content revision.
+ *
+ * @return bool
+ * TRUE if the block content revision is used as an inline block in the
+ * layout entity.
+ */
+ protected function isBlockRevisionUsedInEntity(EntityInterface $layout_entity, BlockContentInterface $block_content) {
+ $sections_blocks_revision_ids = $this->getInlineBlockRevisionIdsInSections($this->getEntitySections($layout_entity));
+ return in_array($block_content->getRevisionId(), $sections_blocks_revision_ids);
+ }
+
+}
diff --git a/core/modules/layout_builder/src/Form/RevertOverridesForm.php b/core/modules/layout_builder/src/Form/RevertOverridesForm.php
index b6d07d9..837140e 100644
--- a/core/modules/layout_builder/src/Form/RevertOverridesForm.php
+++ b/core/modules/layout_builder/src/Form/RevertOverridesForm.php
@@ -103,6 +103,10 @@ class RevertOverridesForm extends ConfirmFormBase {
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Ensure the section storage is loaded from the database.
+ // @todo Remove after https://www.drupal.org/node/2970801.
+ $this->sectionStorage = \Drupal::service('plugin.manager.layout_builder.section_storage')->loadFromStorageId($this->sectionStorage->getStorageType(), $this->sectionStorage->getStorageId());
+
// Remove all sections.
while ($this->sectionStorage->count()) {
$this->sectionStorage->removeSection(0);
diff --git a/core/modules/layout_builder/src/InlineBlockEntityOperations.php b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
new file mode 100644
index 0000000..7e64b83
--- /dev/null
+++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php
@@ -0,0 +1,267 @@
+<?php
+
+namespace Drupal\layout_builder;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\layout_builder\Plugin\Block\InlineBlock;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a class for reacting to entity events related to Inline Blocks.
+ *
+ * @internal
+ */
+class InlineBlockEntityOperations implements ContainerInjectionInterface {
+
+ use LayoutEntityHelperTrait;
+
+ /**
+ * Inline block usage tracking service.
+ *
+ * @var \Drupal\layout_builder\InlineBlockUsage
+ */
+ protected $usage;
+
+ /**
+ * The block content storage.
+ *
+ * @var \Drupal\Core\Entity\EntityStorageInterface
+ */
+ protected $blockContentStorage;
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * Constructs a new EntityOperations object.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+ * The entity type manager service.
+ * @param \Drupal\layout_builder\InlineBlockUsage $usage
+ * Inline block usage tracking service.
+ * @param \Drupal\Core\Database\Connection $database
+ * The database connection.
+ */
+ public function __construct(EntityTypeManagerInterface $entityTypeManager, InlineBlockUsage $usage, Connection $database) {
+ $this->entityTypeManager = $entityTypeManager;
+ $this->blockContentStorage = $entityTypeManager->getStorage('block_content');
+ $this->usage = $usage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('entity_type.manager'),
+ $container->get('inline_block.usage'),
+ $container->get('database')
+ );
+ }
+
+ /**
+ * Remove all unused inline blocks on save.
+ *
+ * Entities that were used in prevision revisions will be removed if not
+ * saving a new revision.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The parent entity.
+ */
+ protected function removeUnusedForEntityOnSave(EntityInterface $entity) {
+ // If the entity is new or '$entity->original' is not set then there will
+ // not be any unused inline blocks to remove.
+ // If this is a revisionable entity then do not remove inline blocks. They
+ // could be referenced in previous revisions even if this is not a new
+ // revision.
+ if ($entity->isNew() || !isset($entity->original) || $entity instanceof RevisionableInterface) {
+ return;
+ }
+ $sections = $this->getEntitySections($entity);
+ // If this is a layout override and there are no sections then it is a new
+ // override.
+ if ($this->isEntityUsingFieldOverride($entity) && empty($sections)) {
+ return;
+ }
+
+ // Delete and remove the usage for inline blocks that were removed.
+ if ($removed_block_ids = $this->getRemovedBlockIds($entity)) {
+ $this->deleteBlocksAndUsage($removed_block_ids);
+ }
+ }
+
+ /**
+ * Gets the IDs of the inline blocks that were removed.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The layout entity.
+ *
+ * @return int[]
+ * The block content IDs that were removed.
+ */
+ protected function getRemovedBlockIds(EntityInterface $entity) {
+ $original_sections = $this->getEntitySections($entity->original);
+ $current_sections = $this->getEntitySections($entity);
+ // Avoid un-needed conversion from revision IDs to block content IDs by
+ // first determining if there are any revisions in the original that are not
+ // also in the current sections.
+ $current_block_content_revision_ids = $this->getInlineBlockRevisionIdsInSections($current_sections);
+ $original_block_content_revision_ids = $this->getInlineBlockRevisionIdsInSections($original_sections);
+ if ($unused_original_revision_ids = array_diff($original_block_content_revision_ids, $current_block_content_revision_ids)) {
+ // If there are any revisions in the original that aren't in the current
+ // there may some blocks that need to be removed.
+ $current_block_content_ids = $this->getBlockIdsForRevisionIds($current_block_content_revision_ids);
+ $unused_original_block_content_ids = $this->getBlockIdsForRevisionIds($unused_original_revision_ids);
+ return array_diff($unused_original_block_content_ids, $current_block_content_ids);
+ }
+ return [];
+ }
+
+ /**
+ * Handles entity tracking on deleting a parent entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The parent entity.
+ */
+ public function handleEntityDelete(EntityInterface $entity) {
+ if ($this->isLayoutCompatibleEntity($entity)) {
+ $this->usage->removeByLayoutEntity($entity);
+ }
+ }
+
+ /**
+ * Handles saving a parent entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The parent entity.
+ */
+ public function handlePreSave(EntityInterface $entity) {
+ if (!$this->isLayoutCompatibleEntity($entity)) {
+ return;
+ }
+ $duplicate_blocks = FALSE;
+
+ if ($sections = $this->getEntitySections($entity)) {
+ if ($this->isEntityUsingFieldOverride($entity)) {
+ if (!$entity->isNew() && isset($entity->original)) {
+ if (empty($this->getEntitySections($entity->original))) {
+ // If there were no sections in the original entity then this is a
+ // new override from a default and the blocks need to be duplicated.
+ $duplicate_blocks = TRUE;
+ }
+ }
+ }
+ $new_revision = FALSE;
+ if ($entity instanceof RevisionableInterface) {
+ // If the parent entity will have a new revision create a new revision
+ // of the block.
+ // @todo Currently revisions are never created for the parent entity.
+ // This will be fixed in https://www.drupal.org/node/2937199.
+ // To work around this always make a revision when the parent entity
+ // is an instance of RevisionableInterface. After the issue is fixed
+ // only create a new revision if '$entity->isNewRevision()'.
+ $new_revision = TRUE;
+ }
+
+ foreach ($this->getInlineBlockComponents($sections) as $component) {
+ $this->saveInlineBlockComponent($entity, $component, $new_revision, $duplicate_blocks);
+ }
+ }
+ $this->removeUnusedForEntityOnSave($entity);
+ }
+
+ /**
+ * Gets a block ID for an inline block plugin.
+ *
+ * @param \Drupal\layout_builder\Plugin\Block\InlineBlock $block_plugin
+ * The inline block plugin.
+ *
+ * @return int
+ * The block content ID or null none available.
+ */
+ protected function getPluginBlockId(InlineBlock $block_plugin) {
+ $configuration = $block_plugin->getConfiguration();
+ if (!empty($configuration['block_revision_id'])) {
+ $revision_ids = $this->getBlockIdsForRevisionIds([$configuration['block_revision_id']]);
+ return array_pop($revision_ids);
+ }
+ return NULL;
+ }
+
+ /**
+ * Delete the inline blocks and the usage records.
+ *
+ * @param int[] $block_content_ids
+ * The block content entity IDs.
+ */
+ protected function deleteBlocksAndUsage(array $block_content_ids) {
+ foreach ($block_content_ids as $block_content_id) {
+ if ($block = $this->blockContentStorage->load($block_content_id)) {
+ $block->delete();
+ }
+ }
+ $this->usage->deleteUsage($block_content_ids);
+ }
+
+ /**
+ * Removes unused inline blocks.
+ *
+ * @param int $limit
+ * The maximum number of inline blocks to remove.
+ */
+ public function removeUnused($limit = 100) {
+ $this->deleteBlocksAndUsage($this->usage->getUnused($limit));
+ }
+
+ /**
+ * Gets blocks IDs for an array of revision IDs.
+ *
+ * @param int[] $revision_ids
+ * The revision IDs.
+ *
+ * @return int[]
+ * The block IDs.
+ */
+ protected function getBlockIdsForRevisionIds(array $revision_ids) {
+ if ($revision_ids) {
+ $query = $this->blockContentStorage->getQuery();
+ $query->condition('revision_id', $revision_ids, 'IN');
+ $block_ids = $query->execute();
+ return $block_ids;
+ }
+ return [];
+ }
+
+ /**
+ * Saves an inline block component.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity with the layout.
+ * @param \Drupal\layout_builder\SectionComponent $component
+ * The section component with an inline block.
+ * @param bool $new_revision
+ * Whether a new revision of the block should be created.
+ * @param bool $duplicate_blocks
+ * Whether the blocks should be duplicated.
+ */
+ protected function saveInlineBlockComponent(EntityInterface $entity, SectionComponent $component, $new_revision, $duplicate_blocks) {
+ /** @var \Drupal\layout_builder\Plugin\Block\InlineBlock $plugin */
+ $plugin = $component->getPlugin();
+ $pre_save_configuration = $plugin->getConfiguration();
+ $plugin->saveBlockContent($new_revision, $duplicate_blocks);
+ $post_save_configuration = $plugin->getConfiguration();
+ if ($duplicate_blocks || (empty($pre_save_configuration['block_revision_id']) && !empty($post_save_configuration['block_revision_id']))) {
+ $this->usage->addUsage($this->getPluginBlockId($plugin), $entity);
+ }
+ $component->setConfiguration($post_save_configuration);
+ }
+
+}
diff --git a/core/modules/layout_builder/src/InlineBlockUsage.php b/core/modules/layout_builder/src/InlineBlockUsage.php
new file mode 100644
index 0000000..098177d
--- /dev/null
+++ b/core/modules/layout_builder/src/InlineBlockUsage.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Drupal\layout_builder;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Service class to track inline block usage.
+ *
+ * @internal
+ */
+class InlineBlockUsage {
+
+ /**
+ * The database connection.
+ *
+ * @var \Drupal\Core\Database\Connection
+ */
+ protected $database;
+
+ /**
+ * Creates an InlineBlockUsage object.
+ *
+ * @param \Drupal\Core\Database\Connection $database
+ * The database connection.
+ */
+ public function __construct(Connection $database) {
+ $this->database = $database;
+ }
+
+ /**
+ * Adds a usage record.
+ *
+ * @param int $block_content_id
+ * The block content id.
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The layout entity.
+ */
+ public function addUsage($block_content_id, EntityInterface $entity) {
+ $this->database->merge('inline_block_usage')
+ ->keys([
+ 'block_content_id' => $block_content_id,
+ 'layout_entity_id' => $entity->id(),
+ 'layout_entity_type' => $entity->getEntityTypeId(),
+ ])->execute();
+ }
+
+ /**
+ * Gets unused inline block IDs.
+ *
+ * @param int $limit
+ * The maximum number of block content entity IDs to return.
+ *
+ * @return int[]
+ * The entity IDs.
+ */
+ public function getUnused($limit = 100) {
+ $query = $this->database->select('inline_block_usage', 't');
+ $query->fields('t', ['block_content_id']);
+ $query->isNull('layout_entity_id');
+ $query->isNull('layout_entity_type');
+ return $query->range(0, $limit)->execute()->fetchCol();
+ }
+
+ /**
+ * Remove usage record by layout entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The layout entity.
+ */
+ public function removeByLayoutEntity(EntityInterface $entity) {
+ $query = $this->database->update('inline_block_usage')
+ ->fields([
+ 'layout_entity_type' => NULL,
+ 'layout_entity_id' => NULL,
+ ]);
+ $query->condition('layout_entity_type', $entity->getEntityTypeId());
+ $query->condition('layout_entity_id', $entity->id());
+ $query->execute();
+ }
+
+ /**
+ * Delete the inline blocks' the usage records.
+ *
+ * @param int[] $block_content_ids
+ * The block content entity IDs.
+ */
+ public function deleteUsage(array $block_content_ids) {
+ $query = $this->database->delete('inline_block_usage')->condition('block_content_id', $block_content_ids, 'IN');
+ $query->execute();
+ }
+
+ /**
+ * Gets usage record for inline block by ID.
+ *
+ * @param int $block_content_id
+ * The block content entity ID.
+ *
+ * @return object
+ * The usage record with properties layout_entity_id and layout_entity_type.
+ */
+ public function getUsage($block_content_id) {
+ $query = $this->database->select('inline_block_usage');
+ $query->condition('block_content_id', $block_content_id);
+ $query->fields('inline_block_usage', ['layout_entity_id', 'layout_entity_type']);
+ $query->range(0, 1);
+ return $query->execute()->fetchObject();
+ }
+
+}
diff --git a/core/modules/layout_builder/src/LayoutBuilderServiceProvider.php b/core/modules/layout_builder/src/LayoutBuilderServiceProvider.php
new file mode 100644
index 0000000..4c4fa63
--- /dev/null
+++ b/core/modules/layout_builder/src/LayoutBuilderServiceProvider.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\layout_builder;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\DependencyInjection\ServiceProviderInterface;
+use Drupal\layout_builder\EventSubscriber\SetInlineBlockDependency;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Reference;
+
+/**
+ * Sets the layout_builder.get_block_dependency_subscriber service definition.
+ *
+ * This service is dependent on the block_content module so it must be provided
+ * dynamically.
+ *
+ * @internal
+ *
+ * @see \Drupal\layout_builder\EventSubscriber\SetInlineBlockDependency
+ */
+class LayoutBuilderServiceProvider implements ServiceProviderInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function register(ContainerBuilder $container) {
+ $modules = $container->getParameter('container.modules');
+ if (isset($modules['block_content'])) {
+ $definition = new Definition(SetInlineBlockDependency::class);
+ $definition->setArguments([
+ new Reference('entity_type.manager'),
+ new Reference('database'),
+ new Reference('inline_block.usage'),
+ ]);
+ $definition->addTag('event_subscriber');
+ $container->setDefinition('layout_builder.get_block_dependency_subscriber', $definition);
+ }
+ }
+
+}
diff --git a/core/modules/layout_builder/src/LayoutEntityHelperTrait.php b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php
new file mode 100644
index 0000000..9124027
--- /dev/null
+++ b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Drupal\layout_builder;
+
+use Drupal\Component\Plugin\DerivativeInspectionInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
+
+/**
+ * Methods to help with entities using the layout builder.
+ *
+ * @internal
+ */
+trait LayoutEntityHelperTrait {
+
+ /**
+ * Determines if an entity can have a layout.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to check.
+ *
+ * @return bool
+ * TRUE if the entity can have a layout otherwise FALSE.
+ */
+ protected function isLayoutCompatibleEntity(EntityInterface $entity) {
+ return $entity instanceof LayoutEntityDisplayInterface || $this->isEntityUsingFieldOverride($entity);
+ }
+
+ /**
+ * Gets revision IDs for layout sections.
+ *
+ * @param \Drupal\layout_builder\Section[] $sections
+ * The layout sections.
+ *
+ * @return int[]
+ * The revision IDs.
+ */
+ protected function getInlineBlockRevisionIdsInSections(array $sections) {
+ $revision_ids = [];
+ foreach ($this->getInlineBlockComponents($sections) as $component) {
+ $configuration = $component->getPlugin()->getConfiguration();
+ if (!empty($configuration['block_revision_id'])) {
+ $revision_ids[] = $configuration['block_revision_id'];
+ }
+ }
+ return $revision_ids;
+ }
+
+ /**
+ * Gets the sections for an entity if any.
+ *
+ * @todo Replace this method with calls to the SectionStorageManagerInterface
+ * method for getting sections from an entity in
+ * https://www.drupal.org/node/2986403.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity.
+ *
+ * @return \Drupal\layout_builder\Section[]|null
+ * The entity layout sections if available.
+ */
+ protected function getEntitySections(EntityInterface $entity) {
+ if ($entity instanceof LayoutEntityDisplayInterface) {
+ return $entity->getSections();
+ }
+ elseif ($this->isEntityUsingFieldOverride($entity)) {
+ return $entity->get('layout_builder__layout')->getSections();
+ }
+ return NULL;
+ }
+
+ /**
+ * Gets components that have Inline Block plugins.
+ *
+ * @param \Drupal\layout_builder\Section[] $sections
+ * The layout sections.
+ *
+ * @return \Drupal\layout_builder\SectionComponent[]
+ * The components that contain Inline Block plugins.
+ */
+ protected function getInlineBlockComponents(array $sections) {
+ $inline_block_components = [];
+ foreach ($sections as $section) {
+ foreach ($section->getComponents() as $component) {
+ $plugin = $component->getPlugin();
+ if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'inline_block') {
+ $inline_block_components[] = $component;
+ }
+ }
+ }
+ return $inline_block_components;
+ }
+
+ /**
+ * Determines if an entity is using a field for the layout override.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity.
+ *
+ * @return bool
+ * TRUE if the entity is using a field for a layout override.
+ */
+ protected function isEntityUsingFieldOverride(EntityInterface $entity) {
+ return $entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout');
+ }
+
+}
diff --git a/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php b/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php
new file mode 100644
index 0000000..9236d02
--- /dev/null
+++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php
@@ -0,0 +1,283 @@
+<?php
+
+namespace Drupal\layout_builder\Plugin\Block;
+
+use Drupal\block_content\Access\RefinableDependentAccessInterface;
+use Drupal\block_content\Access\RefinableDependentAccessTrait;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\SubformStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines an inline block plugin type.
+ *
+ * @Block(
+ * id = "inline_block",
+ * admin_label = @Translation("Inline block"),
+ * category = @Translation("Inline blocks"),
+ * deriver = "Drupal\layout_builder\Plugin\Derivative\InlineBlockDeriver",
+ * )
+ *
+ * @internal
+ * Plugin classes are internal.
+ */
+class InlineBlock extends BlockBase implements ContainerFactoryPluginInterface, RefinableDependentAccessInterface {
+
+ use RefinableDependentAccessTrait;
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * The block content entity.
+ *
+ * @var \Drupal\block_content\BlockContentInterface
+ */
+ protected $blockContent;
+
+ /**
+ * The entity display repository.
+ *
+ * @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
+ */
+ protected $entityDisplayRepository;
+
+ /**
+ * Whether a new block is being created.
+ *
+ * @var bool
+ */
+ protected $isNew = TRUE;
+
+ /**
+ * Constructs a new InlineBlock.
+ *
+ * @param array $configuration
+ * A configuration array containing information about the plugin instance.
+ * @param string $plugin_id
+ * The plugin ID for the plugin instance.
+ * @param mixed $plugin_definition
+ * The plugin implementation definition.
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager service.
+ * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
+ * The entity display repository.
+ */
+ public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityDisplayRepositoryInterface $entity_display_repository) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+ $this->entityTypeManager = $entity_type_manager;
+ $this->entityDisplayRepository = $entity_display_repository;
+ if (!empty($this->configuration['block_revision_id']) || !empty($this->configuration['block_serialized'])) {
+ $this->isNew = FALSE;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('entity_type.manager'),
+ $container->get('entity_display.repository')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'view_mode' => 'full',
+ 'block_revision_id' => NULL,
+ 'block_serialized' => NULL,
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blockForm($form, FormStateInterface $form_state) {
+ $block = $this->getEntity();
+
+ // Add the entity form display in a process callback so that #parents can
+ // be successfully propagated to field widgets.
+ $form['block_form'] = [
+ '#type' => 'container',
+ '#process' => [[static::class, 'processBlockForm']],
+ '#block' => $block,
+ ];
+
+ $options = $this->entityDisplayRepository->getViewModeOptionsByBundle('block_content', $block->bundle());
+
+ $form['view_mode'] = [
+ '#type' => 'select',
+ '#options' => $options,
+ '#title' => $this->t('View mode'),
+ '#description' => $this->t('The view mode in which to render the block.'),
+ '#default_value' => $this->configuration['view_mode'],
+ '#access' => count($options) > 1,
+ ];
+ return $form;
+ }
+
+ /**
+ * Process callback to insert a Custom Block form.
+ *
+ * @param array $element
+ * The containing element.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ *
+ * @return array
+ * The containing element, with the Custom Block form inserted.
+ */
+ public static function processBlockForm(array $element, FormStateInterface $form_state) {
+ /** @var \Drupal\block_content\BlockContentInterface $block */
+ $block = $element['#block'];
+ EntityFormDisplay::collectRenderDisplay($block, 'edit')->buildForm($block, $element, $form_state);
+ $element['revision_log']['#access'] = FALSE;
+ $element['info']['#access'] = FALSE;
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blockValidate($form, FormStateInterface $form_state) {
+ $block_form = $form['block_form'];
+ /** @var \Drupal\block_content\BlockContentInterface $block */
+ $block = $block_form['#block'];
+ $form_display = EntityFormDisplay::collectRenderDisplay($block, 'edit');
+ $complete_form_state = $form_state instanceof SubformStateInterface ? $form_state->getCompleteFormState() : $form_state;
+ $form_display->extractFormValues($block, $block_form, $complete_form_state);
+ $form_display->validateFormValues($block, $block_form, $complete_form_state);
+ // @todo Remove when https://www.drupal.org/project/drupal/issues/2948549 is closed.
+ $form_state->setTemporaryValue('block_form_parents', $block_form['#parents']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function blockSubmit($form, FormStateInterface $form_state) {
+ $this->configuration['view_mode'] = $form_state->getValue('view_mode');
+
+ // @todo Remove when https://www.drupal.org/project/drupal/issues/2948549 is closed.
+ $block_form = NestedArray::getValue($form, $form_state->getTemporaryValue('block_form_parents'));
+ /** @var \Drupal\block_content\BlockContentInterface $block */
+ $block = $block_form['#block'];
+ $form_display = EntityFormDisplay::collectRenderDisplay($block, 'edit');
+ $complete_form_state = $form_state instanceof SubformStateInterface ? $form_state->getCompleteFormState() : $form_state;
+ $form_display->extractFormValues($block, $block_form, $complete_form_state);
+ $block->setInfo($this->configuration['label']);
+ $this->configuration['block_serialized'] = serialize($block);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function blockAccess(AccountInterface $account) {
+ if ($entity = $this->getEntity()) {
+ return $entity->access('view', $account, TRUE);
+ }
+ return AccessResult::forbidden();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build() {
+ $block = $this->getEntity();
+ return $this->entityTypeManager->getViewBuilder($block->getEntityTypeId())->view($block, $this->configuration['view_mode']);
+ }
+
+ /**
+ * Loads or creates the block content entity of the block.
+ *
+ * @return \Drupal\block_content\BlockContentInterface
+ * The block content entity.
+ */
+ protected function getEntity() {
+ if (!isset($this->blockContent)) {
+ if (!empty($this->configuration['block_serialized'])) {
+ $this->blockContent = unserialize($this->configuration['block_serialized']);
+ }
+ elseif (!empty($this->configuration['block_revision_id'])) {
+ $entity = $this->entityTypeManager->getStorage('block_content')->loadRevision($this->configuration['block_revision_id']);
+ $this->blockContent = $entity;
+ }
+ else {
+ $this->blockContent = $this->entityTypeManager->getStorage('block_content')->create([
+ 'type' => $this->getDerivativeId(),
+ 'reusable' => FALSE,
+ ]);
+ }
+ if ($this->blockContent instanceof RefinableDependentAccessInterface && $dependee = $this->getAccessDependency()) {
+ $this->blockContent->setAccessDependency($dependee);
+ }
+ }
+ return $this->blockContent;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $form = parent::buildConfigurationForm($form, $form_state);
+ if ($this->isNew) {
+ // If the Content Block is new then don't provide a default label.
+ unset($form['label']['#default_value']);
+ }
+ $form['label']['#description'] = $this->t('The title of the block as shown to the user.');
+ return $form;
+ }
+
+ /**
+ * Saves the block_content entity for this plugin.
+ *
+ * @param bool $new_revision
+ * Whether to create new revision.
+ * @param bool $duplicate_block
+ * Whether to duplicate the "block_content" entity.
+ */
+ public function saveBlockContent($new_revision = FALSE, $duplicate_block = FALSE) {
+ /** @var \Drupal\block_content\BlockContentInterface $block */
+ $block = NULL;
+ if (!empty($this->configuration['block_serialized'])) {
+ $block = unserialize($this->configuration['block_serialized']);
+ }
+ if ($duplicate_block) {
+ if (empty($block) && !empty($this->configuration['block_revision_id'])) {
+ $block = $this->entityTypeManager->getStorage('block_content')->loadRevision($this->configuration['block_revision_id']);
+ }
+ if ($block) {
+ $block = $block->createDuplicate();
+ }
+ }
+
+ if ($block) {
+ if ($new_revision) {
+ $block->setNewRevision();
+ }
+ $block->save();
+ $this->configuration['block_revision_id'] = $block->getRevisionId();
+ $this->configuration['block_serialized'] = NULL;
+ }
+ }
+
+}
diff --git a/core/modules/layout_builder/src/Plugin/Derivative/InlineBlockDeriver.php b/core/modules/layout_builder/src/Plugin/Derivative/InlineBlockDeriver.php
new file mode 100644
index 0000000..1faeef1
--- /dev/null
+++ b/core/modules/layout_builder/src/Plugin/Derivative/InlineBlockDeriver.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\layout_builder\Plugin\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides inline block plugin definitions for all custom block types.
+ *
+ * @internal
+ */
+class InlineBlockDeriver extends DeriverBase implements ContainerDeriverInterface {
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * Constructs a BlockContentDeriver object.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ */
+ public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, $base_plugin_id) {
+ return new static(
+ $container->get('entity_type.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinitions($base_plugin_definition) {
+ $this->derivatives = [];
+ if ($this->entityTypeManager->hasDefinition('block_content_type')) {
+ $block_content_types = $this->entityTypeManager->getStorage('block_content_type')->loadMultiple();
+ foreach ($block_content_types as $id => $type) {
+ $this->derivatives[$id] = $base_plugin_definition;
+ $this->derivatives[$id]['admin_label'] = $type->label();
+ $this->derivatives[$id]['config_dependencies'][$type->getConfigDependencyKey()][] = $type->getConfigDependencyName();
+ }
+ }
+ return parent::getDerivativeDefinitions($base_plugin_definition);
+ }
+
+}
diff --git a/core/modules/layout_builder/tests/modules/no_transitions_css/css/css_fix.theme.css b/core/modules/layout_builder/tests/modules/no_transitions_css/css/css_fix.theme.css
new file mode 100644
index 0000000..ffe0614
--- /dev/null
+++ b/core/modules/layout_builder/tests/modules/no_transitions_css/css/css_fix.theme.css
@@ -0,0 +1,23 @@
+/**
+ * Remove all transitions for testing.
+ */
+* {
+ /* CSS transitions. */
+ -o-transition-property: none !important;
+ -moz-transition-property: none !important;
+ -ms-transition-property: none !important;
+ -webkit-transition-property: none !important;
+ transition-property: none !important;
+ /* CSS transforms. */
+ -o-transform: none !important;
+ -moz-transform: none !important;
+ -ms-transform: none !important;
+ -webkit-transform: none !important;
+ transform: none !important;
+ /* CSS animations. */
+ -webkit-animation: none !important;
+ -moz-animation: none !important;
+ -o-animation: none !important;
+ -ms-animation: none !important;
+ animation: none !important;
+}
diff --git a/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.info.yml b/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.info.yml
new file mode 100644
index 0000000..80082db
--- /dev/null
+++ b/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.info.yml
@@ -0,0 +1,6 @@
+name: 'CSS Test fix'
+type: module
+description: 'Provides CSS fixes for tests.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.libraries.yml b/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.libraries.yml
new file mode 100644
index 0000000..0fdaffd
--- /dev/null
+++ b/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.libraries.yml
@@ -0,0 +1,5 @@
+drupal.css_fix:
+ version: VERSION
+ css:
+ theme:
+ css/css_fix.theme.css: {}
diff --git a/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.module b/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.module
new file mode 100644
index 0000000..3375399
--- /dev/null
+++ b/core/modules/layout_builder/tests/modules/no_transitions_css/no_transitions_css.module
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * @file
+ * Module for attaching CSS during tests.
+ *
+ * CSS pointer-events properties cause testing errors.
+ */
+
+/**
+ * Implements hook_page_attachments().
+ */
+function settings_tray_test_css_page_attachments(array &$attachments) {
+ // Unconditionally attach an asset to the page.
+ $attachments['#attached']['library'][] = 'settings_tray_test_css/drupal.css_fix';
+}
diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
index d39c7e6..293f7b9 100644
--- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
+++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
@@ -122,6 +122,7 @@ class LayoutBuilderTest extends BrowserTestBase {
// Save the defaults.
$assert_session->linkExists('Save Layout');
$this->clickLink('Save Layout');
+ $assert_session->pageTextContains('The layout has been saved.');
$assert_session->addressEquals("$field_ui_prefix/display/default");
// The node uses the defaults, no overrides available.
diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockPrivateFilesTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockPrivateFilesTest.php
new file mode 100644
index 0000000..05892bd
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockPrivateFilesTest.php
@@ -0,0 +1,291 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\FunctionalJavascript;
+
+use Drupal\file\Entity\File;
+use Drupal\file\FileInterface;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\Tests\file\Functional\FileFieldCreationTrait;
+use Drupal\Tests\TestFileCreationTrait;
+
+/**
+ * Test access to private files in block fields on the Layout Builder.
+ *
+ * @group layout_builder
+ */
+class InlineBlockPrivateFilesTest extends InlineBlockTestBase {
+
+ use FileFieldCreationTrait;
+ use TestFileCreationTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = [
+ 'file',
+ ];
+
+ /**
+ * The file system service.
+ *
+ * @var \Drupal\Core\File\FileSystemInterface
+ */
+ protected $fileSystem;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ // Update the test node type to not create new revisions by default. This
+ // allows testing for cases when a new revision is made and when it isn't.
+ $node_type = NodeType::load('bundle_with_section_field');
+ $node_type->setNewRevision(FALSE);
+ $node_type->save();
+ $field_settings = [
+ 'file_extensions' => 'txt',
+ 'uri_scheme' => 'private',
+ ];
+ $this->createFileField('field_file', 'block_content', 'basic', $field_settings);
+ $this->fileSystem = $this->container->get('file_system');
+ }
+
+ /**
+ * Test access to private files added via inline blocks in the layout builder.
+ */
+ public function testPrivateFiles() {
+ $assert_session = $this->assertSession();
+ $this->drupalLogin($this->drupalCreateUser([
+ 'access contextual links',
+ 'configure any layout',
+ 'administer node display',
+ 'administer node fields',
+ ]));
+
+ // Enable layout builder and overrides.
+ $this->drupalPostForm(
+ static::FIELD_UI_PREFIX . '/display/default',
+ ['layout[enabled]' => TRUE, 'layout[allow_custom]' => TRUE],
+ 'Save'
+ );
+ $this->drupalLogout();
+
+ // Log in as user you can only configure layouts and access content.
+ $this->drupalLogin($this->drupalCreateUser([
+ 'access contextual links',
+ 'configure any layout',
+ 'access content',
+ ]));
+ $this->drupalGet('node/1/layout');
+ $file = $this->createPrivateFile('drupal.txt');
+
+ $file_real_path = $this->fileSystem->realpath($file->getFileUri());
+ $this->assertFileExists($file_real_path);
+ $this->addInlineFileBlockToLayout('The file', $file);
+ $this->assertSaveLayout();
+
+ $this->drupalGet('node/1');
+ $private_href1 = $this->assertFileAccessibleOnNode($file);
+
+ // Remove the inline block with the private file.
+ $this->drupalGet('node/1/layout');
+ $this->removeInlineBlockFromLayout();
+ $this->assertSaveLayout();
+
+ $this->drupalGet('node/1');
+ $assert_session->pageTextNotContains($file->label());
+ // Try to access file directly after it has been removed. Since a new
+ // revision was not created for the node the inline block is not in the
+ // layout of a previous revision of the node.
+ $this->drupalGet($private_href1);
+ $assert_session->pageTextContains('You are not authorized to access this page');
+ $assert_session->pageTextNotContains($this->getFileSecret($file));
+ $this->assertFileExists($file_real_path);
+
+ $file2 = $this->createPrivateFile('2ndFile.txt');
+
+ $this->drupalGet('node/1/layout');
+ $this->addInlineFileBlockToLayout('Number2', $file2);
+ $this->assertSaveLayout();
+
+ $this->drupalGet('node/1');
+ $private_href2 = $this->assertFileAccessibleOnNode($file2);
+
+ $this->createNewNodeRevision(1);
+
+ $file3 = $this->createPrivateFile('3rdFile.txt');
+ $this->drupalGet('node/1/layout');
+ $this->replaceFileInBlock($file3);
+ $this->assertSaveLayout();
+
+ $this->drupalGet('node/1');
+ $private_href3 = $this->assertFileAccessibleOnNode($file3);
+
+ // $file2 is on a previous revision of the block which is on a previous
+ // revision of the node. The user does not have access to view the previous
+ // revision of the node.
+ $this->drupalGet($private_href2);
+ $assert_session->pageTextContains('You are not authorized to access this page');
+
+ $node = Node::load(1);
+ $node->setUnpublished();
+ $node->save();
+ $this->drupalGet('node/1');
+ $assert_session->pageTextContains('You are not authorized to access this page');
+ $this->drupalGet($private_href3);
+ $assert_session->pageTextNotContains($this->getFileSecret($file3));
+ $assert_session->pageTextContains('You are not authorized to access this page');
+
+ $this->drupalGet('node/2/layout');
+ $file4 = $this->createPrivateFile('drupal.txt');
+ $this->addInlineFileBlockToLayout('The file', $file4);
+ $this->assertSaveLayout();
+
+ $this->drupalGet('node/2');
+ $private_href4 = $this->assertFileAccessibleOnNode($file4);
+
+ $this->createNewNodeRevision(2);
+
+ // Remove the inline block with the private file.
+ // The inline block will still be attached to the previous revision of the
+ // node.
+ $this->drupalGet('node/2/layout');
+ $this->removeInlineBlockFromLayout();
+ $this->assertSaveLayout();
+
+ // Ensure that since the user cannot view the previous revision of the node
+ // they can not view the file which is only used on that revision.
+ $this->drupalGet($private_href4);
+ $assert_session->pageTextContains('You are not authorized to access this page');
+ }
+
+ /**
+ * Replaces the file in the block with another one.
+ *
+ * @param \Drupal\file\FileInterface $file
+ * The file entity.
+ */
+ protected function replaceFileInBlock(FileInterface $file) {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+ $this->clickContextualLink(static::INLINE_BLOCK_LOCATOR, 'Configure');
+ $assert_session->assertWaitOnAjaxRequest();
+ $page->pressButton('Remove');
+ $assert_session->assertWaitOnAjaxRequest();
+ $this->attachFileToBlockForm($file);
+ $page->pressButton('Update');
+ $this->assertDialogClosedAndTextVisible($file->label(), static::INLINE_BLOCK_LOCATOR);
+ }
+
+ /**
+ * Adds an entity block with a file.
+ *
+ * @param string $title
+ * The title field value.
+ * @param \Drupal\file\Entity\File $file
+ * The file entity.
+ */
+ protected function addInlineFileBlockToLayout($title, File $file) {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+ $page->clickLink('Add Block');
+ $assert_session->assertWaitOnAjaxRequest();
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-categories details:contains(Create new block)'));
+ $this->clickLink('Basic block');
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->fieldValueEquals('Title', '');
+ $page->findField('Title')->setValue($title);
+ $this->attachFileToBlockForm($file);
+ $page->pressButton('Add Block');
+ $this->assertDialogClosedAndTextVisible($file->label(), static::INLINE_BLOCK_LOCATOR);
+ }
+
+ /**
+ * Creates a private file.
+ *
+ * @param string $file_name
+ * The file name.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface|\Drupal\file\Entity\File
+ * The file entity.
+ */
+ protected function createPrivateFile($file_name) {
+ // Create a new file entity.
+ $file = File::create([
+ 'uid' => 1,
+ 'filename' => $file_name,
+ 'uri' => "private://$file_name",
+ 'filemime' => 'text/plain',
+ 'status' => FILE_STATUS_PERMANENT,
+ ]);
+ file_put_contents($file->getFileUri(), $this->getFileSecret($file));
+ $file->save();
+ return $file;
+ }
+
+ /**
+ * Asserts a file is accessible on the page.
+ *
+ * @param \Drupal\file\FileInterface $file
+ * The file entity.
+ *
+ * @return string
+ * The file href.
+ */
+ protected function assertFileAccessibleOnNode(FileInterface $file) {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+ $assert_session->linkExists($file->label());
+ $private_href = $page->findLink($file->label())->getAttribute('href');
+ $page->clickLink($file->label());
+ $assert_session->pageTextContains($this->getFileSecret($file));
+
+ // Access file directly.
+ $this->drupalGet($private_href);
+ $assert_session->pageTextContains($this->getFileSecret($file));
+ return $private_href;
+ }
+
+ /**
+ * Gets the text secret for a file.
+ *
+ * @param \Drupal\file\FileInterface $file
+ * The file entity.
+ *
+ * @return string
+ * The text secret.
+ */
+ protected function getFileSecret(FileInterface $file) {
+ return "The secret in {$file->label()}";
+ }
+
+ /**
+ * Attaches a file to the block edit form.
+ *
+ * @param \Drupal\file\FileInterface $file
+ * The file to be attached.
+ */
+ protected function attachFileToBlockForm(FileInterface $file) {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+ $page->attachFileToField("files[settings_block_form_field_file_0]", $this->fileSystem->realpath($file->getFileUri()));
+ $assert_session->assertWaitOnAjaxRequest();
+ $this->assertNotEmpty($assert_session->waitForLink($file->label()));
+ }
+
+ /**
+ * Create a new revision of the node.
+ *
+ * @param int $node_id
+ * The node id.
+ */
+ protected function createNewNodeRevision($node_id) {
+ $node = Node::load($node_id);
+ $node->setTitle('Update node');
+ $node->setNewRevision();
+ $node->save();
+ }
+
+}
diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTest.php
new file mode 100644
index 0000000..9fdc8fd
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTest.php
@@ -0,0 +1,431 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\FunctionalJavascript;
+
+use Drupal\node\Entity\Node;
+
+/**
+ * Tests that the inline block feature works correctly.
+ *
+ * @group layout_builder
+ */
+class InlineBlockTest extends InlineBlockTestBase {
+
+ /**
+ * Tests adding and editing of inline blocks.
+ */
+ public function testInlineBlocks() {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ $this->drupalLogin($this->drupalCreateUser([
+ 'access contextual links',
+ 'configure any layout',
+ 'administer node display',
+ 'administer node fields',
+ ]));
+
+ // Enable layout builder.
+ $this->drupalPostForm(
+ static::FIELD_UI_PREFIX . '/display/default',
+ ['layout[enabled]' => TRUE],
+ 'Save'
+ );
+ $this->clickLink('Manage layout');
+ $assert_session->addressEquals(static::FIELD_UI_PREFIX . '/display-layout/default');
+ // Add a basic block with the body field set.
+ $this->addInlineBlockToLayout('Block title', 'The DEFAULT block body');
+ $this->assertSaveLayout();
+
+ $this->drupalGet('node/1');
+ $assert_session->pageTextContains('The DEFAULT block body');
+ $this->drupalGet('node/2');
+ $assert_session->pageTextContains('The DEFAULT block body');
+
+ // Enable overrides.
+ $this->drupalPostForm(static::FIELD_UI_PREFIX . '/display/default', ['layout[allow_custom]' => TRUE], 'Save');
+ $this->drupalGet('node/1/layout');
+
+ // Confirm the block can be edited.
+ $this->drupalGet('node/1/layout');
+ $this->configureInlineBlock('The DEFAULT block body', 'The NEW block body!');
+ $this->assertSaveLayout();
+ $this->drupalGet('node/1');
+ $assert_session->pageTextContains('The NEW block body');
+ $assert_session->pageTextNotContains('The DEFAULT block body');
+ $this->drupalGet('node/2');
+ // Node 2 should use default layout.
+ $assert_session->pageTextContains('The DEFAULT block body');
+ $assert_session->pageTextNotContains('The NEW block body');
+
+ // Add a basic block with the body field set.
+ $this->drupalGet('node/1/layout');
+ $this->addInlineBlockToLayout('2nd Block title', 'The 2nd block body');
+ $this->assertSaveLayout();
+ $this->drupalGet('node/1');
+ $assert_session->pageTextContains('The NEW block body!');
+ $assert_session->pageTextContains('The 2nd block body');
+ $this->drupalGet('node/2');
+ // Node 2 should use default layout.
+ $assert_session->pageTextContains('The DEFAULT block body');
+ $assert_session->pageTextNotContains('The NEW block body');
+ $assert_session->pageTextNotContains('The 2nd block body');
+
+ // Confirm the block can be edited.
+ $this->drupalGet('node/1/layout');
+ /* @var \Behat\Mink\Element\NodeElement $inline_block_2 */
+ $inline_block_2 = $page->findAll('css', static::INLINE_BLOCK_LOCATOR)[1];
+ $uuid = $inline_block_2->getAttribute('data-layout-block-uuid');
+ $block_css_locator = static::INLINE_BLOCK_LOCATOR . "[data-layout-block-uuid=\"$uuid\"]";
+ $this->configureInlineBlock('The 2nd block body', 'The 2nd NEW block body!', $block_css_locator);
+ $this->assertSaveLayout();
+ $this->drupalGet('node/1');
+ $assert_session->pageTextContains('The NEW block body!');
+ $assert_session->pageTextContains('The 2nd NEW block body!');
+ $this->drupalGet('node/2');
+ // Node 2 should use default layout.
+ $assert_session->pageTextContains('The DEFAULT block body');
+ $assert_session->pageTextNotContains('The NEW block body!');
+ $assert_session->pageTextNotContains('The 2nd NEW block body!');
+
+ // The default layout entity block should be changed.
+ $this->drupalGet(static::FIELD_UI_PREFIX . '/display-layout/default');
+ $assert_session->pageTextContains('The DEFAULT block body');
+ // Confirm default layout still only has 1 entity block.
+ $assert_session->elementsCount('css', static::INLINE_BLOCK_LOCATOR, 1);
+ }
+
+ /**
+ * Tests adding a new entity block and then not saving the layout.
+ *
+ * @dataProvider layoutNoSaveProvider
+ */
+ public function testNoLayoutSave($operation, $no_save_link_text, $confirm_button_text) {
+
+ $this->drupalLogin($this->drupalCreateUser([
+ 'access contextual links',
+ 'configure any layout',
+ 'administer node display',
+ ]));
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+ $this->assertEmpty($this->blockStorage->loadMultiple(), 'No entity blocks exist');
+ // Enable layout builder and overrides.
+ $this->drupalPostForm(
+ static::FIELD_UI_PREFIX . '/display/default',
+ ['layout[enabled]' => TRUE, 'layout[allow_custom]' => TRUE],
+ 'Save'
+ );
+
+ $this->drupalGet('node/1/layout');
+ $this->addInlineBlockToLayout('Block title', 'The block body');
+ $this->clickLink($no_save_link_text);
+ if ($confirm_button_text) {
+ $page->pressButton($confirm_button_text);
+ }
+ $this->drupalGet('node/1');
+ $this->assertEmpty($this->blockStorage->loadMultiple(), 'No entity blocks were created when layout is canceled.');
+ $assert_session->pageTextNotContains('The block body');
+
+ $this->drupalGet('node/1/layout');
+
+ $this->addInlineBlockToLayout('Block title', 'The block body');
+ $this->assertSaveLayout();
+ $this->drupalGet('node/1');
+ $assert_session->pageTextContains('The block body');
+ $blocks = $this->blockStorage->loadMultiple();
+ $this->assertEquals(count($blocks), 1);
+ /* @var \Drupal\Core\Entity\ContentEntityBase $block */
+ $block = array_pop($blocks);
+ $revision_id = $block->getRevisionId();
+
+ // Confirm the block can be edited.
+ $this->drupalGet('node/1/layout');
+ $this->configureInlineBlock('The block body', 'The block updated body');
+
+ $this->clickLink($no_save_link_text);
+ if ($confirm_button_text) {
+ $page->pressButton($confirm_button_text);
+ }
+ $this->drupalGet('node/1');
+
+ $blocks = $this->blockStorage->loadMultiple();
+ // When reverting or canceling the update block should not be on the page.
+ $assert_session->pageTextNotContains('The block updated body');
+ if ($operation === 'cancel') {
+ // When canceling the original block body should appear.
+ $assert_session->pageTextContains('The block body');
+
+ $this->assertEquals(count($blocks), 1);
+ $block = array_pop($blocks);
+ $this->assertEquals($block->getRevisionId(), $revision_id);
+ $this->assertEquals($block->get('body')->getValue()[0]['value'], 'The block body');
+ }
+ else {
+ // The block should not be visible.
+ // Blocks are currently only deleted when the parent entity is deleted.
+ $assert_session->pageTextNotContains('The block body');
+ }
+ }
+
+ /**
+ * Provides test data for ::testNoLayoutSave().
+ */
+ public function layoutNoSaveProvider() {
+ return [
+ 'cancel' => [
+ 'cancel',
+ 'Cancel Layout',
+ NULL,
+ ],
+ 'revert' => [
+ 'revert',
+ 'Revert to defaults',
+ 'Revert',
+ ],
+ ];
+ }
+
+ /**
+ * Tests entity blocks revisioning.
+ */
+ public function testInlineBlocksRevisioning() {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ $this->drupalLogin($this->drupalCreateUser([
+ 'access contextual links',
+ 'configure any layout',
+ 'administer node display',
+ 'administer node fields',
+ 'administer nodes',
+ 'bypass node access',
+ ]));
+ // Enable layout builder and overrides.
+ $this->drupalPostForm(
+ static::FIELD_UI_PREFIX . '/display/default',
+ ['layout[enabled]' => TRUE, 'layout[allow_custom]' => TRUE],
+ 'Save'
+ );
+ $this->drupalGet('node/1/layout');
+
+ // Add an inline block.
+ $this->addInlineBlockToLayout('Block title', 'The DEFAULT block body');
+ $this->assertSaveLayout();
+ $this->drupalGet('node/1');
+
+ $assert_session->pageTextContains('The DEFAULT block body');
+
+ /** @var \Drupal\node\NodeStorageInterface $node_storage */
+ $node_storage = $this->container->get('entity_type.manager')->getStorage('node');
+ $original_revision_id = $node_storage->getLatestRevisionId(1);
+
+ // Create a new revision.
+ $this->drupalGet('node/1/edit');
+ $page->findField('title[0][value]')->setValue('Node updated');
+ $page->pressButton('Save');
+
+ $this->drupalGet('node/1');
+ $assert_session->pageTextContains('The DEFAULT block body');
+
+ $assert_session->linkExists('Revisions');
+
+ // Update the block.
+ $this->drupalGet('node/1/layout');
+ $this->configureInlineBlock('The DEFAULT block body', 'The NEW block body');
+ $this->assertSaveLayout();
+ $this->drupalGet('node/1');
+ $assert_session->pageTextContains('The NEW block body');
+ $assert_session->pageTextNotContains('The DEFAULT block body');
+
+ $revision_url = "node/1/revisions/$original_revision_id";
+
+ // Ensure viewing the previous revision shows the previous block revision.
+ $this->drupalGet("$revision_url/view");
+ $assert_session->pageTextContains('The DEFAULT block body');
+ $assert_session->pageTextNotContains('The NEW block body');
+
+ // Revert to first revision.
+ $revision_url = "$revision_url/revert";
+ $this->drupalGet($revision_url);
+ $page->pressButton('Revert');
+
+ $this->drupalGet('node/1');
+ $assert_session->pageTextContains('The DEFAULT block body');
+ $assert_session->pageTextNotContains('The NEW block body');
+ }
+
+ /**
+ * Tests that entity blocks deleted correctly.
+ */
+ public function testDeletion() {
+ /** @var \Drupal\Core\Cron $cron */
+ $cron = \Drupal::service('cron');
+ /** @var \Drupal\layout_builder\InlineBlockUsage $usage */
+ $usage = \Drupal::service('inline_block.usage');
+ $this->drupalLogin($this->drupalCreateUser([
+ 'administer content types',
+ 'access contextual links',
+ 'configure any layout',
+ 'administer node display',
+ 'administer node fields',
+ 'administer nodes',
+ 'bypass node access',
+ ]));
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ // Enable layout builder.
+ $this->drupalPostForm(
+ static::FIELD_UI_PREFIX . '/display/default',
+ ['layout[enabled]' => TRUE],
+ 'Save'
+ );
+ // Add a block to default layout.
+ $this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
+ $this->clickLink('Manage layout');
+ $assert_session->addressEquals(static::FIELD_UI_PREFIX . '/display-layout/default');
+ $this->addInlineBlockToLayout('Block title', 'The DEFAULT block body');
+ $this->assertSaveLayout();
+
+ $this->assertCount(1, $this->blockStorage->loadMultiple());
+ $default_block_id = $this->getLatestBlockEntityId();
+
+ // Ensure the block shows up on node pages.
+ $this->drupalGet('node/1');
+ $assert_session->pageTextContains('The DEFAULT block body');
+ $this->drupalGet('node/2');
+ $assert_session->pageTextContains('The DEFAULT block body');
+
+ // Enable overrides.
+ $this->drupalPostForm(static::FIELD_UI_PREFIX . '/display/default', ['layout[allow_custom]' => TRUE], 'Save');
+
+ // Ensure we have 2 copies of the block in node overrides.
+ $this->drupalGet('node/1/layout');
+ $this->assertSaveLayout();
+ $node_1_block_id = $this->getLatestBlockEntityId();
+
+ $this->drupalGet('node/2/layout');
+ $this->assertSaveLayout();
+ $node_2_block_id = $this->getLatestBlockEntityId();
+ $this->assertCount(3, $this->blockStorage->loadMultiple());
+
+ $this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
+ $this->clickLink('Manage layout');
+ $assert_session->addressEquals(static::FIELD_UI_PREFIX . '/display-layout/default');
+
+ $this->assertNotEmpty($this->blockStorage->load($default_block_id));
+ $this->assertNotEmpty($usage->getUsage($default_block_id));
+ // Remove block from default.
+ $this->removeInlineBlockFromLayout();
+ $this->assertSaveLayout();
+ // Ensure the block in the default was deleted.
+ $this->blockStorage->resetCache([$default_block_id]);
+ $this->assertEmpty($this->blockStorage->load($default_block_id));
+ // Ensure other blocks still exist.
+ $this->assertCount(2, $this->blockStorage->loadMultiple());
+ $this->assertEmpty($usage->getUsage($default_block_id));
+
+ $this->drupalGet('node/1/layout');
+ $assert_session->pageTextContains('The DEFAULT block body');
+
+ $this->removeInlineBlockFromLayout();
+ $this->assertSaveLayout();
+ $cron->run();
+ // Ensure entity block is not deleted because it is needed in revision.
+ $this->assertNotEmpty($this->blockStorage->load($node_1_block_id));
+ $this->assertCount(2, $this->blockStorage->loadMultiple());
+
+ $this->assertNotEmpty($usage->getUsage($node_1_block_id));
+ // Ensure entity block is deleted when node is deleted.
+ $this->drupalGet('node/1/delete');
+ $page->pressButton('Delete');
+ $this->assertEmpty(Node::load(1));
+ $cron->run();
+ $this->assertEmpty($this->blockStorage->load($node_1_block_id));
+ $this->assertEmpty($usage->getUsage($node_1_block_id));
+ $this->assertCount(1, $this->blockStorage->loadMultiple());
+
+ // Add another block to the default.
+ $this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
+ $this->clickLink('Manage layout');
+ $assert_session->addressEquals(static::FIELD_UI_PREFIX . '/display-layout/default');
+ $this->addInlineBlockToLayout('Title 2', 'Body 2');
+ $this->assertSaveLayout();
+ $cron->run();
+ $default_block2_id = $this->getLatestBlockEntityId();
+ $this->assertCount(2, $this->blockStorage->loadMultiple());
+
+ // Delete the other node so bundle can be deleted.
+ $this->assertNotEmpty($usage->getUsage($node_2_block_id));
+ $this->drupalGet('node/2/delete');
+ $page->pressButton('Delete');
+ $this->assertEmpty(Node::load(2));
+ $cron->run();
+ // Ensure entity block was deleted.
+ $this->assertEmpty($this->blockStorage->load($node_2_block_id));
+ $this->assertEmpty($usage->getUsage($node_2_block_id));
+ $this->assertCount(1, $this->blockStorage->loadMultiple());
+
+ // Delete the bundle which has the default layout.
+ $this->assertNotEmpty($usage->getUsage($default_block2_id));
+ $this->drupalGet(static::FIELD_UI_PREFIX . '/delete');
+ $page->pressButton('Delete');
+ $cron->run();
+
+ // Ensure the entity block in default is deleted when bundle is deleted.
+ $this->assertEmpty($this->blockStorage->load($default_block2_id));
+ $this->assertEmpty($usage->getUsage($default_block2_id));
+ $this->assertCount(0, $this->blockStorage->loadMultiple());
+ }
+
+ /**
+ * Tests access to the block edit form of inline blocks.
+ *
+ * This module does not provide links to these forms but in case the paths are
+ * accessed directly they should accessible by users with the
+ * 'configure any layout' permission.
+ *
+ * @see layout_builder_block_content_access()
+ */
+ public function testAccess() {
+ $this->drupalLogin($this->drupalCreateUser([
+ 'access contextual links',
+ 'configure any layout',
+ 'administer node display',
+ 'administer node fields',
+ ]));
+ $assert_session = $this->assertSession();
+
+ // Enable layout builder and overrides.
+ $this->drupalPostForm(
+ static::FIELD_UI_PREFIX . '/display/default',
+ ['layout[enabled]' => TRUE, 'layout[allow_custom]' => TRUE],
+ 'Save'
+ );
+
+ // Ensure we have 2 copies of the block in node overrides.
+ $this->drupalGet('node/1/layout');
+ $this->addInlineBlockToLayout('Block title', 'Block body');
+ $this->assertSaveLayout();
+ $node_1_block_id = $this->getLatestBlockEntityId();
+
+ $this->drupalGet("block/$node_1_block_id");
+ $assert_session->pageTextNotContains('You are not authorized to access this page');
+
+ $this->drupalLogout();
+ $this->drupalLogin($this->drupalCreateUser([
+ 'administer nodes',
+ ]));
+
+ $this->drupalGet("block/$node_1_block_id");
+ $assert_session->pageTextContains('You are not authorized to access this page');
+
+ $this->drupalLogin($this->drupalCreateUser([
+ 'configure any layout',
+ ]));
+ $this->drupalGet("block/$node_1_block_id");
+ $assert_session->pageTextNotContains('You are not authorized to access this page');
+ }
+
+}
diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php
new file mode 100644
index 0000000..6c99c6c
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php
@@ -0,0 +1,222 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\FunctionalJavascript;
+
+use Drupal\block_content\Entity\BlockContentType;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
+
+/**
+ * Base class for testing inline blocks.
+ */
+abstract class InlineBlockTestBase extends WebDriverTestBase {
+
+ use ContextualLinkClickTrait;
+
+ /**
+ * Locator for inline blocks.
+ */
+ const INLINE_BLOCK_LOCATOR = '.block-inline-blockbasic';
+
+ /**
+ * Path prefix for the field UI for the test bundle.
+ */
+ const FIELD_UI_PREFIX = 'admin/structure/types/manage/bundle_with_section_field';
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = [
+ 'block_content',
+ 'layout_builder',
+ 'block',
+ 'node',
+ 'contextual',
+ // @todo Remove after https://www.drupal.org/project/drupal/issues/2901792.
+ 'no_transitions_css',
+ ];
+
+ /**
+ * The block storage.
+ *
+ * @var \Drupal\Core\Entity\EntityStorageInterface
+ */
+ protected $blockStorage;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+ // @todo The Layout Builder UI relies on local tasks; fix in
+ // https://www.drupal.org/project/drupal/issues/2917777.
+ $this->drupalPlaceBlock('local_tasks_block');
+
+ $this->createContentType(['type' => 'bundle_with_section_field', 'new_revision' => TRUE]);
+ $this->createNode([
+ 'type' => 'bundle_with_section_field',
+ 'title' => 'The node title',
+ 'body' => [
+ [
+ 'value' => 'The node body',
+ ],
+ ],
+ ]);
+ $this->createNode([
+ 'type' => 'bundle_with_section_field',
+ 'title' => 'The node2 title',
+ 'body' => [
+ [
+ 'value' => 'The node2 body',
+ ],
+ ],
+ ]);
+ $bundle = BlockContentType::create([
+ 'id' => 'basic',
+ 'label' => 'Basic block',
+ 'revision' => 1,
+ ]);
+ $bundle->save();
+ block_content_add_body_field($bundle->id());
+
+ $this->blockStorage = $this->container->get('entity_type.manager')->getStorage('block_content');
+ }
+
+ /**
+ * Saves a layout and asserts the message is correct.
+ */
+ protected function assertSaveLayout() {
+ $assert_session = $this->assertSession();
+ $assert_session->linkExists('Save Layout');
+ // Go to the Save Layout page. Currently there are random test failures if
+ // 'clickLink()' is used.
+ // @todo Convert tests that extend this class to NightWatch tests in
+ // https://www.drupal.org/node/2984161
+ $link = $this->getSession()->getPage()->findLink('Save Layout');
+ $this->drupalGet($link->getAttribute('href'));
+ $this->assertNotEmpty($assert_session->waitForElement('css', '.messages--status'));
+
+ if (stristr($this->getUrl(), 'admin/structure') === FALSE) {
+ $assert_session->pageTextContains('The layout override has been saved.');
+ }
+ else {
+ $assert_session->pageTextContains('The layout has been saved.');
+ }
+ }
+
+ /**
+ * Gets the latest block entity id.
+ */
+ protected function getLatestBlockEntityId() {
+ $block_ids = \Drupal::entityQuery('block_content')->sort('id', 'DESC')->range(0, 1)->execute();
+ $block_id = array_pop($block_ids);
+ $this->assertNotEmpty($this->blockStorage->load($block_id));
+ return $block_id;
+ }
+
+ /**
+ * Removes an entity block from the layout but does not save the layout.
+ */
+ protected function removeInlineBlockFromLayout() {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+ $block_text = $page->find('css', static::INLINE_BLOCK_LOCATOR)->getText();
+ $this->assertNotEmpty($block_text);
+ $assert_session->pageTextContains($block_text);
+ $this->clickContextualLink(static::INLINE_BLOCK_LOCATOR, 'Remove block');
+ $assert_session->waitForElement('css', "#drupal-off-canvas input[value='Remove']");
+ $assert_session->assertWaitOnAjaxRequest();
+ $page->find('css', '#drupal-off-canvas')->pressButton('Remove');
+ $this->waitForNoElement('#drupal-off-canvas');
+ $this->waitForNoElement(static::INLINE_BLOCK_LOCATOR);
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->pageTextNotContains($block_text);
+ }
+
+ /**
+ * Adds an entity block to the layout.
+ *
+ * @param string $title
+ * The title field value.
+ * @param string $body
+ * The body field value.
+ */
+ protected function addInlineBlockToLayout($title, $body) {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+ $page->clickLink('Add Block');
+ $assert_session->assertWaitOnAjaxRequest();
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-categories details:contains(Create new block)'));
+ $this->clickLink('Basic block');
+ $assert_session->assertWaitOnAjaxRequest();
+ $textarea = $assert_session->waitForElement('css', '[name="settings[block_form][body][0][value]"]');
+ $this->assertNotEmpty($textarea);
+ $assert_session->fieldValueEquals('Title', '');
+ $page->findField('Title')->setValue($title);
+ $textarea->setValue($body);
+ $page->pressButton('Add Block');
+ $this->assertDialogClosedAndTextVisible($body, static::INLINE_BLOCK_LOCATOR);
+ }
+
+ /**
+ * Configures an inline block in the Layout Builder.
+ *
+ * @param string $old_body
+ * The old body field value.
+ * @param string $new_body
+ * The new body field value.
+ * @param string $block_css_locator
+ * The CSS locator to use to select the contextual link.
+ */
+ protected function configureInlineBlock($old_body, $new_body, $block_css_locator = NULL) {
+ $block_css_locator = $block_css_locator ?: static::INLINE_BLOCK_LOCATOR;
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+ $this->clickContextualLink($block_css_locator, 'Configure');
+ $textarea = $assert_session->waitForElementVisible('css', '[name="settings[block_form][body][0][value]"]');
+ $this->assertNotEmpty($textarea);
+ $this->assertSame($old_body, $textarea->getValue());
+ $textarea->setValue($new_body);
+ $page->pressButton('Update');
+ $this->waitForNoElement('#drupal-off-canvas');
+ $assert_session->assertWaitOnAjaxRequest();
+ $this->assertDialogClosedAndTextVisible($new_body);
+ }
+
+ /**
+ * Waits for an element to be removed from the page.
+ *
+ * @param string $selector
+ * CSS selector.
+ * @param int $timeout
+ * (optional) Timeout in milliseconds, defaults to 10000.
+ *
+ * @todo Remove in https://www.drupal.org/node/2892440.
+ */
+ protected function waitForNoElement($selector, $timeout = 10000) {
+ $condition = "(typeof jQuery !== 'undefined' && jQuery('$selector').length === 0)";
+ $this->assertJsCondition($condition, $timeout);
+ }
+
+ /**
+ * Asserts that the dialog closes and the new text appears on the main canvas.
+ *
+ * @param string $text
+ * The text.
+ * @param string|null $css_locator
+ * The css locator to use inside the main canvas if any.
+ */
+ protected function assertDialogClosedAndTextVisible($text, $css_locator = NULL) {
+ $assert_session = $this->assertSession();
+ $this->waitForNoElement('#drupal-off-canvas');
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->elementNotExists('css', '#drupal-off-canvas');
+ if ($css_locator) {
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', ".dialog-off-canvas-main-canvas $css_locator:contains('$text')"));
+ }
+ else {
+ $this->assertNotEmpty($assert_session->waitForElementVisible('css', ".dialog-off-canvas-main-canvas:contains('$text')"));
+ }
+ }
+
+}
diff --git a/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php b/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php
index 6571a8f..3082084 100644
--- a/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php
+++ b/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php
@@ -2,12 +2,17 @@
namespace Drupal\Tests\layout_builder\Unit;
+use Drupal\block_content\Access\RefinableDependentAccessInterface;
+use Drupal\Component\Plugin\Context\ContextInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Plugin\Context\ContextHandlerInterface;
use Drupal\Core\Session\AccountInterface;
+use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed;
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
use Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray;
use Drupal\layout_builder\SectionComponent;
@@ -34,6 +39,16 @@ class BlockComponentRenderArrayTest extends UnitTestCase {
protected $blockManager;
/**
+ * Dataprovider for test functions that should test block types.
+ */
+ public function providerBlockTypes() {
+ return [
+ [TRUE],
+ [FALSE],
+ ];
+ }
+
+ /**
* {@inheritdoc}
*/
protected function setUp() {
@@ -44,14 +59,30 @@ class BlockComponentRenderArrayTest extends UnitTestCase {
$container = new ContainerBuilder();
$container->set('plugin.manager.block', $this->blockManager->reveal());
+ $container->set('context.handler', $this->prophesize(ContextHandlerInterface::class));
\Drupal::setContainer($container);
}
/**
* @covers ::onBuildRender
+ *
+ * @dataProvider providerBlockTypes
*/
- public function testOnBuildRender() {
- $block = $this->prophesize(BlockPluginInterface::class);
+ public function testOnBuildRender($refinable_dependent_access) {
+ $contexts = [];
+ if ($refinable_dependent_access) {
+ $block = $this->prophesize(TestBlockPluginWithRefinableDependentAccessInterface::class);
+ $layout_entity = $this->prophesize(EntityInterface::class);
+ $layout_entity = $layout_entity->reveal();
+ $context = $this->prophesize(ContextInterface::class);
+ $context->getContextValue()->willReturn($layout_entity);
+ $contexts['layout_builder.entity'] = $context->reveal();
+
+ $block->setAccessDependency($layout_entity)->shouldBeCalled();
+ }
+ else {
+ $block = $this->prophesize(BlockPluginInterface::class);
+ }
$access_result = AccessResult::allowed();
$block->access($this->account->reveal(), TRUE)->willReturn($access_result)->shouldBeCalled();
$block->getCacheContexts()->willReturn([]);
@@ -67,7 +98,6 @@ class BlockComponentRenderArrayTest extends UnitTestCase {
$this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal());
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
- $contexts = [];
$in_preview = FALSE;
$event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview);
@@ -100,9 +130,26 @@ class BlockComponentRenderArrayTest extends UnitTestCase {
/**
* @covers ::onBuildRender
+ *
+ * @dataProvider providerBlockTypes
*/
- public function testOnBuildRenderDenied() {
- $block = $this->prophesize(BlockPluginInterface::class);
+ public function testOnBuildRenderDenied($refinable_dependent_access) {
+ $contexts = [];
+ if ($refinable_dependent_access) {
+ $block = $this->prophesize(TestBlockPluginWithRefinableDependentAccessInterface::class);
+
+ $layout_entity = $this->prophesize(EntityInterface::class);
+ $layout_entity = $layout_entity->reveal();
+ $context = $this->prophesize(ContextInterface::class);
+ $context->getContextValue()->willReturn($layout_entity);
+ $contexts['layout_builder.entity'] = $context->reveal();
+
+ $block->setAccessDependency($layout_entity)->shouldBeCalled();
+ }
+ else {
+ $block = $this->prophesize(BlockPluginInterface::class);
+ }
+
$access_result = AccessResult::forbidden();
$block->access($this->account->reveal(), TRUE)->willReturn($access_result)->shouldBeCalled();
$block->getCacheContexts()->shouldNotBeCalled();
@@ -118,7 +165,6 @@ class BlockComponentRenderArrayTest extends UnitTestCase {
$this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal());
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
- $contexts = [];
$in_preview = FALSE;
$event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview);
@@ -142,9 +188,26 @@ class BlockComponentRenderArrayTest extends UnitTestCase {
/**
* @covers ::onBuildRender
+ *
+ * @dataProvider providerBlockTypes
*/
- public function testOnBuildRenderInPreview() {
- $block = $this->prophesize(BlockPluginInterface::class);
+ public function testOnBuildRenderInPreview($refinable_dependent_access) {
+ $contexts = [];
+ if ($refinable_dependent_access) {
+ $block = $this->prophesize(TestBlockPluginWithRefinableDependentAccessInterface::class);
+ $block->setAccessDependency(new LayoutPreviewAccessAllowed())->shouldBeCalled();
+
+ $layout_entity = $this->prophesize(EntityInterface::class);
+ $layout_entity = $layout_entity->reveal();
+ $layout_entity->in_preview = TRUE;
+ $context = $this->prophesize(ContextInterface::class);
+ $context->getContextValue()->willReturn($layout_entity);
+ $contexts['layout_builder.entity'] = $context->reveal();
+ }
+ else {
+ $block = $this->prophesize(BlockPluginInterface::class);
+ }
+
$block->access($this->account->reveal(), TRUE)->shouldNotBeCalled();
$block->getCacheContexts()->willReturn([]);
$block->getCacheTags()->willReturn(['test']);
@@ -159,7 +222,6 @@ class BlockComponentRenderArrayTest extends UnitTestCase {
$this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal());
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
- $contexts = [];
$in_preview = TRUE;
$event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview);
@@ -220,3 +282,10 @@ class BlockComponentRenderArrayTest extends UnitTestCase {
}
}
+
+/**
+ * Test interface for dependent access block plugins.
+ */
+interface TestBlockPluginWithRefinableDependentAccessInterface extends BlockPluginInterface, RefinableDependentAccessInterface {
+
+}