From b66af73c82ead96e1a18ae033b0408b606633856 Mon Sep 17 00:00:00 2001
From: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date: Fri, 5 Jan 2018 07:42:17 +1000
Subject: [PATCH] Issue #2918500 by tim.plunkett, EclipseGc, tedbow, larowlan,
 jibran, Wim Leers, phenaproxima, amateescu, borisson_, samuel.mortenson,
 gaurav.kapoor, KarlShea, hctom, mroycroft, neerajsingh, DamienMcKenna,
 dsnopek, Xano, TravisCarden, Tim Bozeman: Create a block which can render
 entity fields

---
 core/config/schema/core.entity.schema.yml     |  58 +--
 .../src/Controller/BlockLibraryController.php |   4 +
 .../src/Plugin/Block/FieldBlock.php           | 363 ++++++++++++++++++
 .../Plugin/Derivative/FieldBlockDeriver.php   | 169 ++++++++
 .../FunctionalJavascript/FieldBlockTest.php   | 121 ++++++
 .../tests/src/Kernel/FieldBlockTest.php       | 199 ++++++++++
 core/modules/system/system.post_update.php    |   7 +
 7 files changed, 898 insertions(+), 23 deletions(-)
 create mode 100644 core/modules/layout_builder/src/Plugin/Block/FieldBlock.php
 create mode 100644 core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php
 create mode 100644 core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php
 create mode 100644 core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php

diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml
index df2a2fd02d97..f58db9ef9197 100644
--- a/core/config/schema/core.entity.schema.yml
+++ b/core/config/schema/core.entity.schema.yml
@@ -55,29 +55,7 @@ core.entity_view_display.*.*.*:
       type: sequence
       label: 'Field formatters'
       sequence:
-        type: mapping
-        label: 'Field formatter'
-        mapping:
-          type:
-            type: string
-            label: 'Format type machine name'
-          weight:
-            type: integer
-            label: 'Weight'
-          region:
-            type: string
-            label: 'Region'
-          label:
-             type: string
-             label: 'Label setting machine name'
-          settings:
-            type: field.formatter.settings.[%parent.type]
-            label: 'Settings'
-          third_party_settings:
-             type: sequence
-             label: 'Third party settings'
-             sequence:
-               type: field.formatter.third_party.[%key]
+        type: field_formatter.entity_view_display
     hidden:
       type: sequence
       label: 'Field display setting'
@@ -85,6 +63,35 @@ core.entity_view_display.*.*.*:
         type: boolean
         label: 'Value'
 
+field_formatter:
+  type: mapping
+  label: 'Field formatter'
+  mapping:
+    type:
+      type: string
+      label: 'Format type machine name'
+    label:
+       type: string
+       label: 'Label setting machine name'
+    settings:
+      type: field.formatter.settings.[%parent.type]
+      label: 'Settings'
+    third_party_settings:
+       type: sequence
+       label: 'Third party settings'
+       sequence:
+         type: field.formatter.third_party.[%key]
+
+field_formatter.entity_view_display:
+  type: field_formatter
+  mapping:
+    weight:
+      type: integer
+      label: 'Weight'
+    region:
+      type: string
+      label: 'Region'
+
 # Overview configuration information for form mode displays.
 core.entity_form_display.*.*.*:
   type: config_entity
@@ -362,3 +369,8 @@ field.formatter.settings.entity_reference_label:
       type: boolean
       label: 'Link label to the referenced entity'
 
+block.settings.field_block:*:*:
+  type: block_settings
+  mapping:
+    formatter:
+      type: field_formatter
diff --git a/core/modules/block/src/Controller/BlockLibraryController.php b/core/modules/block/src/Controller/BlockLibraryController.php
index 79d6eff8cd02..959c642bbaf4 100644
--- a/core/modules/block/src/Controller/BlockLibraryController.php
+++ b/core/modules/block/src/Controller/BlockLibraryController.php
@@ -105,6 +105,10 @@ public function listBlocks(Request $request, $theme) {
     $definitions = $this->blockManager->getDefinitionsForContexts($this->contextRepository->getAvailableContexts());
     // Order by category, and then by admin label.
     $definitions = $this->blockManager->getSortedDefinitions($definitions);
+    // Filter out definitions that are not intended to be placed by the UI.
+    $definitions = array_filter($definitions, function (array $definition) {
+      return empty($definition['_block_ui_hidden']);
+    });
 
     $region = $request->query->get('region');
     $weight = $request->query->get('weight');
diff --git a/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php b/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php
new file mode 100644
index 000000000000..11b3e4588c08
--- /dev/null
+++ b/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php
@@ -0,0 +1,363 @@
+<?php
+
+namespace Drupal\layout_builder\Plugin\Block;
+
+use Drupal\Component\Plugin\Factory\DefaultFactory;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Block\BlockBase;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Entity\EntityDisplayBase;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FormatterInterface;
+use Drupal\Core\Field\FormatterPluginManager;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Plugin\ContextAwarePluginInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a block that renders a field from an entity.
+ *
+ * @Block(
+ *   id = "field_block",
+ *   deriver = "\Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver",
+ * )
+ */
+class FieldBlock extends BlockBase implements ContextAwarePluginInterface, ContainerFactoryPluginInterface {
+
+  /**
+   * The entity field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $entityFieldManager;
+
+  /**
+   * The formatter manager.
+   *
+   * @var \Drupal\Core\Field\FormatterPluginManager
+   */
+  protected $formatterManager;
+
+  /**
+   * The entity type ID.
+   *
+   * @var string
+   */
+  protected $entityTypeId;
+
+  /**
+   * The field name.
+   *
+   * @var string
+   */
+  protected $fieldName;
+
+  /**
+   * The field definition.
+   *
+   * @var \Drupal\Core\Field\FieldDefinitionInterface
+   */
+  protected $fieldDefinition;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * Constructs a new FieldBlock.
+   *
+   * @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\EntityFieldManagerInterface $entity_field_manager
+   *   The entity field manager.
+   * @param \Drupal\Core\Field\FormatterPluginManager $formatter_manager
+   *   The formatter manager.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityFieldManagerInterface $entity_field_manager, FormatterPluginManager $formatter_manager, ModuleHandlerInterface $module_handler) {
+    $this->entityFieldManager = $entity_field_manager;
+    $this->formatterManager = $formatter_manager;
+    $this->moduleHandler = $module_handler;
+
+    // Get the entity type and field name from the plugin ID.
+    list (, $entity_type_id, $field_name) = explode(static::DERIVATIVE_SEPARATOR, $plugin_id, 3);
+    $this->entityTypeId = $entity_type_id;
+    $this->fieldName = $field_name;
+
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity_field.manager'),
+      $container->get('plugin.manager.field.formatter'),
+      $container->get('module_handler')
+    );
+  }
+
+  /**
+   * Gets the entity that has the field.
+   *
+   * @return \Drupal\Core\Entity\FieldableEntityInterface
+   *   The entity.
+   */
+  protected function getEntity() {
+    return $this->getContextValue('entity');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    $display_settings = $this->getConfiguration()['formatter'];
+    $build = $this->getEntity()->get($this->fieldName)->view($display_settings);
+    CacheableMetadata::createFromObject($this)->applyTo($build);
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function blockAccess(AccountInterface $account) {
+    $entity = $this->getEntity();
+
+    // First consult the entity.
+    $access = $entity->access('view', $account, TRUE);
+    if (!$access->isAllowed()) {
+      return $access;
+    }
+
+    // Check that the entity in question has this field.
+    if (!$entity instanceof FieldableEntityInterface || !$entity->hasField($this->fieldName)) {
+      return $access->andIf(AccessResult::forbidden());
+    }
+
+    // Check field access.
+    $field = $entity->get($this->fieldName);
+    $access = $access->andIf($field->access('view', $account, TRUE));
+    if (!$access->isAllowed()) {
+      return $access;
+    }
+
+    // Check to see if the field has any values.
+    if ($field->isEmpty()) {
+      return $access->andIf(AccessResult::forbidden());
+    }
+    return $access;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'formatter' => [
+        'label' => 'above',
+        'type' => $this->pluginDefinition['default_formatter'],
+        'settings' => [],
+        'third_party_settings' => [],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function blockForm($form, FormStateInterface $form_state) {
+    $config = $this->getConfiguration();
+
+    $form['formatter'] = [
+      '#tree' => TRUE,
+      '#process' => [
+        [$this, 'formatterSettingsProcessCallback'],
+      ],
+    ];
+    $form['formatter']['label'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Label'),
+      // @todo This is directly copied from
+      //   \Drupal\field_ui\Form\EntityViewDisplayEditForm::getFieldLabelOptions(),
+      //   resolve this in https://www.drupal.org/project/drupal/issues/2933924.
+      '#options' => [
+        'above' => $this->t('Above'),
+        'inline' => $this->t('Inline'),
+        'hidden' => '- ' . $this->t('Hidden') . ' -',
+        'visually_hidden' => '- ' . $this->t('Visually Hidden') . ' -',
+      ],
+      '#default_value' => $config['formatter']['label'],
+    ];
+
+    $form['formatter']['type'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Formatter'),
+      '#options' => $this->getApplicablePluginOptions($this->getFieldDefinition()),
+      '#required' => TRUE,
+      '#default_value' => $config['formatter']['type'],
+      '#ajax' => [
+        'callback' => [static::class, 'formatterSettingsAjaxCallback'],
+        'wrapper' => 'formatter-settings-wrapper',
+      ],
+    ];
+
+    // Add the formatter settings to the form via AJAX.
+    $form['formatter']['settings_wrapper'] = [
+      '#prefix' => '<div id="formatter-settings-wrapper">',
+      '#suffix' => '</div>',
+    ];
+
+    return $form;
+  }
+
+  /**
+   * Render API callback: builds the formatter settings elements.
+   */
+  public function formatterSettingsProcessCallback(array &$element, FormStateInterface $form_state, array &$complete_form) {
+    if ($formatter = $this->getFormatter($element['#parents'], $form_state)) {
+      $element['settings_wrapper']['settings'] = $formatter->settingsForm($complete_form, $form_state);
+      $element['settings_wrapper']['settings']['#parents'] = array_merge($element['#parents'], ['settings']);
+      $element['settings_wrapper']['third_party_settings'] = $this->thirdPartySettingsForm($formatter, $this->getFieldDefinition(), $complete_form, $form_state);
+      $element['settings_wrapper']['third_party_settings']['#parents'] = array_merge($element['#parents'], ['third_party_settings']);
+
+      // Store the array parents for our element so that we can retrieve the
+      // formatter settings in our AJAX callback.
+      $form_state->set('field_block_array_parents', $element['#array_parents']);
+    }
+    return $element;
+  }
+
+  /**
+   * Adds the formatter third party settings forms.
+   *
+   * @param \Drupal\Core\Field\FormatterInterface $plugin
+   *   The formatter.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The field definition.
+   * @param array $form
+   *   The (entire) configuration form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   *
+   * @return array
+   *   The formatter third party settings form.
+   */
+  protected function thirdPartySettingsForm(FormatterInterface $plugin, FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
+    $settings_form = [];
+    // Invoke hook_field_formatter_third_party_settings_form(), keying resulting
+    // subforms by module name.
+    foreach ($this->moduleHandler->getImplementations('field_formatter_third_party_settings_form') as $module) {
+      $settings_form[$module] = $this->moduleHandler->invoke($module, 'field_formatter_third_party_settings_form', [
+        $plugin,
+        $field_definition,
+        EntityDisplayBase::CUSTOM_MODE,
+        $form,
+        $form_state,
+      ]);
+    }
+    return $settings_form;
+  }
+
+  /**
+   * Render API callback: gets the layout settings elements.
+   */
+  public static function formatterSettingsAjaxCallback(array $form, FormStateInterface $form_state) {
+    $formatter_array_parents = $form_state->get('field_block_array_parents');
+    return NestedArray::getValue($form, array_merge($formatter_array_parents, ['settings_wrapper']));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function blockSubmit($form, FormStateInterface $form_state) {
+    $this->configuration['formatter'] = $form_state->getValue('formatter');
+  }
+
+  /**
+   * Gets the field definition.
+   *
+   * @return \Drupal\Core\Field\FieldDefinitionInterface
+   *   The field definition.
+   */
+  protected function getFieldDefinition() {
+    if (empty($this->fieldDefinition)) {
+      $bundle = reset($this->getPluginDefinition()['bundles']);
+      $field_definitions = $this->entityFieldManager->getFieldDefinitions($this->entityTypeId, $bundle);
+      $this->fieldDefinition = $field_definitions[$this->fieldName];
+    }
+    return $this->fieldDefinition;
+  }
+
+  /**
+   * Returns an array of applicable formatter options for a field.
+   *
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The field definition.
+   *
+   * @return array
+   *   An array of applicable formatter options.
+   *
+   * @see \Drupal\field_ui\Form\EntityDisplayFormBase::getApplicablePluginOptions()
+   */
+  protected function getApplicablePluginOptions(FieldDefinitionInterface $field_definition) {
+    $options = $this->formatterManager->getOptions($field_definition->getType());
+    $applicable_options = [];
+    foreach ($options as $option => $label) {
+      $plugin_class = DefaultFactory::getPluginClass($option, $this->formatterManager->getDefinition($option));
+      if ($plugin_class::isApplicable($field_definition)) {
+        $applicable_options[$option] = $label;
+      }
+    }
+    return $applicable_options;
+  }
+
+  /**
+   * Gets the formatter object.
+   *
+   * @param array $parents
+   *   The #parents of the element representing the formatter.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return \Drupal\Core\Field\FormatterInterface
+   *   The formatter object.
+   */
+  protected function getFormatter(array $parents, FormStateInterface $form_state) {
+    // Use the processed values, if available.
+    $configuration = NestedArray::getValue($form_state->getValues(), $parents);
+    if (!$configuration) {
+      // Next check the raw user input.
+      $configuration = NestedArray::getValue($form_state->getUserInput(), $parents);
+      if (!$configuration) {
+        // If no user input exists, use the default values.
+        $configuration = $this->getConfiguration()['formatter'];
+      }
+    }
+
+    return $this->formatterManager->getInstance([
+      'configuration' => $configuration,
+      'field_definition' => $this->getFieldDefinition(),
+      'view_mode' => EntityDisplayBase::CUSTOM_MODE,
+      'prepare' => TRUE,
+    ]);
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php b/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php
new file mode 100644
index 000000000000..6a39f4a17c4c
--- /dev/null
+++ b/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace Drupal\layout_builder\Plugin\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityTypeRepositoryInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\Field\FormatterPluginManager;
+use Drupal\Core\Plugin\Context\ContextDefinition;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides entity field block definitions for every field.
+ *
+ * @internal
+ */
+class FieldBlockDeriver extends DeriverBase implements ContainerDeriverInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The entity type repository.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeRepositoryInterface
+   */
+  protected $entityTypeRepository;
+
+  /**
+   * The entity field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $entityFieldManager;
+
+  /**
+   * The field type manager.
+   *
+   * @var \Drupal\Core\Field\FieldTypePluginManagerInterface
+   */
+  protected $fieldTypeManager;
+
+  /**
+   * The formatter manager.
+   *
+   * @var \Drupal\Core\Field\FormatterPluginManager
+   */
+  protected $formatterManager;
+
+  /**
+   * Constructs new FieldBlockDeriver.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeRepositoryInterface $entity_type_repository
+   *   The entity type repository.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+   *   The entity field manager.
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
+   *   The field type manager.
+   * @param \Drupal\Core\Field\FormatterPluginManager $formatter_manager
+   *   The formatter manager.
+   */
+  public function __construct(EntityTypeRepositoryInterface $entity_type_repository, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, FormatterPluginManager $formatter_manager) {
+    $this->entityTypeRepository = $entity_type_repository;
+    $this->entityFieldManager = $entity_field_manager;
+    $this->fieldTypeManager = $field_type_manager;
+    $this->formatterManager = $formatter_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, $base_plugin_id) {
+    return new static(
+      $container->get('entity_type.repository'),
+      $container->get('entity_field.manager'),
+      $container->get('plugin.manager.field.field_type'),
+      $container->get('plugin.manager.field.formatter')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    $entity_type_labels = $this->entityTypeRepository->getEntityTypeLabels();
+    foreach ($this->entityFieldManager->getFieldMap() as $entity_type_id => $entity_field_map) {
+      foreach ($this->entityFieldManager->getFieldStorageDefinitions($entity_type_id) as $field_storage_definition) {
+        $derivative = $base_plugin_definition;
+        $field_name = $field_storage_definition->getName();
+
+        // The blocks are based on fields. However, we are looping through field
+        // storages for which no fields may exist. If that is the case, skip
+        // this field storage.
+        if (!isset($entity_field_map[$field_name])) {
+          continue;
+        }
+        $field_info = $entity_field_map[$field_name];
+
+        // Skip fields without any formatters.
+        $options = $this->formatterManager->getOptions($field_storage_definition->getType());
+        if (empty($options)) {
+          continue;
+        }
+
+        // Store the default formatter on the definition.
+        $derivative['default_formatter'] = '';
+        $field_type_definition = $this->fieldTypeManager->getDefinition($field_storage_definition->getType());
+        if (isset($field_type_definition['default_formatter'])) {
+          $derivative['default_formatter'] = $field_type_definition['default_formatter'];
+        }
+
+        // Get the admin label for both base and configurable fields.
+        if ($field_storage_definition->isBaseField()) {
+          $admin_label = $field_storage_definition->getLabel();
+        }
+        else {
+          // We take the field label used on the first bundle.
+          $first_bundle = reset($field_info['bundles']);
+          $bundle_field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $first_bundle);
+
+          // The field storage config may exist, but it's possible that no
+          // fields are actually using it. If that's the case, skip to the next
+          // field.
+          if (empty($bundle_field_definitions[$field_name])) {
+            continue;
+          }
+          $admin_label = $bundle_field_definitions[$field_name]->getLabel();
+        }
+
+        // Set plugin definition for derivative.
+        $derivative['category'] = $this->t('@entity', ['@entity' => $entity_type_labels[$entity_type_id]]);
+        $derivative['admin_label'] = $admin_label;
+        $bundles = array_keys($field_info['bundles']);
+
+        // For any field that is not display configurable, mark it as
+        // unavailable to place in the block UI.
+        $block_ui_hidden = TRUE;
+        foreach ($bundles as $bundle) {
+          $field_definition = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle)[$field_name];
+          if ($field_definition->isDisplayConfigurable('view')) {
+            $block_ui_hidden = FALSE;
+            break;
+          }
+        }
+        $derivative['_block_ui_hidden'] = $block_ui_hidden;
+        $derivative['bundles'] = $bundles;
+        $context_definition = new ContextDefinition('entity:' . $entity_type_id, $entity_type_labels[$entity_type_id], TRUE);
+        // Limit available blocks by bundles to which the field is attached.
+        // @todo To workaround https://www.drupal.org/node/2671964 this only
+        //   adds a bundle constraint if the entity type has bundles. When an
+        //   entity type has no bundles, the entity type ID itself is used.
+        if (count($bundles) > 1 || !isset($field_info['bundles'][$entity_type_id])) {
+          $context_definition->addConstraint('Bundle', $bundles);
+        }
+        $derivative['context'] = [
+          'entity' => $context_definition,
+        ];
+
+        $derivative_id = $entity_type_id . PluginBase::DERIVATIVE_SEPARATOR . $field_name;
+        $this->derivatives[$derivative_id] = $derivative;
+      }
+    }
+    return $this->derivatives;
+  }
+
+}
diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php
new file mode 100644
index 000000000000..e7ae850d42bb
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\FunctionalJavascript;
+
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\layout_builder\Plugin\Block\FieldBlock
+ *
+ * @group field
+ */
+class FieldBlockTest extends JavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['block', 'datetime', 'layout_builder', 'user'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => 'field_date',
+      'entity_type' => 'user',
+      'type' => 'datetime',
+    ]);
+    $field_storage->save();
+    $field = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => 'user',
+      'label' => 'Date field',
+    ]);
+    $field->save();
+
+    $user = $this->drupalCreateUser([
+      'administer blocks',
+      'access administration pages',
+    ]);
+    $user->field_date = '1978-11-19T05:00:00';
+    $user->save();
+    $this->drupalLogin($user);
+  }
+
+  /**
+   * Tests configuring a field block for a user field.
+   */
+  public function testFieldBlock() {
+    $page = $this->getSession()->getPage();
+    $assert_session = $this->assertSession();
+
+    // Assert that the field value is not displayed.
+    $this->drupalGet('admin');
+    $assert_session->pageTextNotContains('Sunday, November 19, 1978 - 16:00');
+
+    $this->drupalGet('admin/structure/block');
+    $this->clickLink('Place block');
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // Ensure that fields without any formatters are not available.
+    $assert_session->pageTextNotContains('Password');
+    // Ensure that non-display-configurable fields are not available.
+    $assert_session->pageTextNotContains('Initial email');
+
+    $assert_session->pageTextContains('Date field');
+    $block_url = 'admin/structure/block/add/field_block%3Auser%3Afield_date/classy';
+    $assert_session->linkByHrefExists($block_url);
+
+    $this->drupalGet($block_url);
+    $page->fillField('region', 'content');
+
+    // Assert the default formatter configuration.
+    $assert_session->fieldValueEquals('settings[formatter][type]', 'datetime_default');
+    $assert_session->fieldValueEquals('settings[formatter][settings][format_type]', 'medium');
+
+    // Change the formatter.
+    $page->selectFieldOption('settings[formatter][type]', 'datetime_time_ago');
+    $assert_session->assertWaitOnAjaxRequest();
+    // Changing the formatter removes the old settings and introduces new ones.
+    $assert_session->fieldNotExists('settings[formatter][settings][format_type]');
+    $assert_session->fieldExists('settings[formatter][settings][granularity]');
+    $page->pressButton('Save block');
+    $assert_session->pageTextContains('The block configuration has been saved.');
+
+    // Configure the block and change the formatter again.
+    $this->clickLink('Configure');
+    $page->selectFieldOption('settings[formatter][type]', 'datetime_default');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->fieldValueEquals('settings[formatter][settings][format_type]', 'medium');
+    $page->selectFieldOption('settings[formatter][settings][format_type]', 'long');
+
+    $page->pressButton('Save block');
+    $assert_session->pageTextContains('The block configuration has been saved.');
+
+    // Assert that the field value is updated.
+    $this->clickLink('Configure');
+    $assert_session->fieldValueEquals('settings[formatter][settings][format_type]', 'long');
+
+    // Assert that the field block is configured as expected.
+    $expected = [
+      'label' => 'above',
+      'type' => 'datetime_default',
+      'settings' => [
+        'format_type' => 'long',
+        'timezone_override' => '',
+      ],
+      'third_party_settings' => [],
+    ];
+    $config = $this->container->get('config.factory')->get('block.block.datefield');
+    $this->assertEquals($expected, $config->get('settings.formatter'));
+
+    // Assert that the block is displaying the user field.
+    $this->drupalGet('admin');
+    $assert_session->pageTextContains('Sunday, November 19, 1978 - 16:00');
+  }
+
+}
diff --git a/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php b/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php
new file mode 100644
index 000000000000..a118cf5bef8a
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php
@@ -0,0 +1,199 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\Kernel;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FormatterPluginManager;
+use Drupal\Core\Plugin\Context\ContextDefinition;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\layout_builder\Plugin\Block\FieldBlock;
+use Prophecy\Prophecy\ProphecyInterface;
+
+/**
+ * @coversDefaultClass \Drupal\layout_builder\Plugin\Block\FieldBlock
+ * @group Field
+ */
+class FieldBlockTest extends EntityKernelTestBase {
+
+  /**
+   * Tests entity access.
+   *
+   * @covers ::blockAccess
+   * @dataProvider providerTestBlockAccessNotAllowed
+   */
+  public function testBlockAccessEntityNotAllowed($expected, $entity_access) {
+    $entity = $this->prophesize(FieldableEntityInterface::class);
+    $block = $this->getTestBlock($entity);
+
+    $account = $this->prophesize(AccountInterface::class);
+    $entity->access('view', $account->reveal(), TRUE)->willReturn($entity_access);
+    $entity->hasField()->shouldNotBeCalled();
+
+    $access = $block->access($account->reveal(), TRUE);
+    $this->assertSame($expected, $access->isAllowed());
+  }
+
+  /**
+   * Provides test data for ::testBlockAccessEntityNotAllowed().
+   */
+  public function providerTestBlockAccessNotAllowed() {
+    $data = [];
+    $data['entity_forbidden'] = [
+      FALSE,
+      AccessResult::forbidden(),
+    ];
+    $data['entity_neutral'] = [
+      FALSE,
+      AccessResult::neutral(),
+    ];
+    return $data;
+  }
+
+  /**
+   * Tests unfieldable entity.
+   *
+   * @covers ::blockAccess
+   */
+  public function testBlockAccessEntityAllowedNotFieldable() {
+    $entity = $this->prophesize(EntityInterface::class);
+    $block = $this->getTestBlock($entity);
+
+    $account = $this->prophesize(AccountInterface::class);
+    $entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed());
+
+    $access = $block->access($account->reveal(), TRUE);
+    $this->assertSame(FALSE, $access->isAllowed());
+  }
+
+  /**
+   * Tests fieldable entity without a particular field.
+   *
+   * @covers ::blockAccess
+   */
+  public function testBlockAccessEntityAllowedNoField() {
+    $entity = $this->prophesize(FieldableEntityInterface::class);
+    $block = $this->getTestBlock($entity);
+
+    $account = $this->prophesize(AccountInterface::class);
+    $entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed());
+    $entity->hasField('the_field_name')->willReturn(FALSE);
+    $entity->get('the_field_name')->shouldNotBeCalled();
+
+    $access = $block->access($account->reveal(), TRUE);
+    $this->assertSame(FALSE, $access->isAllowed());
+  }
+
+  /**
+   * Tests field access.
+   *
+   * @covers ::blockAccess
+   * @dataProvider providerTestBlockAccessNotAllowed
+   */
+  public function testBlockAccessEntityAllowedFieldNotAllowed($expected, $field_access) {
+    $entity = $this->prophesize(FieldableEntityInterface::class);
+    $block = $this->getTestBlock($entity);
+
+    $account = $this->prophesize(AccountInterface::class);
+    $entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed());
+    $entity->hasField('the_field_name')->willReturn(TRUE);
+    $field = $this->prophesize(FieldItemListInterface::class);
+    $entity->get('the_field_name')->willReturn($field->reveal());
+
+    $field->access('view', $account->reveal(), TRUE)->willReturn($field_access);
+    $field->isEmpty()->shouldNotBeCalled();
+
+    $access = $block->access($account->reveal(), TRUE);
+    $this->assertSame($expected, $access->isAllowed());
+  }
+
+  /**
+   * Tests populated vs empty build.
+   *
+   * @covers ::blockAccess
+   * @covers ::build
+   * @dataProvider providerTestBlockAccessEntityAllowedFieldHasValue
+   */
+  public function testBlockAccessEntityAllowedFieldHasValue($expected, $is_empty) {
+    $entity = $this->prophesize(FieldableEntityInterface::class);
+    $block = $this->getTestBlock($entity);
+
+    $account = $this->prophesize(AccountInterface::class);
+    $entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed());
+    $entity->hasField('the_field_name')->willReturn(TRUE);
+    $field = $this->prophesize(FieldItemListInterface::class);
+    $entity->get('the_field_name')->willReturn($field->reveal());
+
+    $field->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed());
+    $field->isEmpty()->willReturn($is_empty)->shouldBeCalled();
+
+    $access = $block->access($account->reveal(), TRUE);
+    $this->assertSame($expected, $access->isAllowed());
+  }
+
+  /**
+   * Provides test data for ::testBlockAccessEntityAllowedFieldHasValue().
+   */
+  public function providerTestBlockAccessEntityAllowedFieldHasValue() {
+    $data = [];
+    $data['empty'] = [
+      FALSE,
+      TRUE,
+    ];
+    $data['populated'] = [
+      TRUE,
+      FALSE,
+    ];
+    return $data;
+  }
+
+  /**
+   * Instantiates a block for testing.
+   *
+   * @param \Prophecy\Prophecy\ProphecyInterface $entity_prophecy
+   *   An entity prophecy for use as an entity context value.
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param array $plugin_definition
+   *   The plugin implementation definition.
+   *
+   * @return \Drupal\layout_builder\Plugin\Block\FieldBlock
+   *   The block to test.
+   */
+  protected function getTestBlock(ProphecyInterface $entity_prophecy, array $configuration = [], array $plugin_definition = []) {
+    $entity_prophecy->getCacheContexts()->willReturn([]);
+    $entity_prophecy->getCacheTags()->willReturn([]);
+    $entity_prophecy->getCacheMaxAge()->willReturn(0);
+
+    $plugin_definition += [
+      'provider' => 'test',
+      'default_formatter' => '',
+      'category' => 'Test',
+      'admin_label' => 'Test Block',
+      'bundles' => ['entity_test'],
+      'context' => [
+        'entity' => new ContextDefinition('entity:entity_test', 'Test', TRUE),
+      ],
+    ];
+    $entity_field_manager = $this->prophesize(EntityFieldManagerInterface::class);
+    $formatter_manager = $this->prophesize(FormatterPluginManager::class);
+    $module_handler = $this->prophesize(ModuleHandlerInterface::class);
+
+    $block = new FieldBlock(
+      $configuration,
+      'field_block:entity_test:the_field_name',
+      $plugin_definition,
+      $entity_field_manager->reveal(),
+      $formatter_manager->reveal(),
+      $module_handler->reveal()
+    );
+    $block->setContextValue('entity', $entity_prophecy->reveal());
+    return $block;
+  }
+
+}
diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php
index 36039dff0d3b..eb11a19f93fc 100644
--- a/core/modules/system/system.post_update.php
+++ b/core/modules/system/system.post_update.php
@@ -81,3 +81,10 @@ function system_post_update_classy_message_library() {
 function system_post_update_field_type_plugins() {
   // Empty post-update hook.
 }
+
+/**
+ * Clear caches due to schema changes in core.entity.schema.yml.
+ */
+function system_post_update_field_formatter_entity_schema() {
+  // Empty post-update hook.
+}
-- 
GitLab