diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml
index df2a2fd02d97be9c5ccd1bc8181964751a73f76a..f58db9ef919773091c6d3511c3c836f961a6fa0b 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 79d6eff8cd0229a9840706ba955425b2d2338e5d..959c642bbaf4b064598193e7a98f80c3eaa1d238 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 0000000000000000000000000000000000000000..11b3e4588c08eb90c8ff0a1cf768a2cba0fc668a
--- /dev/null
+++ b/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php
@@ -0,0 +1,363 @@
+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' => '
',
+ '#suffix' => '
',
+ ];
+
+ 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 0000000000000000000000000000000000000000..6a39f4a17c4c28bac7d2fbc9aab2d49d13e1b394
--- /dev/null
+++ b/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php
@@ -0,0 +1,169 @@
+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 0000000000000000000000000000000000000000..e7ae850d42bbf5f35b425a76888278ffd2dfd11f
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php
@@ -0,0 +1,121 @@
+ '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 0000000000000000000000000000000000000000..a118cf5bef8a99e351d4449826878a6f7e120c24
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php
@@ -0,0 +1,199 @@
+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 36039dff0d3bddb0a41f6569cde15ed978ec46e8..eb11a19f93fcaf69cc12ea275280bf6ec578aced 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.
+}