From e14a020de78e00403203b4591b0f6eaf41dfe143 Mon Sep 17 00:00:00 2001 From: effulgentsia <alex.bronstein@acquia.com> Date: Wed, 25 Jan 2017 17:55:44 -0800 Subject: [PATCH] Issue #2796173 by tim.plunkett, samuel.mortenson, xjm, jibran, effulgentsia, dawehner, swentel, tedbow, phenaproxima, larowlan, yoroy, dsnopek, tacituseu, japerry, webchick, amateescu, aspilicious: Add experimental Field Layout module to allow entity view/form modes to switch between layouts --- core/composer.json | 1 + .../config/schema/field_layout.schema.yml | 16 + .../field_layout/field_layout.info.yml | 8 + .../modules/field_layout/field_layout.install | 42 +++ .../field_layout/field_layout.layouts.yml | 22 ++ .../field_layout/field_layout.libraries.yml | 5 + core/modules/field_layout/field_layout.module | 69 ++++ .../layouts/onecol/layout--onecol.html.twig | 24 ++ .../layouts/twocol/layout--twocol.html.twig | 28 ++ .../layouts/twocol/twocol.layout.css | 14 + .../EntityDisplayWithLayoutInterface.php | 78 +++++ .../Entity/FieldLayoutEntityDisplayTrait.php | 164 ++++++++++ .../Entity/FieldLayoutEntityFormDisplay.php | 24 ++ .../Entity/FieldLayoutEntityViewDisplay.php | 24 ++ .../field_layout/src/FieldLayoutBuilder.php | 153 +++++++++ .../FieldLayoutEntityDisplayFormTrait.php | 177 ++++++++++ .../FieldLayoutEntityFormDisplayEditForm.php | 44 +++ .../FieldLayoutEntityViewDisplayEditForm.php | 44 +++ .../schema/field_layout_test.schema.yml | 7 + .../field_layout_test.info.yml | 8 + .../field_layout_test.routing.yml | 7 + .../Plugin/Layout/TestLayoutContentFooter.php | 27 ++ .../Plugin/Layout/TestLayoutMainFooter.php | 50 +++ .../tests/src/Functional/FieldLayoutTest.php | 76 +++++ .../FunctionalJavascript/FieldLayoutTest.php | 266 +++++++++++++++ .../Kernel/FieldLayoutEntityDisplayTest.php | 182 +++++++++++ .../tests/src/Unit/FieldLayoutBuilderTest.php | 308 ++++++++++++++++++ 27 files changed, 1868 insertions(+) create mode 100644 core/modules/field_layout/config/schema/field_layout.schema.yml create mode 100644 core/modules/field_layout/field_layout.info.yml create mode 100644 core/modules/field_layout/field_layout.install create mode 100644 core/modules/field_layout/field_layout.layouts.yml create mode 100644 core/modules/field_layout/field_layout.libraries.yml create mode 100644 core/modules/field_layout/field_layout.module create mode 100644 core/modules/field_layout/layouts/onecol/layout--onecol.html.twig create mode 100644 core/modules/field_layout/layouts/twocol/layout--twocol.html.twig create mode 100644 core/modules/field_layout/layouts/twocol/twocol.layout.css create mode 100644 core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php create mode 100644 core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php create mode 100644 core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php create mode 100644 core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php create mode 100644 core/modules/field_layout/src/FieldLayoutBuilder.php create mode 100644 core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php create mode 100644 core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php create mode 100644 core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php create mode 100644 core/modules/field_layout/tests/modules/field_layout_test/config/schema/field_layout_test.schema.yml create mode 100644 core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml create mode 100644 core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml create mode 100644 core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutContentFooter.php create mode 100644 core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutMainFooter.php create mode 100644 core/modules/field_layout/tests/src/Functional/FieldLayoutTest.php create mode 100644 core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php create mode 100644 core/modules/field_layout/tests/src/Kernel/FieldLayoutEntityDisplayTest.php create mode 100644 core/modules/field_layout/tests/src/Unit/FieldLayoutBuilderTest.php diff --git a/core/composer.json b/core/composer.json index 71f07d029fdc..4b5de2ec2e99 100644 --- a/core/composer.json +++ b/core/composer.json @@ -95,6 +95,7 @@ "drupal/editor": "self.version", "drupal/entity_reference": "self.version", "drupal/field": "self.version", + "drupal/field_layout": "self.version", "drupal/field_ui": "self.version", "drupal/file": "self.version", "drupal/filter": "self.version", diff --git a/core/modules/field_layout/config/schema/field_layout.schema.yml b/core/modules/field_layout/config/schema/field_layout.schema.yml new file mode 100644 index 000000000000..185fb4ef7a64 --- /dev/null +++ b/core/modules/field_layout/config/schema/field_layout.schema.yml @@ -0,0 +1,16 @@ +core.entity_view_display.*.*.*.third_party.field_layout: + type: field_layout.third_party_settings + +core.entity_form_display.*.*.*.third_party.field_layout: + type: field_layout.third_party_settings + +field_layout.third_party_settings: + type: mapping + label: 'Per-view-mode field layout settings' + mapping: + id: + type: string + label: 'Layout ID' + settings: + type: layout_plugin.settings.[%parent.id] + label: 'Layout settings' diff --git a/core/modules/field_layout/field_layout.info.yml b/core/modules/field_layout/field_layout.info.yml new file mode 100644 index 000000000000..237f18dcd8d4 --- /dev/null +++ b/core/modules/field_layout/field_layout.info.yml @@ -0,0 +1,8 @@ +name: 'Field Layout' +type: module +description: 'Adds layout capabilities to the Field UI.' +package: Core (Experimental) +version: VERSION +core: 8.x +dependencies: + - layout_discovery diff --git a/core/modules/field_layout/field_layout.install b/core/modules/field_layout/field_layout.install new file mode 100644 index 000000000000..5956bf109541 --- /dev/null +++ b/core/modules/field_layout/field_layout.install @@ -0,0 +1,42 @@ +<?php + +/** + * @file + * Contains install and update functions for Field Layout. + */ + +use Drupal\Core\Cache\Cache; +use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface; + +/** + * Implements hook_install(). + */ +function field_layout_install() { + // Ensure each entity display has a layout. + $entity_save = function (EntityDisplayWithLayoutInterface $entity) { + $entity->ensureLayout()->save(); + }; + array_map($entity_save, EntityViewDisplay::loadMultiple()); + array_map($entity_save, EntityFormDisplay::loadMultiple()); + + // Invalidate the render cache since all content will now have a layout. + Cache::invalidateTags(['rendered']); +} + +/** + * Implements hook_uninstall(). + */ +function field_layout_uninstall() { + // Reset each entity display to use the one-column layout to best approximate + // the absence of layouts. + $entity_save = function (EntityDisplayWithLayoutInterface $entity) { + $entity->setLayoutId('layout_onecol')->save(); + }; + array_map($entity_save, EntityViewDisplay::loadMultiple()); + array_map($entity_save, EntityFormDisplay::loadMultiple()); + + // Invalidate the render cache since all content will no longer have a layout. + Cache::invalidateTags(['rendered']); +} diff --git a/core/modules/field_layout/field_layout.layouts.yml b/core/modules/field_layout/field_layout.layouts.yml new file mode 100644 index 000000000000..7f51a69f2a2d --- /dev/null +++ b/core/modules/field_layout/field_layout.layouts.yml @@ -0,0 +1,22 @@ +# @todo Move to layout_discovery in https://www.drupal.org/node/2840832. +layout_onecol: + label: 'One column' + path: layouts/onecol + template: layout--onecol + category: 'Columns: 1' + default_region: content + regions: + content: + label: Content +layout_twocol: + label: 'Two column' + path: layouts/twocol + template: layout--twocol + library: field_layout/drupal.layout.twocol + category: 'Columns: 2' + default_region: left + regions: + left: + label: Left + right: + label: Right diff --git a/core/modules/field_layout/field_layout.libraries.yml b/core/modules/field_layout/field_layout.libraries.yml new file mode 100644 index 000000000000..aafe7ee1a1a0 --- /dev/null +++ b/core/modules/field_layout/field_layout.libraries.yml @@ -0,0 +1,5 @@ +drupal.layout.twocol: + version: VERSION + css: + layout: + layouts/twocol/twocol.layout.css: {} diff --git a/core/modules/field_layout/field_layout.module b/core/modules/field_layout/field_layout.module new file mode 100644 index 000000000000..5a5fa11a49bd --- /dev/null +++ b/core/modules/field_layout/field_layout.module @@ -0,0 +1,69 @@ +<?php + +/** + * @file + * Provides hook implementations for Field Layout. + */ + +use Drupal\Core\Entity\ContentEntityFormInterface; +use Drupal\Core\Entity\Display\EntityViewDisplayInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface; +use Drupal\field_layout\Entity\FieldLayoutEntityFormDisplay; +use Drupal\field_layout\Entity\FieldLayoutEntityViewDisplay; +use Drupal\field_layout\FieldLayoutBuilder; +use Drupal\field_layout\Form\FieldLayoutEntityFormDisplayEditForm; +use Drupal\field_layout\Form\FieldLayoutEntityViewDisplayEditForm; + +/** + * Implements hook_help(). + */ +function field_layout_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + case 'help.page.field_layout': + $output = '<h3>' . t('About') . '</h3>'; + $output .= '<p>' . t('The Field Layout module allows you to arrange fields into regions on forms and displays of entities such as nodes and users.') . '</p>'; + $output .= '<p>' . t('For more information, see the <a href=":field-layout-documentation">online documentation for the Field Layout module</a>.', [':field-layout-documentation' => 'https://www.drupal.org/documentation/modules/field_layout']) . '</p>'; + return $output; + } +} + +/** + * Implements hook_entity_type_alter(). + */ +function field_layout_entity_type_alter(array &$entity_types) { + /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */ + $entity_types['entity_view_display']->setClass(FieldLayoutEntityViewDisplay::class); + $entity_types['entity_form_display']->setClass(FieldLayoutEntityFormDisplay::class); + + // The form classes are only needed when Field UI is installed. + if (\Drupal::moduleHandler()->moduleExists('field_ui')) { + $entity_types['entity_view_display']->setFormClass('edit', FieldLayoutEntityViewDisplayEditForm::class); + $entity_types['entity_form_display']->setFormClass('edit', FieldLayoutEntityFormDisplayEditForm::class); + } +} + +/** + * Implements hook_entity_view_alter(). + */ +function field_layout_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) { + if ($display instanceof EntityDisplayWithLayoutInterface) { + \Drupal::classResolver()->getInstanceFromDefinition(FieldLayoutBuilder::class) + ->buildView($build, $display); + } +} + +/** + * Implements hook_form_alter(). + */ +function field_layout_form_alter(&$form, FormStateInterface $form_state, $form_id) { + $form_object = $form_state->getFormObject(); + if ($form_object instanceof ContentEntityFormInterface && $display = $form_object->getFormDisplay($form_state)) { + if ($display instanceof EntityDisplayWithLayoutInterface) { + \Drupal::classResolver()->getInstanceFromDefinition(FieldLayoutBuilder::class) + ->buildForm($form, $display); + } + } +} diff --git a/core/modules/field_layout/layouts/onecol/layout--onecol.html.twig b/core/modules/field_layout/layouts/onecol/layout--onecol.html.twig new file mode 100644 index 000000000000..69fed0308d65 --- /dev/null +++ b/core/modules/field_layout/layouts/onecol/layout--onecol.html.twig @@ -0,0 +1,24 @@ +{# +/** + * @file + * Default theme implementation to display a one-column layout. + * + * Available variables: + * - content: The content for this layout. + * - attributes: HTML attributes for the layout <div>. + * + * @ingroup themeable + */ +#} +{% +set classes = [ +'layout--onecol', +] +%} +{% if content %} +<div{{ attributes.addClass(classes) }}> + <div class="layout-region layout-region--content"> + {{ content }} + </div> +</div> +{% endif %} diff --git a/core/modules/field_layout/layouts/twocol/layout--twocol.html.twig b/core/modules/field_layout/layouts/twocol/layout--twocol.html.twig new file mode 100644 index 000000000000..8e54d307a2a3 --- /dev/null +++ b/core/modules/field_layout/layouts/twocol/layout--twocol.html.twig @@ -0,0 +1,28 @@ +{# +/** + * @file + * Default theme implementation to display a two-column layout. + * + * Available variables: + * - content: The content for this layout. + * - attributes: HTML attributes for the layout <div>. + * + * @ingroup themeable + */ +#} +{% +set classes = [ +'layout--twocol', +] +%} +{% if content %} + <div{{ attributes.addClass(classes) }}> + <div class="layout-region layout-region--left"> + {{ content.left }} + </div> + + <div class="layout-region layout-region--right"> + {{ content.right }} + </div> + </div> +{% endif %} diff --git a/core/modules/field_layout/layouts/twocol/twocol.layout.css b/core/modules/field_layout/layouts/twocol/twocol.layout.css new file mode 100644 index 000000000000..78a447dc9df1 --- /dev/null +++ b/core/modules/field_layout/layouts/twocol/twocol.layout.css @@ -0,0 +1,14 @@ +.layout--twocol { + display: flex; + flex-wrap: wrap; + justify-content: space-between; +} +.layout--twocol > .layout-region { + flex: 0 1 50%; + max-width: 50%; +} + +.layout--twocol > .layout-region--left { + max-width: calc(50% - 10px); + margin-right: 10px; +} diff --git a/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php b/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php new file mode 100644 index 000000000000..3bee65ec220f --- /dev/null +++ b/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php @@ -0,0 +1,78 @@ +<?php + +namespace Drupal\field_layout\Display; + +use Drupal\Core\Entity\Display\EntityDisplayInterface; +use Drupal\Core\Layout\LayoutInterface; + +/** + * Provides a common interface for entity displays that have layout. + */ +interface EntityDisplayWithLayoutInterface extends EntityDisplayInterface { + + /** + * Gets the default region. + * + * @return string + * The default region for this display. + */ + public function getDefaultRegion(); + + /** + * Gets the layout plugin ID for this display. + * + * @return string + * The layout plugin ID. + */ + public function getLayoutId(); + + /** + * Gets the layout plugin settings for this display. + * + * @return mixed[] + * The layout plugin settings. + */ + public function getLayoutSettings(); + + /** + * Sets the layout plugin ID for this display. + * + * @param string|null $layout_id + * Either a valid layout plugin ID, or NULL to remove the layout setting. + * @param array $layout_settings + * (optional) An array of settings for this layout. + * + * @return $this + */ + public function setLayoutId($layout_id, array $layout_settings = []); + + /** + * Sets the layout plugin for this display. + * + * @param \Drupal\Core\Layout\LayoutInterface $layout + * A layout plugin. + * + * @return $this + */ + public function setLayout(LayoutInterface $layout); + + /** + * Gets the layout plugin for this display. + * + * @return \Drupal\Core\Layout\LayoutInterface + * The layout plugin. + */ + public function getLayout(); + + /** + * Ensures this entity has a layout. + * + * @param string $default_layout_id + * (optional) The layout ID to use as a default. Defaults to + * 'layout_onecol'. + * + * @return $this + */ + public function ensureLayout($default_layout_id = 'layout_onecol'); + +} diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php new file mode 100644 index 000000000000..49da6a97998e --- /dev/null +++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php @@ -0,0 +1,164 @@ +<?php + +namespace Drupal\field_layout\Entity; + +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Layout\LayoutInterface; + +/** + * Provides shared code for entity displays. + * + * Both EntityViewDisplay and EntityFormDisplay must maintain their parent + * hierarchy, while being identically enhanced by Field Layout. This trait + * contains the code they both share. + */ +trait FieldLayoutEntityDisplayTrait { + + /** + * Gets a layout definition. + * + * @param string $layout_id + * The layout ID. + * + * @return \Drupal\Core\Layout\LayoutDefinition + * The layout definition. + */ + protected function getLayoutDefinition($layout_id) { + return \Drupal::service('plugin.manager.core.layout')->getDefinition($layout_id); + } + + /** + * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::getLayoutId(). + */ + public function getLayoutId() { + return $this->getThirdPartySetting('field_layout', 'id'); + } + + /** + * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::getLayoutSettings(). + */ + public function getLayoutSettings() { + return $this->getThirdPartySetting('field_layout', 'settings', []); + } + + /** + * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::setLayoutId(). + */ + public function setLayoutId($layout_id, array $layout_settings = []) { + if ($this->getLayoutId() !== $layout_id) { + // @todo Devise a mechanism for mapping old regions to new ones in + // https://www.drupal.org/node/2796877. + $layout_definition = $this->getLayoutDefinition($layout_id); + $new_region = $layout_definition->getDefaultRegion(); + $layout_regions = $layout_definition->getRegions(); + foreach ($this->getComponents() as $name => $component) { + if (isset($component['region']) && !isset($layout_regions[$component['region']])) { + $component['region'] = $new_region; + $this->setComponent($name, $component); + } + } + } + $this->setThirdPartySetting('field_layout', 'id', $layout_id); + // Instantiate the plugin and consult it for the updated plugin + // configuration. Once layouts are no longer stored as third party settings, + // this will be handled by the code in + // \Drupal\Core\Config\Entity\ConfigEntityBase::set() that handles + // \Drupal\Core\Entity\EntityWithPluginCollectionInterface. + $layout_settings = $this->doGetLayout($layout_id, $layout_settings)->getConfiguration(); + $this->setThirdPartySetting('field_layout', 'settings', $layout_settings); + return $this; + } + + /** + * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::setLayout(). + */ + public function setLayout(LayoutInterface $layout) { + $this->setLayoutId($layout->getPluginId(), $layout->getConfiguration()); + return $this; + } + + /** + * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::getLayout(). + */ + public function getLayout() { + return $this->doGetLayout($this->getLayoutId(), $this->getLayoutSettings()); + } + + /** + * Gets the layout plugin. + * + * @param string $layout_id + * A layout plugin ID. + * @param array $layout_settings + * An array of settings. + * + * @return \Drupal\Core\Layout\LayoutInterface + * The layout plugin. + */ + protected function doGetLayout($layout_id, array $layout_settings) { + return \Drupal::service('plugin.manager.core.layout')->createInstance($layout_id, $layout_settings); + } + + /** + * Overrides \Drupal\Core\Entity\EntityDisplayBase::init(). + */ + protected function init() { + $this->ensureLayout(); + parent::init(); + } + + /** + * Overrides \Drupal\Core\Entity\EntityDisplayBase::preSave(). + */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + + // Ensure the plugin configuration is updated. Once layouts are no longer + // stored as third party settings, this will be handled by the code in + // \Drupal\Core\Config\Entity\ConfigEntityBase::preSave() that handles + // \Drupal\Core\Entity\EntityWithPluginCollectionInterface. + if ($this->getLayoutId()) { + $this->setLayout($this->getLayout()); + } + } + + /** + * {@inheritdoc} + */ + public function ensureLayout($default_layout_id = 'layout_onecol') { + if (!$this->getLayoutId()) { + $this->setLayoutId($default_layout_id); + } + + return $this; + } + + /** + * Overrides \Drupal\Core\Entity\EntityDisplayBase::calculateDependencies(). + * + * @see \Drupal\Core\Plugin\PluginDependencyTrait::calculatePluginDependencies() + * + * @todo Remove once https://www.drupal.org/node/2821191 is resolved. + */ + public function calculateDependencies() { + parent::calculateDependencies(); + + // This can be called during uninstallation, so check for a valid ID first. + if ($this->getLayoutId()) { + /** @var \Drupal\Core\Layout\LayoutInterface $layout */ + $layout = $this->getLayout(); + $definition = $layout->getPluginDefinition(); + + if (!in_array($definition->getProvider(), ['core', 'component'])) { + $this->addDependency('module', $definition->getProvider()); + } + if ($config_dependencies = $definition->getConfigDependencies()) { + $this->addDependencies($config_dependencies); + } + if ($layout_dependencies = $layout->calculateDependencies()) { + $this->addDependencies($layout_dependencies); + } + } + } + +} diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php new file mode 100644 index 000000000000..c58938e0a43d --- /dev/null +++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php @@ -0,0 +1,24 @@ +<?php + +namespace Drupal\field_layout\Entity; + +use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface; + +/** + * Provides an entity form display entity that has a layout. + */ +class FieldLayoutEntityFormDisplay extends EntityFormDisplay implements EntityDisplayWithLayoutInterface { + + use FieldLayoutEntityDisplayTrait; + + /** + * {@inheritdoc} + */ + public function getDefaultRegion() { + // This cannot be provided by the trait due to + // https://bugs.php.net/bug.php?id=71414 which is fixed in PHP 7.0.6. + return $this->getLayoutDefinition($this->getLayoutId())->getDefaultRegion(); + } + +} diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php new file mode 100644 index 000000000000..4f0c27402f6e --- /dev/null +++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php @@ -0,0 +1,24 @@ +<?php + +namespace Drupal\field_layout\Entity; + +use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface; + +/** + * Provides an entity view display entity that has a layout. + */ +class FieldLayoutEntityViewDisplay extends EntityViewDisplay implements EntityDisplayWithLayoutInterface { + + use FieldLayoutEntityDisplayTrait; + + /** + * {@inheritdoc} + */ + public function getDefaultRegion() { + // This cannot be provided by the trait due to + // https://bugs.php.net/bug.php?id=71414 which is fixed in PHP 7.0.6. + return $this->getLayoutDefinition($this->getLayoutId())->getDefaultRegion(); + } + +} diff --git a/core/modules/field_layout/src/FieldLayoutBuilder.php b/core/modules/field_layout/src/FieldLayoutBuilder.php new file mode 100644 index 000000000000..8ef5d048fc72 --- /dev/null +++ b/core/modules/field_layout/src/FieldLayoutBuilder.php @@ -0,0 +1,153 @@ +<?php + +namespace Drupal\field_layout; + +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface; +use Drupal\Core\Layout\LayoutPluginManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Builds a field layout. + */ +class FieldLayoutBuilder implements ContainerInjectionInterface { + + /** + * The layout plugin manager. + * + * @var \Drupal\Core\Layout\LayoutPluginManagerInterface + */ + protected $layoutPluginManager; + + /** + * The entity field manager. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface + */ + protected $entityFieldManager; + + /** + * Constructs a new FieldLayoutBuilder. + * + * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_plugin_manager + * The layout plugin manager. + * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager + * The entity field manager. + */ + public function __construct(LayoutPluginManagerInterface $layout_plugin_manager, EntityFieldManagerInterface $entity_field_manager) { + $this->layoutPluginManager = $layout_plugin_manager; + $this->entityFieldManager = $entity_field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.core.layout'), + $container->get('entity_field.manager') + ); + } + + /** + * Applies the layout to an entity build. + * + * @param array $build + * A renderable array representing the entity content or form. + * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $display + * The entity display holding the display options configured for the entity + * components. + */ + public function buildView(array &$build, EntityDisplayWithLayoutInterface $display) { + $layout_definition = $this->layoutPluginManager->getDefinition($display->getLayoutId(), FALSE); + if ($layout_definition && $fields = $this->getFields($build, $display, 'view')) { + // Add the regions to the $build in the correct order. + $regions = array_fill_keys($layout_definition->getRegionNames(), []); + + foreach ($fields as $name => $field) { + // Move the field from the top-level of $build into a region-specific + // section. + // @todo Ideally the array structure would remain unchanged, see + // https://www.drupal.org/node/2846393. + $regions[$field['region']][$name] = $build[$name]; + unset($build[$name]); + } + // Ensure this will not conflict with any existing array elements by + // prefixing with an underscore. + $build['_field_layout'] = $display->getLayout()->build($regions); + } + } + + /** + * Applies the layout to an entity form. + * + * @param array $build + * A renderable array representing the entity content or form. + * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $display + * The entity display holding the display options configured for the entity + * components. + */ + public function buildForm(array &$build, EntityDisplayWithLayoutInterface $display) { + $layout_definition = $this->layoutPluginManager->getDefinition($display->getLayoutId(), FALSE); + if ($layout_definition && $fields = $this->getFields($build, $display, 'form')) { + $fill = []; + $fill['#process'][] = '\Drupal\Core\Render\Element\RenderElement::processGroup'; + $fill['#pre_render'][] = '\Drupal\Core\Render\Element\RenderElement::preRenderGroup'; + // Add the regions to the $build in the correct order. + $regions = array_fill_keys($layout_definition->getRegionNames(), $fill); + + foreach ($fields as $name => $field) { + // As this is a form, #group can be used to relocate the fields. This + // avoids breaking hook_form_alter() implementations by not actually + // moving the field in the form structure. If a #group is already set, + // do not overwrite it. + if (!isset($build[$name]['#group'])) { + $build[$name]['#group'] = $field['region']; + } + } + // Ensure this will not conflict with any existing array elements by + // prefixing with an underscore. + $build['_field_layout'] = $display->getLayout()->build($regions); + } + } + + /** + * Gets the fields that need to be processed. + * + * @param array $build + * A renderable array representing the entity content or form. + * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $display + * The entity display holding the display options configured for the entity + * components. + * @param string $display_context + * The display context, either 'form' or 'view'. + * + * @return array + * An array of configurable fields present in the build. + */ + protected function getFields(array $build, EntityDisplayWithLayoutInterface $display, $display_context) { + $components = $display->getComponents(); + + // Ignore any extra fields from the list of field definitions. Field + // definitions can have a non-configurable display, but all extra fields are + // always displayed. + $field_definitions = array_diff_key( + $this->entityFieldManager->getFieldDefinitions($display->getTargetEntityTypeId(), $display->getTargetBundle()), + $this->entityFieldManager->getExtraFields($display->getTargetEntityTypeId(), $display->getTargetBundle()) + ); + + $fields_to_exclude = array_filter($field_definitions, function (FieldDefinitionInterface $field_definition) use ($display_context) { + // Remove fields with a non-configurable display. + return !$field_definition->isDisplayConfigurable($display_context); + }); + $components = array_diff_key($components, $fields_to_exclude); + + // Only include fields present in the build. + $components = array_intersect_key($components, $build); + + return $components; + } + +} diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php new file mode 100644 index 000000000000..043e5c739700 --- /dev/null +++ b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php @@ -0,0 +1,177 @@ +<?php + +namespace Drupal\field_layout\Form; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Form\SubformState; +use Drupal\Core\Plugin\PluginFormInterface; +use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface; + +/** + * Provides shared code for entity display forms. + * + * Both EntityViewDisplayEditForm and EntityFormDisplayEditForm must maintain + * their parent hierarchy, while being identically enhanced by Field Layout. + * This trait contains the code they both share. + */ +trait FieldLayoutEntityDisplayFormTrait { + + /** + * The field layout plugin manager. + * + * @var \Drupal\Core\Layout\LayoutPluginManagerInterface + */ + protected $layoutPluginManager; + + /** + * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::getRegions(). + */ + public function getRegions() { + $regions = []; + + $layout_definition = $this->layoutPluginManager->getDefinition($this->getEntity()->getLayoutId()); + foreach ($layout_definition->getRegions() as $name => $region) { + $regions[$name] = [ + 'title' => $region['label'], + 'message' => $this->t('No field is displayed.'), + ]; + } + + $regions['hidden'] = [ + 'title' => $this->t('Disabled', [], ['context' => 'Plural']), + 'message' => $this->t('No field is hidden.'), + ]; + + return $regions; + } + + /** + * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::form(). + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + $form['field_layouts'] = [ + '#type' => 'details', + '#title' => $this->t('Layout settings'), + ]; + + $layout_plugin = $this->getLayout($this->getEntity(), $form_state); + + $form['field_layouts']['field_layout'] = [ + '#type' => 'select', + '#title' => $this->t('Select a layout'), + '#options' => $this->layoutPluginManager->getLayoutOptions(), + '#default_value' => $layout_plugin->getPluginId(), + '#ajax' => [ + 'callback' => '::settingsAjax', + 'wrapper' => 'field-layout-settings-wrapper', + 'trigger_as' => ['name' => 'field_layout_change'], + ], + ]; + $form['field_layouts']['submit'] = [ + '#type' => 'submit', + '#name' => 'field_layout_change', + '#value' => $this->t('Change layout'), + '#submit' => ['::settingsAjaxSubmit'], + '#attributes' => ['class' => ['js-hide']], + '#ajax' => [ + 'callback' => '::settingsAjax', + 'wrapper' => 'field-layout-settings-wrapper', + ], + ]; + + $form['field_layouts']['settings_wrapper'] = [ + '#type' => 'container', + '#id' => 'field-layout-settings-wrapper', + '#tree' => TRUE, + ]; + + if ($layout_plugin instanceof PluginFormInterface) { + $form['field_layouts']['settings_wrapper']['layout_settings'] = []; + $subform_state = SubformState::createForSubform($form['field_layouts']['settings_wrapper']['layout_settings'], $form, $form_state); + $form['field_layouts']['settings_wrapper']['layout_settings'] = $layout_plugin->buildConfigurationForm($form['field_layouts']['settings_wrapper']['layout_settings'], $subform_state); + } + + return $form; + } + + /** + * Gets the layout plugin for the currently selected field layout. + * + * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $entity + * The current form entity. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\Core\Layout\LayoutInterface + * The layout plugin. + */ + protected function getLayout(EntityDisplayWithLayoutInterface $entity, FormStateInterface $form_state) { + if (!$layout_plugin = $form_state->get('layout_plugin')) { + $stored_layout_id = $entity->getLayoutId(); + // Use selected layout if it exists, falling back to the stored layout. + $layout_id = $form_state->getValue('field_layout', $stored_layout_id); + // If the current layout is the stored layout, use the stored layout + // settings. Otherwise leave the settings empty. + $layout_settings = $layout_id === $stored_layout_id ? $entity->getLayoutSettings() : []; + + $layout_plugin = $this->layoutPluginManager->createInstance($layout_id, $layout_settings); + $form_state->set('layout_plugin', $layout_plugin); + } + return $layout_plugin; + } + + /** + * Ajax callback for the field layout settings form. + */ + public static function settingsAjax($form, FormStateInterface $form_state) { + return $form['field_layouts']['settings_wrapper']; + } + + /** + * Submit handler for the non-JS case. + */ + public function settingsAjaxSubmit($form, FormStateInterface $form_state) { + $form_state->set('layout_plugin', NULL); + $form_state->setRebuild(); + } + + /** + * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::validateForm(). + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + $layout_plugin = $this->getLayout($this->getEntity(), $form_state); + if ($layout_plugin instanceof PluginFormInterface) { + $subform_state = SubformState::createForSubform($form['field_layouts']['settings_wrapper']['layout_settings'], $form, $form_state); + $layout_plugin->validateConfigurationForm($form['field_layouts']['settings_wrapper']['layout_settings'], $subform_state); + } + } + + /** + * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::submitForm(). + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + + $entity = $this->getEntity(); + $layout_plugin = $this->getLayout($entity, $form_state); + if ($layout_plugin instanceof PluginFormInterface) { + $subform_state = SubformState::createForSubform($form['field_layouts']['settings_wrapper']['layout_settings'], $form, $form_state); + $layout_plugin->submitConfigurationForm($form['field_layouts']['settings_wrapper']['layout_settings'], $subform_state); + } + + $entity->setLayout($layout_plugin); + } + + /** + * Gets the form entity. + * + * @return \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface + * The current form entity. + */ + abstract public function getEntity(); + +} diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php new file mode 100644 index 000000000000..c42a9d85a81e --- /dev/null +++ b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php @@ -0,0 +1,44 @@ +<?php + +namespace Drupal\field_layout\Form; + +use Drupal\Component\Plugin\PluginManagerBase; +use Drupal\Core\Field\FieldTypePluginManagerInterface; +use Drupal\Core\Layout\LayoutPluginManagerInterface; +use Drupal\field_ui\Form\EntityFormDisplayEditForm; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Edit form for the EntityFormDisplay entity type. + */ +class FieldLayoutEntityFormDisplayEditForm extends EntityFormDisplayEditForm { + + use FieldLayoutEntityDisplayFormTrait; + + /** + * FieldLayoutEntityFormDisplayEditForm constructor. + * + * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager + * The field type manager. + * @param \Drupal\Component\Plugin\PluginManagerBase $plugin_manager + * The widget plugin manager. + * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_plugin_manager + * The layout plugin manager. + */ + public function __construct(FieldTypePluginManagerInterface $field_type_manager, PluginManagerBase $plugin_manager, LayoutPluginManagerInterface $layout_plugin_manager) { + parent::__construct($field_type_manager, $plugin_manager); + $this->layoutPluginManager = $layout_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.field.field_type'), + $container->get('plugin.manager.field.widget'), + $container->get('plugin.manager.core.layout') + ); + } + +} diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php new file mode 100644 index 000000000000..92bfbde022d4 --- /dev/null +++ b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php @@ -0,0 +1,44 @@ +<?php + +namespace Drupal\field_layout\Form; + +use Drupal\Component\Plugin\PluginManagerBase; +use Drupal\Core\Field\FieldTypePluginManagerInterface; +use Drupal\Core\Layout\LayoutPluginManagerInterface; +use Drupal\field_ui\Form\EntityViewDisplayEditForm; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Edit form for the EntityViewDisplay entity type. + */ +class FieldLayoutEntityViewDisplayEditForm extends EntityViewDisplayEditForm { + + use FieldLayoutEntityDisplayFormTrait; + + /** + * FieldLayoutEntityViewDisplayEditForm constructor. + * + * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager + * The field type manager. + * @param \Drupal\Component\Plugin\PluginManagerBase $plugin_manager + * The formatter plugin manager. + * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_plugin_manager + * The field layout plugin manager. + */ + public function __construct(FieldTypePluginManagerInterface $field_type_manager, PluginManagerBase $plugin_manager, LayoutPluginManagerInterface $layout_plugin_manager) { + parent::__construct($field_type_manager, $plugin_manager); + $this->layoutPluginManager = $layout_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.field.field_type'), + $container->get('plugin.manager.field.formatter'), + $container->get('plugin.manager.core.layout') + ); + } + +} diff --git a/core/modules/field_layout/tests/modules/field_layout_test/config/schema/field_layout_test.schema.yml b/core/modules/field_layout/tests/modules/field_layout_test/config/schema/field_layout_test.schema.yml new file mode 100644 index 000000000000..dc95ab32fd65 --- /dev/null +++ b/core/modules/field_layout/tests/modules/field_layout_test/config/schema/field_layout_test.schema.yml @@ -0,0 +1,7 @@ +layout_plugin.settings.test_layout_main_and_footer: + type: layout_plugin.settings + label: 'Layout test plugin settings' + mapping: + setting_1: + type: string + label: 'Setting 1' diff --git a/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml new file mode 100644 index 000000000000..4d699e463e0e --- /dev/null +++ b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml @@ -0,0 +1,8 @@ +name: 'Field Layout test' +type: module +description: 'Support module for Field Layout tests.' +core: 8.x +package: Testing +version: VERSION +dependencies: + - entity_test diff --git a/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml new file mode 100644 index 000000000000..bcea2888f0c6 --- /dev/null +++ b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml @@ -0,0 +1,7 @@ +entity.entity_test.test_view_mode: + path: '/entity_test/{entity_test}/test' + defaults: + _entity_view: 'entity_test.test' + _title: 'Test test view mode' + requirements: + _entity_access: 'entity_test.view' diff --git a/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutContentFooter.php b/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutContentFooter.php new file mode 100644 index 000000000000..c283c82d7875 --- /dev/null +++ b/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutContentFooter.php @@ -0,0 +1,27 @@ +<?php + +namespace Drupal\field_layout_test\Plugin\Layout; + +use Drupal\Core\Layout\LayoutDefault; + +/** + * Provides an annotated layout plugin for field_layout tests. + * + * @Layout( + * id = "test_layout_content_and_footer", + * label = @Translation("Test plugin: Content and Footer"), + * category = @Translation("Layout test"), + * description = @Translation("Test layout"), + * regions = { + * "content" = { + * "label" = @Translation("Content Region") + * }, + * "footer" = { + * "label" = @Translation("Footer Region") + * } + * }, + * ) + */ +class TestLayoutContentFooter extends LayoutDefault { + +} diff --git a/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutMainFooter.php b/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutMainFooter.php new file mode 100644 index 000000000000..cfe95e137735 --- /dev/null +++ b/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/TestLayoutMainFooter.php @@ -0,0 +1,50 @@ +<?php + +namespace Drupal\field_layout_test\Plugin\Layout; + +use Drupal\Core\Layout\LayoutDefault; + +/** + * Provides an annotated layout plugin for field_layout tests. + * + * @Layout( + * id = "test_layout_main_and_footer", + * label = @Translation("Test plugin: Main and Footer"), + * category = @Translation("Layout test"), + * description = @Translation("Test layout"), + * regions = { + * "main" = { + * "label" = @Translation("Main Region") + * }, + * "footer" = { + * "label" = @Translation("Footer Region") + * } + * }, + * config_dependencies = { + * "module" = { + * "dependency_from_annotation", + * }, + * }, + * ) + */ +class TestLayoutMainFooter extends LayoutDefault { + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'setting_1' => 'Default', + ]; + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $dependencies = parent::calculateDependencies(); + $dependencies['module'][] = 'dependency_from_calculateDependencies'; + return $dependencies; + } + +} diff --git a/core/modules/field_layout/tests/src/Functional/FieldLayoutTest.php b/core/modules/field_layout/tests/src/Functional/FieldLayoutTest.php new file mode 100644 index 000000000000..683ccd2f93f1 --- /dev/null +++ b/core/modules/field_layout/tests/src/Functional/FieldLayoutTest.php @@ -0,0 +1,76 @@ +<?php + +namespace Drupal\Tests\field_layout\Functional; + +use Drupal\Tests\BrowserTestBase; + +/** + * Tests using field layout for entity displays. + * + * @group field_layout + */ +class FieldLayoutTest extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = ['field_layout', 'field_ui', 'node']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->createContentType([ + 'type' => 'article', + ]); + $this->createNode([ + 'type' => 'article', + 'title' => 'The node title', + 'body' => [[ + 'value' => 'The node body', + ]], + ]); + $this->drupalLogin($this->drupalCreateUser([ + 'access administration pages', + 'administer content types', + 'administer nodes', + 'administer node fields', + 'administer node display', + 'administer node form display', + 'view the administration theme', + ])); + } + + /** + * Tests an entity type that has fields shown by default. + */ + public function testNodeView() { + // By default, the one-column layout is used. + $this->drupalGet('node/1'); + $this->assertSession()->elementExists('css', '.layout--onecol'); + $this->assertSession()->elementExists('css', '.layout-region--content .field--name-body'); + + $this->drupalGet('admin/structure/types/manage/article/display'); + $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles()); + $this->assertSession()->optionExists('fields[body][region]', 'content'); + } + + /** + * Gets the region titles on the page. + * + * @return string[] + * An array of region titles. + */ + protected function getRegionTitles() { + $region_titles = []; + $region_title_elements = $this->getSession()->getPage()->findAll('css', '.region-title td'); + /** @var \Behat\Mink\Element\NodeElement[] $region_title_elements */ + foreach ($region_title_elements as $region_title_element) { + $region_titles[] = $region_title_element->getText(); + } + return $region_titles; + } + +} diff --git a/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php new file mode 100644 index 000000000000..1336a704ab80 --- /dev/null +++ b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php @@ -0,0 +1,266 @@ +<?php + +namespace Drupal\Tests\field_layout\FunctionalJavascript; + +use Drupal\entity_test\Entity\EntityTest; +use Drupal\FunctionalJavascriptTests\JavascriptTestBase; + +/** + * Tests using field layout for entity displays. + * + * @group field_layout + */ +class FieldLayoutTest extends JavascriptTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = ['field_layout', 'field_ui', 'field_layout_test', 'layout_test']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $entity = EntityTest::create([ + 'name' => 'The name for this entity', + 'field_test_text' => [[ + 'value' => 'The field test text value', + ]], + ]); + $entity->save(); + $this->drupalLogin($this->drupalCreateUser([ + 'access administration pages', + 'view test entity', + 'administer entity_test content', + 'administer entity_test fields', + 'administer entity_test display', + 'administer entity_test form display', + 'view the administration theme', + ])); + } + + /** + * Tests that layouts are unique per view mode. + */ + public function testEntityViewModes() { + // By default, the field is not visible. + $this->drupalGet('entity_test/1/test'); + $this->assertSession()->elementNotExists('css', '.layout-region--content .field--name-field-test-text'); + $this->drupalGet('entity_test/1'); + $this->assertSession()->elementNotExists('css', '.layout-region--content .field--name-field-test-text'); + + // Change the layout for the "test" view mode. See + // core.entity_view_mode.entity_test.test.yml. + $this->drupalGet('entity_test/structure/entity_test/display'); + $this->click('#edit-modes'); + $this->getSession()->getPage()->checkField('display_modes_custom[test]'); + $this->submitForm([], 'Save'); + $this->clickLink('configure them'); + $this->getSession()->getPage()->pressButton('Show row weights'); + $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->submitForm([], 'Save'); + + // Each view mode has a different layout. + $this->drupalGet('entity_test/1/test'); + $this->assertSession()->elementExists('css', '.layout-region--content .field--name-field-test-text'); + $this->drupalGet('entity_test/1'); + $this->assertSession()->elementNotExists('css', '.layout-region--content .field--name-field-test-text'); + } + + /** + * Tests the use of field layout for entity form displays. + */ + public function testEntityForm() { + // By default, the one-column layout is used. + $this->drupalGet('entity_test/manage/1/edit'); + $this->assertFieldInRegion('field_test_text[0][value]', 'content'); + + // The one-column layout is in use. + $this->drupalGet('entity_test/structure/entity_test/form-display'); + $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles()); + + // Switch the layout to two columns. + $this->click('#edit-field-layouts'); + $this->getSession()->getPage()->selectFieldOption('field_layout', 'layout_twocol'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->submitForm([], 'Save'); + + // The field is moved to the default region for the new layout. + $this->assertSession()->pageTextContains('Your settings have been saved.'); + $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles()); + + $this->drupalGet('entity_test/manage/1/edit'); + // No fields are visible, and the regions don't display when empty. + $this->assertFieldInRegion('field_test_text[0][value]', 'left'); + $this->assertSession()->elementExists('css', '.layout-region--left .field--name-field-test-text'); + + // After a refresh the new regions are still there. + $this->drupalGet('entity_test/structure/entity_test/form-display'); + $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles()); + + // Drag the field to the right region. + $field_test_text_row = $this->getSession()->getPage()->find('css', '#field-test-text'); + $right_region_row = $this->getSession()->getPage()->find('css', '.region-right-message'); + $field_test_text_row->find('css', '.handle')->dragTo($right_region_row); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->submitForm([], 'Save'); + $this->assertSession()->pageTextContains('Your settings have been saved.'); + + // The new layout is used. + $this->drupalGet('entity_test/manage/1/edit'); + $this->assertSession()->elementExists('css', '.layout-region--right .field--name-field-test-text'); + $this->assertFieldInRegion('field_test_text[0][value]', 'right'); + + // Move the field to the right region without tabledrag. + $this->drupalGet('entity_test/structure/entity_test/form-display'); + $this->getSession()->getPage()->pressButton('Show row weights'); + $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'right'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->submitForm([], 'Save'); + $this->assertSession()->pageTextContains('Your settings have been saved.'); + + // The updated region is used. + $this->drupalGet('entity_test/manage/1/edit'); + $this->assertFieldInRegion('field_test_text[0][value]', 'right'); + + // The layout is still in use without Field UI. + $this->container->get('module_installer')->uninstall(['field_ui']); + $this->drupalGet('entity_test/manage/1/edit'); + $this->assertFieldInRegion('field_test_text[0][value]', 'right'); + } + + /** + * Tests the use of field layout for entity view displays. + */ + public function testEntityView() { + // The one-column layout is in use. + $this->drupalGet('entity_test/structure/entity_test/display'); + $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles()); + + // Switch the layout to two columns. + $this->click('#edit-field-layouts'); + $this->getSession()->getPage()->selectFieldOption('field_layout', 'layout_twocol'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->submitForm([], 'Save'); + + $this->assertSession()->pageTextContains('Your settings have been saved.'); + $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles()); + + $this->drupalGet('entity_test/1'); + // No fields are visible, and the regions don't display when empty. + $this->assertSession()->elementNotExists('css', '.layout--twocol'); + $this->assertSession()->elementNotExists('css', '.layout-region'); + $this->assertSession()->elementNotExists('css', '.field--name-field-test-text'); + + // After a refresh the new regions are still there. + $this->drupalGet('entity_test/structure/entity_test/display'); + $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles()); + + // Drag the field to the left region. + $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected()); + $field_test_text_row = $this->getSession()->getPage()->find('css', '#field-test-text'); + $left_region_row = $this->getSession()->getPage()->find('css', '.region-left-message'); + $field_test_text_row->find('css', '.handle')->dragTo($left_region_row); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertFalse($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected()); + $this->submitForm([], 'Save'); + $this->assertSession()->pageTextContains('Your settings have been saved.'); + + // The new layout is used. + $this->drupalGet('entity_test/1'); + $this->assertSession()->elementExists('css', '.layout--twocol'); + $this->assertSession()->elementExists('css', '.layout-region--left .field--name-field-test-text'); + + // Move the field to the right region without tabledrag. + $this->drupalGet('entity_test/structure/entity_test/display'); + $this->getSession()->getPage()->pressButton('Show row weights'); + $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'right'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->submitForm([], 'Save'); + $this->assertSession()->pageTextContains('Your settings have been saved.'); + + // The updated region is used. + $this->drupalGet('entity_test/1'); + $this->assertSession()->elementExists('css', '.layout-region--right .field--name-field-test-text'); + + // The layout is still in use without Field UI. + $this->container->get('module_installer')->uninstall(['field_ui']); + $this->drupalGet('entity_test/1'); + $this->assertSession()->elementExists('css', '.layout--twocol'); + $this->assertSession()->elementExists('css', '.layout-region--right .field--name-field-test-text'); + } + + /** + * Tests layout plugins with forms. + */ + public function testLayoutForms() { + $this->drupalGet('entity_test/structure/entity_test/display'); + // Switch to a field layout with settings. + $this->click('#edit-field-layouts'); + + // Test switching between layouts with and without forms. + $this->getSession()->getPage()->selectFieldOption('field_layout', 'layout_test_plugin'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->fieldExists('settings_wrapper[layout_settings][setting_1]'); + + $this->getSession()->getPage()->selectFieldOption('field_layout', 'layout_test_2col'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->fieldNotExists('settings_wrapper[layout_settings][setting_1]'); + + $this->getSession()->getPage()->selectFieldOption('field_layout', 'layout_test_plugin'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->fieldExists('settings_wrapper[layout_settings][setting_1]'); + + // Move the test field to the content region. + $this->getSession()->getPage()->pressButton('Show row weights'); + $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->submitForm([], 'Save'); + + $this->drupalGet('entity_test/1'); + $this->assertSession()->pageTextContains('Blah: Default'); + + // Update the field layout settings. + $this->drupalGet('entity_test/structure/entity_test/display'); + $this->click('#edit-field-layouts'); + $this->getSession()->getPage()->fillField('settings_wrapper[layout_settings][setting_1]', 'Test text'); + $this->submitForm([], 'Save'); + + $this->drupalGet('entity_test/1'); + $this->assertSession()->pageTextContains('Blah: Test text'); + } + + /** + * Gets the region titles on the page. + * + * @return string[] + * An array of region titles. + */ + protected function getRegionTitles() { + $region_titles = []; + $region_title_elements = $this->getSession()->getPage()->findAll('css', '.region-title td'); + /** @var \Behat\Mink\Element\NodeElement[] $region_title_elements */ + foreach ($region_title_elements as $region_title_element) { + $region_titles[] = $region_title_element->getText(); + } + return $region_titles; + } + + /** + * Asserts that a field exists in a given region. + * + * @param string $field_selector + * The field selector, one of field id|name|label|value. + * @param string $region_name + * The machine name of the region. + */ + protected function assertFieldInRegion($field_selector, $region_name) { + $region_element = $this->getSession()->getPage()->find('css', ".layout-region--$region_name"); + $this->assertNotNull($region_element); + $this->assertSession()->fieldExists($field_selector, $region_element); + } + +} diff --git a/core/modules/field_layout/tests/src/Kernel/FieldLayoutEntityDisplayTest.php b/core/modules/field_layout/tests/src/Kernel/FieldLayoutEntityDisplayTest.php new file mode 100644 index 000000000000..8a7fde9cd165 --- /dev/null +++ b/core/modules/field_layout/tests/src/Kernel/FieldLayoutEntityDisplayTest.php @@ -0,0 +1,182 @@ +<?php + +namespace Drupal\Tests\field_layout\Kernel; + +use Drupal\field_layout\Entity\FieldLayoutEntityViewDisplay; +use Drupal\KernelTests\KernelTestBase; + +/** + * @coversDefaultClass \Drupal\field_layout\Entity\FieldLayoutEntityDisplayTrait + * @group field_layout + */ +class FieldLayoutEntityDisplayTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['layout_discovery', 'field_layout', 'entity_test', 'field_layout_test', 'system']; + + /** + * @covers ::preSave + * @covers ::calculateDependencies + */ + public function testPreSave() { + // Create an entity display with one hidden and one visible field. + $entity_display = FieldLayoutEntityViewDisplay::create([ + 'targetEntityType' => 'entity_test', + 'bundle' => 'entity_test', + 'mode' => 'default', + 'status' => TRUE, + 'content' => [ + 'foo' => ['type' => 'visible'], + 'bar' => ['type' => 'hidden'], + 'name' => ['type' => 'hidden', 'region' => 'content'], + ], + ]); + + $expected = [ + 'langcode' => 'en', + 'status' => TRUE, + 'dependencies' => [], + 'third_party_settings' => [ + 'field_layout' => [ + 'id' => 'layout_onecol', + 'settings' => [], + ], + ], + 'id' => 'entity_test.entity_test.default', + 'targetEntityType' => 'entity_test', + 'bundle' => 'entity_test', + 'mode' => 'default', + 'content' => [ + 'foo' => [ + 'type' => 'visible', + ], + 'bar' => [ + 'type' => 'hidden', + ], + ], + 'hidden' => [], + ]; + $this->assertEntityValues($expected, $entity_display->toArray()); + + // Save the display. + // the 'content' property and the visible field has the default region set. + $entity_display->save(); + + // The dependencies have been updated. + $expected['dependencies']['module'] = [ + 'entity_test', + 'field_layout', + ]; + // A third party setting is added by the entity_test module. + $expected['third_party_settings']['entity_test'] = ['foo' => 'bar']; + // The visible field is assigned the default region. + $expected['content']['foo']['region'] = 'content'; + // The hidden field is removed from the list of visible fields, and marked + // as hidden. + unset($expected['content']['bar']); + $expected['hidden'] = ['bar' => TRUE]; + + $this->assertEntityValues($expected, $entity_display->toArray()); + + // Assign a new layout that has default settings and complex dependencies, + // but do not save yet. + $entity_display->setLayoutId('test_layout_main_and_footer'); + + // The default settings were added. + $expected['third_party_settings']['field_layout'] = [ + 'id' => 'test_layout_main_and_footer', + 'settings' => [ + 'setting_1' => 'Default', + ], + ]; + // The field was moved to the default region. + $expected['content']['foo'] = [ + 'type' => 'visible', + 'region' => 'main', + 'weight' => -4, + 'settings' => [], + 'third_party_settings' => [], + ]; + $this->assertEntityValues($expected, $entity_display->toArray()); + + // After saving, the dependencies have been updated. + $entity_display->save(); + $expected['dependencies']['module'] = [ + 'dependency_from_annotation', + 'dependency_from_calculateDependencies', + 'entity_test', + 'field_layout', + 'field_layout_test', + ]; + $this->assertEntityValues($expected, $entity_display->toArray()); + + // Assign a layout with provided settings. + $entity_display->setLayoutId('test_layout_main_and_footer', ['setting_1' => 'foobar']); + $entity_display->save(); + + // The setting overrides the default value. + $expected['third_party_settings']['field_layout']['settings']['setting_1'] = 'foobar'; + $this->assertEntityValues($expected, $entity_display->toArray()); + + // Move a field to the non-default region. + $component = $entity_display->getComponent('foo'); + $component['region'] = 'footer'; + $entity_display->setComponent('foo', $component); + $entity_display->save(); + + // The field region is saved. + $expected['content']['foo']['region'] = 'footer'; + $this->assertEntityValues($expected, $entity_display->toArray()); + + // Assign a different layout that shares the same non-default region. + $entity_display->setLayoutId('test_layout_content_and_footer'); + $entity_display->save(); + + // The dependencies have been updated. + $expected['dependencies']['module'] = [ + 'entity_test', + 'field_layout', + 'field_layout_test', + ]; + // The layout has been updated. + $expected['third_party_settings']['field_layout'] = [ + 'id' => 'test_layout_content_and_footer', + 'settings' => [], + ]; + // The field remains in its current region instead of moving to the default. + $this->assertEntityValues($expected, $entity_display->toArray()); + + $this->container->get('module_installer')->uninstall(['field_layout']); + + $entity_storage = $this->container->get('entity_type.manager')->getStorage('entity_view_display'); + $entity_display = $entity_storage->load('entity_test.entity_test.default'); + + // The dependencies have been updated. + $expected['dependencies']['module'] = [ + 'entity_test', + ]; + // All field_layout settings were removed. + unset($expected['third_party_settings']['field_layout']); + // The field has returned to the default content region. + $expected['content']['foo']['region'] = 'content'; + $this->assertEntityValues($expected, $entity_display->toArray()); + } + + /** + * Asserts than an entity has the correct values. + * + * @param mixed $expected + * @param array $values + * @param string $message + */ + public static function assertEntityValues($expected, array $values, $message = '') { + + static::assertArrayHasKey('uuid', $values); + unset($values['uuid']); + + static::assertEquals($expected, $values, $message); + } + +} diff --git a/core/modules/field_layout/tests/src/Unit/FieldLayoutBuilderTest.php b/core/modules/field_layout/tests/src/Unit/FieldLayoutBuilderTest.php new file mode 100644 index 000000000000..8e6bf956c4bf --- /dev/null +++ b/core/modules/field_layout/tests/src/Unit/FieldLayoutBuilderTest.php @@ -0,0 +1,308 @@ +<?php + +namespace Drupal\Tests\field_layout\Unit; + +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface; +use Drupal\field_layout\FieldLayoutBuilder; +use Drupal\Core\Layout\LayoutPluginManagerInterface; +use Drupal\Core\Layout\LayoutDefault; +use Drupal\Core\Layout\LayoutDefinition; +use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; + +/** + * @coversDefaultClass \Drupal\field_layout\FieldLayoutBuilder + * @group field_layout + */ +class FieldLayoutBuilderTest extends UnitTestCase { + + /** + * @var \Drupal\Core\Layout\LayoutPluginManager|\Prophecy\Prophecy\ProphecyInterface + */ + protected $layoutPluginManager; + + /** + * @var \Drupal\Core\Entity\EntityFieldManagerInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $entityFieldManager; + + /** + * @var \Drupal\field_layout\FieldLayoutBuilder + */ + protected $fieldLayoutBuilder; + + /** + * @var \Drupal\Core\Layout\LayoutInterface + */ + protected $layoutPlugin; + + /** + * @var \Drupal\Core\Layout\LayoutDefinition + */ + protected $pluginDefinition; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->pluginDefinition = new LayoutDefinition([ + 'library' => 'field_layout/drupal.layout.twocol', + 'theme_hook' => 'layout__twocol', + 'regions' => [ + 'left' => [ + 'label' => 'Left', + ], + 'right' => [ + 'label' => 'Right', + ], + ], + ]); + $this->layoutPlugin = new LayoutDefault([], 'two_column', $this->pluginDefinition); + + $this->layoutPluginManager = $this->prophesize(LayoutPluginManagerInterface::class); + $this->layoutPluginManager->getDefinition('unknown', FALSE)->willReturn(NULL); + $this->layoutPluginManager->getDefinition('two_column', FALSE)->willReturn($this->pluginDefinition); + + $this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class); + + $this->fieldLayoutBuilder = new FieldLayoutBuilder($this->layoutPluginManager->reveal(), $this->entityFieldManager->reveal()); + } + + /** + * @covers ::buildView + * @covers ::getFields + */ + public function testBuildView() { + $definitions = []; + $non_configurable_field_definition = $this->prophesize(FieldDefinitionInterface::class); + $non_configurable_field_definition->isDisplayConfigurable('view')->willReturn(FALSE); + $definitions['non_configurable_field'] = $non_configurable_field_definition->reveal(); + $definitions['non_configurable_field_with_extra_field'] = $non_configurable_field_definition->reveal(); + $this->entityFieldManager->getFieldDefinitions('the_entity_type_id', 'the_entity_type_bundle')->willReturn($definitions); + $extra_fields = []; + $extra_fields['non_configurable_field_with_extra_field'] = [ + 'label' => 'This non-configurable field is also defined in hook_entity_extra_field_info()', + ]; + $this->entityFieldManager->getExtraFields('the_entity_type_id', 'the_entity_type_bundle')->willReturn($extra_fields); + + $build = [ + 'test1' => [ + '#markup' => 'Test1', + ], + 'non_configurable_field' => [ + '#markup' => 'Non-configurable', + ], + 'non_configurable_field_with_extra_field' => [ + '#markup' => 'Non-configurable with extra field', + ], + ]; + + $display = $this->prophesize(EntityDisplayWithLayoutInterface::class); + $display->getTargetEntityTypeId()->willReturn('the_entity_type_id'); + $display->getTargetBundle()->willReturn('the_entity_type_bundle'); + $display->getLayout()->willReturn($this->layoutPlugin); + $display->getLayoutId()->willReturn('two_column'); + $display->getLayoutSettings()->willReturn([]); + $display->getComponents()->willReturn([ + 'test1' => [ + 'region' => 'right', + ], + 'non_configurable_field' => [ + 'region' => 'left', + ], + 'non_configurable_field_with_extra_field' => [ + 'region' => 'left', + ], + ]); + + $expected = [ + 'non_configurable_field' => [ + '#markup' => 'Non-configurable', + ], + '_field_layout' => [ + 'left' => [ + 'non_configurable_field_with_extra_field' => [ + '#markup' => 'Non-configurable with extra field', + ], + ], + 'right' => [ + 'test1' => [ + '#markup' => 'Test1', + ], + ], + '#settings' => [], + '#layout' => $this->pluginDefinition, + '#theme' => 'layout__twocol', + '#attached' => [ + 'library' => [ + 'field_layout/drupal.layout.twocol', + ], + ], + ], + ]; + $this->fieldLayoutBuilder->buildView($build, $display->reveal()); + $this->assertEquals($expected, $build); + $this->assertSame($expected, $build); + } + + /** + * @covers ::buildForm + * @covers ::getFields + */ + public function testBuildForm() { + $definitions = []; + $non_configurable_field_definition = $this->prophesize(FieldDefinitionInterface::class); + $non_configurable_field_definition->isDisplayConfigurable('form')->willReturn(FALSE); + $definitions['non_configurable_field'] = $non_configurable_field_definition->reveal(); + $this->entityFieldManager->getFieldDefinitions('the_entity_type_id', 'the_entity_type_bundle')->willReturn($definitions); + $this->entityFieldManager->getExtraFields('the_entity_type_id', 'the_entity_type_bundle')->willReturn([]); + + $build = [ + 'test1' => [ + '#markup' => 'Test1', + ], + 'test2' => [ + '#markup' => 'Test2', + '#group' => 'existing_group', + ], + 'field_layout' => [ + '#markup' => 'Field created through the UI happens to be named "Layout"', + ], + 'non_configurable_field' => [ + '#markup' => 'Non-configurable', + ], + ]; + + $display = $this->prophesize(EntityDisplayWithLayoutInterface::class); + $display->getTargetEntityTypeId()->willReturn('the_entity_type_id'); + $display->getTargetBundle()->willReturn('the_entity_type_bundle'); + $display->getLayout()->willReturn($this->layoutPlugin); + $display->getLayoutId()->willReturn('two_column'); + $display->getLayoutSettings()->willReturn([]); + $display->getComponents()->willReturn([ + 'test1' => [ + 'region' => 'right', + ], + 'test2' => [ + 'region' => 'left', + ], + 'field_layout' => [ + 'region' => 'right', + ], + 'non_configurable_field' => [ + 'region' => 'left', + ], + ]); + + $expected = [ + 'test1' => [ + '#markup' => 'Test1', + '#group' => 'right', + ], + 'test2' => [ + '#markup' => 'Test2', + '#group' => 'existing_group', + ], + 'field_layout' => [ + '#markup' => 'Field created through the UI happens to be named "Layout"', + '#group' => 'right', + ], + 'non_configurable_field' => [ + '#markup' => 'Non-configurable', + ], + '_field_layout' => [ + 'left' => [ + '#process' => ['\Drupal\Core\Render\Element\RenderElement::processGroup'], + '#pre_render' => ['\Drupal\Core\Render\Element\RenderElement::preRenderGroup'], + ], + 'right' => [ + '#process' => ['\Drupal\Core\Render\Element\RenderElement::processGroup'], + '#pre_render' => ['\Drupal\Core\Render\Element\RenderElement::preRenderGroup'], + ], + '#settings' => [], + '#layout' => $this->pluginDefinition, + '#theme' => 'layout__twocol', + '#attached' => [ + 'library' => [ + 'field_layout/drupal.layout.twocol', + ], + ], + ], + ]; + $this->fieldLayoutBuilder->buildForm($build, $display->reveal()); + $this->assertEquals($expected, $build); + $this->assertSame($expected, $build); + } + + /** + * @covers ::buildForm + */ + public function testBuildFormEmpty() { + $definitions = []; + $non_configurable_field_definition = $this->prophesize(FieldDefinitionInterface::class); + $non_configurable_field_definition->isDisplayConfigurable('form')->willReturn(FALSE); + $definitions['non_configurable_field'] = $non_configurable_field_definition->reveal(); + $this->entityFieldManager->getFieldDefinitions('the_entity_type_id', 'the_entity_type_bundle')->willReturn($definitions); + $this->entityFieldManager->getExtraFields('the_entity_type_id', 'the_entity_type_bundle')->willReturn([]); + + $build = [ + 'non_configurable_field' => [ + '#markup' => 'Non-configurable', + ], + ]; + + $display = $this->prophesize(EntityDisplayWithLayoutInterface::class); + $display->getTargetEntityTypeId()->willReturn('the_entity_type_id'); + $display->getTargetBundle()->willReturn('the_entity_type_bundle'); + $display->getLayout()->willReturn($this->layoutPlugin); + $display->getLayoutId()->willReturn('two_column'); + $display->getLayoutSettings()->willReturn([]); + $display->getComponents()->willReturn([ + 'test1' => [ + 'region' => 'right', + ], + 'non_configurable_field' => [ + 'region' => 'left', + ], + ]); + + $expected = [ + 'non_configurable_field' => [ + '#markup' => 'Non-configurable', + ], + ]; + $this->fieldLayoutBuilder->buildForm($build, $display->reveal()); + $this->assertSame($expected, $build); + } + + /** + * @covers ::buildForm + */ + public function testBuildFormNoLayout() { + $this->entityFieldManager->getFieldDefinitions(Argument::any(), Argument::any())->shouldNotBeCalled(); + + $build = [ + 'test1' => [ + '#markup' => 'Test1', + ], + ]; + + $display = $this->prophesize(EntityDisplayWithLayoutInterface::class); + $display->getLayoutId()->willReturn('unknown'); + $display->getLayoutSettings()->willReturn([]); + $display->getComponents()->shouldNotBeCalled(); + + $expected = [ + 'test1' => [ + '#markup' => 'Test1', + ], + ]; + $this->fieldLayoutBuilder->buildForm($build, $display->reveal()); + $this->assertSame($expected, $build); + } + +} -- GitLab