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. +}